2

AudioPlayer

An instance of AudioPlayer can play an audio from a server, load an audio from a file on the the local disk an play it, record an audio with a microphone and play it, send a loaded or a recorded audio file to a server, delete an audio file from a server. All functionalities are optional.

REMINDER: The layout and the style of an interface are in the hands of the programmer. No graphical design is imposed. Coding the look of an instance of AudioPlayer is done instantly. The examples on this page use the icons from Font Awesome.

Coordinating an audio player with a playlist managed by an instance of a AudioPlaylist takes just a few lines of code.

Objective
  • Responder
    • View
      • AudioPlayer

Press the play button to start playing the audio. Press the pause button to pause it. Move the slider or click in the progression bar to jump forward or backward in the audio. Turn on or off playing the audio in a loop.

Click on the load button to load an audio file from the local disk. Select a MP3, OGG or WAV file. NOTE: The maximum size of the file is configured to 10 MB. Open a folder containing audios with the explorer of your file system. Drag and drop a MP3, OGG or WAV file over the player. NOTE: Reading AAC and AC3 files is hazardous. Try to read a WEBM and different video containers.

Click on the microphone to start recording an audio. Click on the open microphone to stop the recording.

Once you have loaded or recorded an audio, you can click on the upload button to send the file to the server. The file is transferred in 100 kB blocks. The progression is displayed by a percentage. In case of error, the upload button turns red. Click on the trash can to delete the file on the server. If the server returns an error, the trash can turns red.

NOTE: In the example implementation, writing data and destroying the file are simulated by the server.

To start loading an audio from a file with a button, a code can run the method loadAudio of the player when the button is clicked. IMPORTANT: For security reasons, loadAudio must be called in response to a user interaction.

  1. <p><button id="btn_load_audio" type="submit" class="narrow" title=""><i class="fa fa-file-audio"></i></button></p>
  2. <script>
  3. document.getElementById('btn_load_audio').onclick = () => audioplayer.loadAudio();
  4. </script>

In the console of the browser, display the duration of the audio:

audioplayer.duration

Change the audio in the player:

audioplayer.src='/files/sounds/smoke.ogg'

or

audioplayer.src='/files/sounds/smoke.mp3'

Start playing the audio:

audioplayer.play()

Pause it:

audioplayer.pause()

Destroy the interface:

audioplayer.destroyWidget()

Restart the audio:

audioplayer.replay()

Create and show the interface then reset it:

audioplayer.createManagedWidget().resetWidget()
  1. function AudioPlayer(options = false) {
  2.     const audio = new Audio();
  3.  
  4.     if (! (audio.canPlayType('audio/ogg') || audio.canPlayType('audio/mpeg')) )
  5.         throw new TypeError();
  6.  
  7.     this._audio = audio;
  8.  
  9.     options = options || {};
  10.  
  11.     let recorder = options.recorder ? true : false;
  12.  
  13.     let load = options.load ? true : false;
  14.     let draganddrop = options.draganddrop ? true : false;
  15.  
  16.     let deleteURL = options.deleteURL;
  17.     let uploadURL = options.uploadURL;
  18.  
  19.     if (! (typeof deleteURL === 'undefined' || deleteURL === null || typeof deleteURL === 'string'))
  20.         throw new TypeError();
  21.  
  22.     if (! (typeof uploadURL === 'undefined' || uploadURL === null || typeof uploadURL === 'string'))
  23.         throw new TypeError();
  24.  
  25.     if (! (recorder || load || draganddrop))
  26.         uploadURL = null;
  27.  
  28.     if (uploadURL) {
  29.         let chunksize = options.chunksize;
  30.  
  31.         if (chunksize === undefined)
  32.             chunksize = 100000;
  33.         else if (!Number.isInteger(chunksize))
  34.             throw new TypeError();
  35.         else if (chunksize < 10000)
  36.             throw new RangeError();
  37.  
  38.         this._chunksize = chunksize;
  39.     }
  40.  
  41.     View.call(this);
  42.  
  43.     if (recorder) {
  44.         if (MediaRecorder.isTypeSupported('audio/ogg'))
  45.             this._mediatype = 'audio/ogg';
  46.         else if (MediaRecorder.isTypeSupported('audio/mpeg'))
  47.             this._mediatype = 'audio/mpeg';
  48.         else if (MediaRecorder.isTypeSupported('audio/webm'))
  49.             this._mediatype = 'audio/webm';
  50.         else
  51.             this._mediatype = null;
  52.  
  53.         recorder = this._mediatype ? true : false;
  54.     }
  55.  
  56.     this._recorder = recorder;
  57.     this._mediarecorder = null;
  58.  
  59.     this._mediablob = null;
  60.  
  61.     this._load = load;
  62.     this._draganddrop = draganddrop;
  63.  
  64.     this._deleteURL = deleteURL;
  65.     this._uploadURL = uploadURL;
  66.  
  67.     this._playWidget = null;
  68.     this._pauseWidget = null;
  69.     this._barWidget = null;
  70.     this._timeWidget = null;
  71.     this._loopWidget = null;
  72.  
  73.     this._loadWidget = null;
  74.     this._fileWidget = null;
  75.  
  76.     this._deleteWidget = null;
  77.     this._uploadWidget = null;
  78.     this._statusWidget = null;
  79.  
  80.     this._recordStartWidget = null;
  81.     this._recordStopWidget = null;
  82.  
  83.     this._seeking = false;
  84.     this._uploading = false;
  85.  
  86.     this._deletable = deleteURL ? true : false;
  87.     this._uploadable = false;
  88.  
  89.     this._autoplay = false;
  90.  
  91.     this._error = null;
  92. }
  93.  
  94. AudioPlayer.prototype = Object.create(View.prototype);
  95.  
  96. Object.defineProperty(AudioPlayer.prototype, 'constructor', { value: AudioPlayer, enumerable: false, writable: true });

The AudioPlayer class inherits from the View class. The parameter options of the constructor is an object which configures the options recorder, load, dragandrop, deleteURL, uploadURL and chunksize. If recorder is true, the instance accepts to manage the recording of an audio with the microphone of the browser. If load is true, the instance accepts to load an audio file from the local disk in response to a user interaction. If draganddrop is true, the instance accepts to load an audio file that the user has dropped on the interface. deleteURL is a character string which specifies the URL of the POST sent to the server to delete an audio. uploadURL is a character string which specifies the URL of the POST sent to the server to upload an audio. chunksize specifies the size of the data blocks sent to the server, 100 kB by default.

If the browser cannot play an audio of type audio/ogg or audio/mpeg, the constructor throws an error TypeError.

If one the the options recorder, load or dragandrop isn't true, the instance will never be able to upload an audio and the option uploadURL is set to false.

If chunksize is defined and is not an integer, the constructor throws an error TypeError. If chunksize is a number less than 10000, the constructor throws an error RangeError.

if recorder is true, the constructor checks if the browser can record an audio with the format audio/ogg, audio/mpeg or audio/webm and memorizes it. Otherwise, the option recorder is set to false. NOTE;: The recorder is created in response to the first request for a recording.

  1. Object.defineProperty(AudioPlayer.prototype, 'duration', {
  2.     get:    function() {
  3.                 return this._audio.src ? Math.floor(this._audio.duration * 1000) : 0;
  4.             }
  5. });

duration is an accessor which returns the duration in milliseconds of the audio of this, 0 by default.

  1. Object.defineProperty(AudioPlayer.prototype, 'currentTime', {
  2.     get:    function() {
  3.                 return this._audio.src ? Math.floor(this._audio.currentTime * 1000) : 0;
  4.             },
  5.     set:    function(ms) {
  6.                 if (this._recording || this._uploading)
  7.                     return;
  8.  
  9.                 if (!this._audio.src)
  10.                     return;
  11.  
  12.                 this._audio.currentTime = ms / 1000;
  13.             }
  14. });

currentTime is an accessor which returns or sets the current position in milliseconds of the audio of this. If this is recording or uploading the audio, changing the position of the audio is ignored.

  1. Object.defineProperty(AudioPlayer.prototype, 'src', {
  2.     get:    function() {
  3.                 return this._audio.src;
  4.             },
  5.     set:    function(url) {
  6.                 if (this._recording || this._uploading)
  7.                     return;
  8.  
  9.                 this._autoplay = this.playing && url;
  10.  
  11.                 if (this._audio.src)
  12.                     URL.revokeObjectURL(this._audio.src);
  13.  
  14.                 if (url)
  15.                     this._audio.src = url;
  16.                 else {
  17.                     this._audio.removeAttribute('src');
  18.  
  19.                     if (this._timeWidget)
  20.                         this._showDuration();
  21.                 }
  22.  
  23.                 this._mediablob = null;
  24.  
  25.                 this._uploadable = false;
  26.  
  27.                 this._error = null;
  28.             }
  29. });

src is an accessor which returns or sets the URL of the audio of this. If this is recording or uploading the audio, changing the URL of the audio is ignored.

  1. Object.defineProperty(AudioPlayer.prototype, 'loop', {
  2.     get:    function() {
  3.                 return this._audio.loop;
  4.             },
  5.     set:    function(flag) {
  6.                 this._audio.loop = flag ? true : false;
  7.  
  8.                 if (this._loopWidget) {
  9.                     if (this._audio.loop)
  10.                         this._loopWidget.classList.remove('off');
  11.                     else
  12.                         this._loopWidget.classList.add('off');
  13.                 }
  14.             }
  15. });
  1. Object.defineProperty(AudioPlayer.prototype, 'playing', {
  2.     get:    function() {
  3.                 return this._audio.src && !this._audio.paused;
  4.             }
  5. });
  1. Object.defineProperty(AudioPlayer.prototype, 'recording', {
  2.     get:    function() {
  3.                 return this._mediarecorder && this._mediarecorder.state !== 'inactive';
  4.             }
  5. });
  1. AudioPlayer.prototype.setFromTracks = function(soundtracks) {
  2.     let url = null;
  3.  
  4.     if (soundtracks) {
  5.         if ('audio/wav' in soundtracks && this._audio.canPlayType('audio/wav')) {
  6.             url = soundtracks['audio/wav'];
  7.         }
  8.         else if ('audio/ogg' in soundtracks && this._audio.canPlayType('audio/ogg')) {
  9.             url = soundtracks['audio/ogg'];
  10.         }
  11.         else if ('audio/mpeg' in soundtracks && this._audio.canPlayType('audio/mpeg')) {
  12.             url = soundtracks['audio/mpeg'];
  13.         }
  14.         else if ('audio/webm' in soundtracks && this._audio.canPlayType('audio/webm')) {
  15.             url = soundtracks['audio/webm'];
  16.         }
  17.     }
  18.  
  19.     return this.src = url;
  20. }
  1. AudioPlayer.prototype.setFromAudio = function(w) {
  2.     if (w.tagName != 'AUDIO')
  3.         throw new TypeError();
  4.  
  5.     const soundtracks = [];
  6.  
  7.     for (let source of w.querySelectorAll('source'))
  8.         soundtracks[source.getAttribute('type')] = source.getAttribute('src');
  9.  
  10.     return this.setFromTracks(soundtracks);
  11. }
  1. AudioPlayer.prototype.canPlayType = function(type) {
  2.     return this._audio.canPlayType(type) ? true : false;
  3. }
  1. AudioPlayer.prototype.play = function() {
  2.     if (!this._audio.src)
  3.         return this;
  4.  
  5.     if (this._recording)
  6.         return this;
  7.  
  8.     this._audio.play();
  9.  
  10.     return this;
  11. }
  1. AudioPlayer.prototype.pause = function() {
  2.     if (!this._audio.src)
  3.         return this;
  4.  
  5.     if (this._recording)
  6.         return this;
  7.  
  8.     this._audio.pause();
  9.  
  10.     return this;
  11. }
  1. AudioPlayer.prototype.replay = function() {
  2.     if (!this._audio.src)
  3.         return this;
  4.  
  5.     if (this._recording)
  6.         return this;
  7.  
  8.     this._audio.currentTime = 0;
  9.  
  10.     this._audio.play();
  11.  
  12.     return this;
  13. }
  1. AudioPlayer.prototype.resetWidget = function() {
  2.     if (this._playWidget && this._pauseWidget) {
  3.         if (!this._audio.src || this._recording) {
  4.             this._playWidget.classList.add('disabled');
  5.             this._pauseWidget.classList.add('disabled');
  6.         }
  7.         else {
  8.             this._playWidget.classList.remove('disabled');
  9.             this._pauseWidget.classList.remove('disabled');
  10.         }
  11.     }
  12.  
  13.     if (this._barWidget ) {
  14.         if (!this._audio.src || this._recording)
  15.             this._barWidget.disabled = true;
  16.         else
  17.             this._barWidget.disabled = false;
  18.     }
  19.  
  20.     if (this._loopWidget ) {
  21.         if (this._audio.loop)
  22.             this._loopWidget.classList.remove('off');
  23.         else
  24.             this._loopWidget.classList.add('off');
  25.     }
  26.  
  27.     if (this._uploadWidget) {
  28.         if (this._uploadable && !this._uploading && !this._recording)
  29.             this._uploadWidget.classList.remove('disabled');
  30.         else
  31.             this._uploadWidget.classList.add('disabled');
  32.  
  33.         if (this._error == 'upload')
  34.             this._uploadWidget.classList.add('inerror');
  35.         else
  36.             this._uploadWidget.classList.remove('inerror');
  37.     }
  38.  
  39.     if (this._deleteWidget) {
  40.         if (this._deletable && !this._uploading)
  41.             this._deleteWidget.classList.remove('disabled');
  42.         else
  43.             this._deleteWidget.classList.add('disabled');
  44.  
  45.         if (this._error == 'delete')
  46.             this._deleteWidget.classList.add('inerror');
  47.         else
  48.             this._deleteWidget.classList.remove('inerror');
  49.     }
  50.  
  51.     if (this._recordStartWidget && this._recordStopWidget) {
  52.         if (!this._recorder || this._uploading) {
  53.             this._recordStartWidget.classList.add('disabled');
  54.             this._recordStopWidget.classList.add('disabled');
  55.         }
  56.         else {
  57.             this._recordStartWidget.classList.remove('disabled');
  58.             this._recordStopWidget.classList.remove('disabled');
  59.  
  60.             if (this._error == 'record')
  61.                 this._recordStartWidget.classList.add('inerror');
  62.             else
  63.                 this._recordStartWidget.classList.remove('inerror');
  64.         }
  65.     }
  66.  
  67.     if (this._loadWidget) {
  68.         if (this._recording || this._uploading)
  69.             this._loadWidget.classList.add('disabled');
  70.         else
  71.             this._loadWidget.classList.remove('disabled');
  72.  
  73.         if (this._error == 'load')
  74.             this._loadWidget.classList.add('inerror');
  75.         else
  76.             this._loadWidget.classList.remove('inerror');
  77.     }
  78.  
  79.     return this;
  80. }
  1. AudioPlayer.prototype.setWidget = function(w) {
  2.     View.prototype.setWidget.call(this, w);
  3.  
  4.     this._playWidget = w.querySelector('.audioplay');
  5.     this._pauseWidget = w.querySelector('.audiopause');
  6.  
  7.     this._loopWidget = w.querySelector('.audioloop');
  8.  
  9.     this._timeWidget = w.querySelector('.audiotime');
  10.  
  11.     this._barWidget = w.querySelector('.audiobar');
  12.  
  13.     if (this._barWidget && this._barWidget.tagName != 'INPUT')
  14.         this._barWidget = null;
  15.  
  16.     this._recordStartWidget = w.querySelector('.recordstart');
  17.     this._recordStopWidget = w.querySelector('.recordstop');
  18.  
  19.     this._loadWidget = w.querySelector('.fileload');
  20.     this._fileWidget = w.querySelector('.mediafile');
  21.  
  22.     if (this._fileWidget && this._fileWidget.tagName != 'INPUT')
  23.         this._fileWidget = null;
  24.  
  25.     this._deleteWidget = w.querySelector('.audiodelete');
  26.     this._uploadWidget = w.querySelector('.audioupload');
  27.  
  28.     this._statusWidget = w.querySelector('.mediastatus');
  29.  
  30.     const playing = this._audio.src && !this._audio.paused;
  31.     const recording = this._mediarecorder && this._mediarecorder.state !== 'inactive';
  32.  
  33.     if (this._playWidget) {
  34.         this._playWidget.hidden = playing ? true : false;
  35.  
  36.         this._playWidget.addEventListener('click', () => {
  37.             if (!this._playWidget.classList.contains('disabled'))
  38.                 this.play();
  39.         });
  40.     }
  41.  
  42.     if (this._pauseWidget) {
  43.         this._pauseWidget.hidden = playing ? false : true;
  44.  
  45.         this._pauseWidget.addEventListener('click', () => {
  46.             if (!this._pauseWidget.classList.contains('disabled'))
  47.                 this.pause();
  48.         });
  49.     }
  50.  
  51.     if (this._loopWidget) {
  52.         this._loopWidget.addEventListener('click', () => {
  53.             if (!this._loopWidget.classList.contains('disabled'))
  54.                 this.loop = !this.loop;
  55.         });
  56.     }
  57.  
  58.     if (this._barWidget) {
  59.         if (!playing) {
  60.             this._barWidget.value = (this._audio.currentTime ? Math.floor(this._audio.currentTime / this._audio.duration * 100) : 0);
  61.             this._showCurrentTime();
  62.         }
  63.  
  64.         this._barWidget.addEventListener('mousedown', () => this._seeking = true);
  65.         this._barWidget.addEventListener('mouseup', () => this._seeking = false);
  66.  
  67.         this._barWidget.addEventListener('change', () => {
  68.             const duration = this._audio.src ? this._audio.duration : 0;
  69.  
  70.             if (duration) {
  71.                 let secs = this._barWidget.value / 100 * duration;
  72.  
  73.                 secs = secs < this._audio.currentTime ? Math.floor(secs) : Math.ceil(secs);
  74.  
  75.                 this._audio.currentTime = this._audio.loop && secs >= duration ? 0 : secs;
  76.             }
  77.         });
  78.     }
  79.  
  80.     if (this._draganddrop) {
  81.         w.addEventListener('drop', (e) => {
  82.             const dt = e.dataTransfer;
  83.  
  84.             e.preventDefault();
  85.  
  86.             if (dt.types.indexOf('Files') != -1) {
  87.                 this._loadFile(dt.files[0]);
  88.             }
  89.         });
  90.  
  91.         w.addEventListener('dragenter', (e) => {
  92.             const dt = e.dataTransfer;
  93.  
  94.             if (dt.types.indexOf('Files') != -1) {
  95.                 e.preventDefault();
  96.             }
  97.         });
  98.  
  99.         w.addEventListener('dragleave', (e) => {
  100.             e.preventDefault();
  101.         });
  102.  
  103.         w.addEventListener('dragover', (e) => {
  104.             const dt = e.dataTransfer;
  105.  
  106.             e.preventDefault();
  107.  
  108.             dt.dropEffect = dt.types.indexOf('Files') != -1 && !(this._recording || this._uploading) ? 'copy' : 'none';
  109.         });
  110.     }
  111.  
  112.     if (this._fileWidget)
  113.         this._fileWidget.hidden = true;
  114.  
  115.     if (this._loadWidget) {
  116.         this._loadWidget.classList.add('disabled');
  117.  
  118.         if (this._fileWidget) {
  119.             if (this._load) {
  120.                 this._loadWidget.addEventListener('click', () => {
  121.                     if (!this._loadWidget.classList.contains('disabled') && !(this._recording || this._uploading))
  122.                         this._fileWidget.click();
  123.                 });
  124.  
  125.                 this._fileWidget.addEventListener('change', (e) => {
  126.                     if (e.target.value) {
  127.                         this._loadFile(e.target.files[0]);
  128.                     }
  129.                 });
  130.             }
  131.         }
  132.         else
  133.             this._loadWidget = null;
  134.     }
  135.  
  136.     if (this._recordStartWidget) {
  137.         this._recordStartWidget.classList.add('disabled');
  138.         this._recordStartWidget.hidden = recording ? true : false;
  139.  
  140.         if (this._recorder) {
  141.             this._recordStartWidget.addEventListener('click', () => {
  142.                 if (!this._recordStartWidget.classList.contains('disabled'))
  143.                     this.recordStart();
  144.             });
  145.         }
  146.         else
  147.             this._recordStartWidget = null;
  148.     }
  149.  
  150.     if (this._recordStopWidget) {
  151.         this._recordStopWidget.hidden = recording ? false : true;
  152.  
  153.         if (this._recorder) {
  154.             this._recordStopWidget.addEventListener('click', () => {
  155.                 if (!this._recordStopWidget.classList.contains('disabled'))
  156.                     this.recordStop();
  157.             });
  158.         }
  159.         else
  160.             this._recordStopWidget = null;
  161.     }
  162.  
  163.     if (this._uploadWidget) {
  164.         this._uploadWidget.classList.add('disabled');
  165.  
  166.         if (this._uploadURL) {
  167.             this._uploadWidget.addEventListener('click', () => {
  168.                 if (!this._uploadWidget.classList.contains('disabled'))
  169.                     this.uploadFile();
  170.             });
  171.         }
  172.         else
  173.             this._uploadWidget = null;
  174.     }
  175.  
  176.     if (this._deleteWidget) {
  177.         this._deleteWidget.classList.add('disabled');
  178.  
  179.         if (this._deleteURL) {
  180.             this._deleteWidget.addEventListener('click', () => {
  181.                 if (!this._deleteWidget.classList.contains('disabled'))
  182.                     this.deleteFile();
  183.             });
  184.         }
  185.         else
  186.             this._deleteWidget = null;
  187.     }
  188.  
  189.     this._audio.onloadedmetadata = () => {
  190.         if (this.interfaced())
  191.             this.resetWidget();
  192.  
  193.         this._showDuration();
  194.  
  195.         if (this._autoplay)
  196.             this._audio.play();
  197.     }
  198.  
  199.     this._audio.ontimeupdate = () => {
  200.         if (this._barWidget && !this._seeking)
  201.             this._barWidget.value = (this._audio.currentTime ? Math.floor(this._audio.currentTime / this._audio.duration * 100) : 0);
  202.  
  203.         this._showCurrentTime();
  204.     };
  205.  
  206.     this._audio.onplay = () => {
  207.         if (this._playWidget && this._pauseWidget) {
  208.             this._playWidget.hidden = true;
  209.             this._pauseWidget.hidden = false;
  210.         }
  211.  
  212.         this.notify('audioPlayed', this);
  213.     };
  214.  
  215.     this._audio.onpause = () => {
  216.         if (this._playWidget && this._pauseWidget) {
  217.             this._pauseWidget.hidden = true;
  218.             this._playWidget.hidden = false;
  219.         }
  220.  
  221.         this.notify('audioPaused', this);
  222.     };
  223.  
  224.     this._audio.onended = () => {
  225.         if (this._playWidget && this._pauseWidget) {
  226.             this._pauseWidget.hidden = true;
  227.             this._playWidget.hidden = false;
  228.         }
  229.  
  230.         this.notify('audioEnded', this);
  231.     };
  232.  
  233.     this._audio.onerror = () => {
  234.         if (this._playWidget && this._pauseWidget) {
  235.             this._pauseWidget.hidden = true;
  236.             this._playWidget.hidden = false;
  237.         }
  238.  
  239.         this._audio.removeAttribute('src');
  240.  
  241.         if (this.interfaced())
  242.             this.resetWidget();
  243.  
  244.         this._showDuration();
  245.  
  246.         this.notify('audioError', this);
  247.     }
  248.  
  249.     return this;
  250. }
  1. AudioPlayer.prototype.destroyWidget = function() {
  2.     View.prototype.destroyWidget.call(this);
  3.  
  4.     this._playWidget = null;
  5.     this._pauseWidget = null;
  6.     this._barWidget = null;
  7.     this._timeWidget = null;
  8.     this._loopWidget = null;
  9.  
  10.     this._loadWidget = null;
  11.     this._fileWidget = null;
  12.  
  13.     this._deleteWidget = null;
  14.     this._uploadWidget = null;
  15.     this._statusWidget = null;
  16.  
  17.     this._recordStartWidget = null;
  18.     this._recordStopWidget = null;
  19.  
  20.     return this;
  21. }
  1. AudioPlayer.prototype.recordStart = function() {
  2.     if (!this._recorder)
  3.         return this;
  4.  
  5.     if (this._recording || this._uploading)
  6.         return this;
  7.  
  8.     this._audio.pause();
  9.  
  10.     this._recording = true;
  11.  
  12.     this._error = null;
  13.  
  14.     if (this.interfaced())
  15.         this.resetWidget();
  16.  
  17.     if (this._mediarecorder === null) {
  18.         const options = {mimeType: this._mediatype, audioBitsPerSecond: 128000};
  19.  
  20.         navigator.mediaDevices.getUserMedia({audio: true}).then((stream) => {
  21.             this._mediarecorder = new MediaRecorder(stream, options);
  22.             this._mediarecorder.ignoreMutedMedia = true;
  23.  
  24.             let recordedtime = 0;
  25.             let mediachunks = [];
  26.  
  27.             this._mediarecorder.ondataavailable = (e) => {
  28.                 if (this._timeWidget)
  29.                     this._timeWidget.innerText = AudioPlayer._toHHMMSS(++recordedtime);
  30.  
  31.                 mediachunks.push(e.data);
  32.             };
  33.  
  34.             this._mediarecorder.onstart = () => {
  35.                 if (this._recordStartWidget && this._recordStopWidget) {
  36.                     this._recordStartWidget.hidden = true;
  37.                     this._recordStopWidget.hidden = false;
  38.                 }
  39.  
  40.                 if (this._barWidget)
  41.                     this._barWidget.value = 0;
  42.  
  43.                 if (this._timeWidget)
  44.                     this._timeWidget.innerText = AudioPlayer._toHHMMSS(recordedtime = 0);
  45.  
  46.                 this.notify('audioRecordStarted', this);
  47.             };
  48.  
  49.             this._mediarecorder.onstop = () => {
  50.                 if (this._recordStartWidget && this._recordStopWidget) {
  51.                     this._recordStartWidget.hidden = false;
  52.                     this._recordStopWidget.hidden = true;
  53.                 }
  54.  
  55.                 const blob = new Blob(mediachunks.splice(0, mediachunks.length), {type: this._mediatype});
  56.  
  57.                 if (this._audio.src)
  58.                     URL.revokeObjectURL(this._audio.src);
  59.  
  60.                 this._audio.src = URL.createObjectURL(blob);
  61.  
  62.                 this._mediablob = blob;
  63.  
  64.                 this._recording = false;
  65.  
  66.                 this._uploadable = true;
  67.  
  68.                 if (this.interfaced())
  69.                     this.resetWidget();
  70.  
  71.                 this.notify('audioRecordStopped', this);
  72.             };
  73.  
  74.             this._mediarecorder.onerror = () => {
  75.                 if (this._recordStartWidget && this._recordStopWidget) {
  76.                     this._recordStartWidget.addClass('inerror');
  77.  
  78.                     this._recordStartWidget.hidden = false;
  79.                     this._recordStopWidget.hidden = true;
  80.                 }
  81.  
  82.                 this._recording = false;
  83.  
  84.                 this._error = 'record';
  85.  
  86.                 if (this.interfaced())
  87.                     this.resetWidget();
  88.  
  89.                 this.notify('audioRecordError', this);
  90.             };
  91.  
  92.             this._mediarecorder.start(1000);
  93.         })
  94.         .catch((err) => {
  95.             this._recorder = false;
  96.  
  97.             this._recording = false;
  98.  
  99.             if (this.interfaced())
  100.                 this.resetWidget();
  101.         });
  102.     }
  103.     else
  104.         this._mediarecorder.start(1000);
  105.  
  106.     return this;
  107. }
  1. AudioPlayer.prototype.recordStop = function() {
  2.     if (this._mediarecorder)
  3.         this._mediarecorder.stop();
  4.  
  5.     return this;
  6. }
  1. AudioPlayer.prototype.uploadFile = function() {
  2.     if (!this._uploadURL)
  3.         return this;
  4.  
  5.     if (!this._mediablob)
  6.         return this;
  7.  
  8.     if (this._recording || this._uploading)
  9.         return this;
  10.  
  11.     const mediablob = this._mediablob;
  12.     const uploadurl = this._uploadURL;
  13.     const chunksize = this._chunksize;
  14.  
  15.     const filesize = mediablob.size;
  16.     const filetype = mediablob.type;
  17.  
  18.     const filereader = new FileReader();
  19.  
  20.     filereader.onloadend = (e) => postdata(e.target.result);
  21.  
  22.     let offset = 0, progress = 0, blob;
  23.  
  24.     const uploadslice = () => {
  25.         if (this._statusWidget && filesize > chunksize)
  26.             this._statusWidget.innerText = `${progress}%`;
  27.  
  28.         blob = mediablob.slice(offset, offset + chunksize);
  29.         filereader.readAsDataURL(blob);
  30.     }
  31.  
  32.     const postdata = (data) => {
  33.         $.post(uploadurl, {file_size: filesize, file_type: filetype, file_offset: offset, file_data: data})
  34.             .done(() => {
  35.                 offset += blob.size;
  36.                 progress = Math.floor((offset / filesize) * 100);
  37.  
  38.                 if (progress < 100)
  39.                     uploadslice();
  40.                 else {
  41.                     if (this._statusWidget)
  42.                         this._statusWidget.innerText = '';
  43.  
  44.                     this._uploading = false;
  45.  
  46.                     this._uploadable = false;
  47.                     this._deletable = true;
  48.  
  49.                     this._error = null;
  50.  
  51.                     if (this.interfaced())
  52.                         this.resetWidget();
  53.  
  54.                     this.notify('audioUploaded', this);
  55.                 }
  56.             })
  57.             .fail(() => {
  58.                 if (this._statusWidget)
  59.                     this._statusWidget.innerText = '';
  60.  
  61.                 this._uploading = false;
  62.  
  63.                 this._error = 'upload';
  64.  
  65.                 if (this.interfaced())
  66.                     this.resetWidget();
  67.  
  68.                 this.notify('audioError', this);
  69.             });
  70.     };
  71.  
  72.     this._uploading = true;
  73.  
  74.     if (this.interfaced())
  75.         this.resetWidget();
  76.  
  77.     uploadslice();
  78.  
  79.     return this;
  80. }
  81.  
  82. AudioPlayer.prototype.deleteFile = function() {
  83.     if (!this._deleteURL)
  1.  
  2.     if (this._uploading)
  3.         return this;
  4.  
  5.     const deleteurl = this._deleteURL;
  6.  
  7.     const deletefile = () => {
  8.         $.post(deleteurl)
  9.             .done(() => {
  10.                 this._uploadable = this._mediablob ? true : false;
  11.                 this._deletable = false;
  12.  
  13.                 this._error = null;
  14.  
  15.                 if (this.interfaced())
  16.                     this.resetWidget();
  17.  
  18.                 this.notify('audioDeleted', this);
  19.             })
  20.             .fail(() => {
  21.                 this._error = 'delete';
  22.  
  23.                 if (this.interfaced())
  24.                     this.resetWidget();
  25.  
  26.                 this.notify('audioError', this);
  27.             });
  28.     };
  29.  
  30.     deletefile();
  31.  
  32.     return this;
  33. }
  1. AudioPlayer.prototype.loadAudio = function() {
  2.     if (this._recording || this._uploading)
  3.         return this;
  4.  
  5.     if (this._fileWidget)
  6.         this._fileWidget.click();
  7.  
  8.     return this;
  9. }
  1. AudioPlayer.prototype._loadFile = function(fd) {
  2.     if (!this._audio.canPlayType(fd.type)) {
  3.         this._error = 'load';
  4.  
  5.         this.resetWidget();
  6.  
  7.         return;
  8.     }
  9.  
  10.     this._autoplay = this.playing;
  11.  
  12.     if (this._audio.src)
  13.         URL.revokeObjectURL(this._audio.src);
  14.  
  15.     this.src = URL.createObjectURL(fd);
  16.  
  17.     this._mediablob = fd;
  18.  
  19.     this._uploadable = true;
  20.  
  21.     this._error = null;
  22.  
  23.     this.resetWidget();
  24.  
  25.     this.notify('audioLoaded', this);
  26. }
  1. AudioPlayer.prototype._showDuration = function() {
  2.     if (this._timeWidget)
  3.         this._timeWidget.innerText = AudioPlayer._toHHMMSS(this._audio.src ? this._audio.duration : 0);
  4. }
  1. AudioPlayer.prototype._showCurrentTime = function() {
  2.     if (this._timeWidget)
  3.         this._timeWidget.innerText = AudioPlayer._toHHMMSS(this._audio.src ? this._audio.currentTime : 0);
  4. }
  1. AudioPlayer._toHHMMSS = function(nsecs) {
  2.     nsecs = Math.floor(nsecs);
  3.  
  4.     let hh = Math.floor(nsecs / 3600);
  5.     let mm = Math.floor((nsecs - (hh * 3600)) / 60);
  6.     let ss = nsecs - (hh * 3600) - (mm * 60);
  7.  
  8.     hh = (hh < 10 ? '0' : '') + hh;
  9.     mm = (mm < 10 ? '0' : '') + mm;
  10.     ss = (ss < 10 ? '0' : '') + ss;
  11.  
  12.     return `${hh}:${mm}:${ss}`;
  13. }

The audio is copied in the disk space of the server in response to a POST by a function which extracts from it the MIME type and the size of the file, the block of data which is transferred, its size and its position in the file. A block of audio data is encoded in BASE64.

In iZend, the file uploadaudio.php is installed directly in the folder actions. To activate the URL /uploadaudio in a website, an entry is added in the file aliases.inc to the list of URLs common to all languages.

  1. $aliases = array(
  2.     array(
  3.         'captcha' => 'captcha',
...
  1.         'uploadaudio' => 'uploadaudio',
  2.     ),
  3.     'en'    => array(
...
  1.     ),
  2. );

Adapt the installation of this function to your development environment.

  1. define('AUDIO_MAX_SIZE', 10*1000000);

Defines AUDIO_MAX_SIZE - the maximum size of an audio - to 5 MB.

  1. function uploadaudio($lang, $arglist=false) {

Defines the function uploadaudio which is called by the URL /uploadaudio of the site. The arguments $lang - the language of the output, which will always be false - and $arglist - the parameters passed in the URL - are not used.

  1.     $maxfilesize=AUDIO_MAX_SIZE;

Initializes $maxfilesize to AUDIO_MAX_SIZE.

  1.     $type=$data=false;
  2.     $size=$offset=0;

Initializes the variables of the arguments of the POST.

  1.     if (isset($_POST['file_size'])) {
  2.         $size=$_POST['file_size'];
  3.     }
  4.     if (isset($_POST['file_type'])) {
  5.         $type=$_POST['file_type'];
  6.     }
  7.     if (isset($_POST['file_offset'])) {
  8.         $offset=$_POST['file_offset'];
  9.     }
  10.     if (isset($_POST['file_data'])) {
  11.         $data=explode(';base64,', $_POST['file_data']);
  12.         $data=is_array($data) && isset($data[1]) ? base64_decode($data[1]) : false;
  13.     }

Extracts the arguments of the POST. The block of image data encoded in BASE64 is decoded.

  1.     if (($offset = filter_var($offset, FILTER_VALIDATE_INT, array('options' => array('min_range' => 0)))) === false)
  2.         goto badrequest;
  3.  
  4.     if (($size = filter_var($size, FILTER_VALIDATE_INT, array('options' => array('min_range' => 0, 'max_range' => $maxfilesize))))  === false)
  5.         goto badrequest;
  6.  
  7.     if (! (substr($type, 0, 5) == 'audio' or substr($type, 0, 5) == 'video')) {
  8.         goto badrequest;
  9.     }

Checks the arguments of the POST.

  1.     if (!$data) {
  2.         goto badrequest;
  3.     }
  4.  
  5.     $datasize=strlen($data);
  6.  
  7.     if ($offset + $datasize > $size) {
  8.         goto badrequest;
  9.     }

Checks the size of the data sent in the POST.

  1.     $file = '/dev/null';

Defines the name of file to /dev/null, i. e. writing the file is simulated. NOTE: To check that the audio has been copied properly by listening to it, replace /dev/null by the name of file, e.g. audio.ogg or audio.mp3, in a folder which is writable by the server.

  1.     $fout = @fopen($file, $offset == 0 ? 'wb' : 'cb');
  2.  
  3.     if ($fout === false) {
  4.         goto internalerror;
  5.     }

Opens the audio file in binary mode. If the POST contains the first block of data, the file is created.

  1.     $r = fseek($fout, $offset);
  2.  
  3.     if ($r == -1) {
  4.         goto internalerror;
  5.     }

Moves the file pointer to the position of the block of data.

  1.     $r = fwrite($fout, $data);
  2.  
  3.     if ($r === false) {
  4.         goto internalerror;
  5.     }

Writes the block of data.

  1.     if ($offset + $datasize < $size) {
  2.         return false;
  3.     }

Returns a plain header 200 Ok if more blocks of data are expected.

  1.     return false;

Returns a plain header 200 Ok if al the blocks of data have been saved. NOTE: The code leaves room to process the file like a conversion in different formats with FFmpeg.

  1. badrequest:
  2.     header('HTTP/1.1 400 Bad Request');
  3.     return false;

Returns a plain header 400 Bad Request if an element of the POST is invalid.

  1. internalerror:
  2.     header('HTTP/1.1 500 Internal Error');
  3.     return false;
  4. }

Returns a plain header 500 Internal Error if the image couldn't be saved properly.

To ask the server to delete an audio, the player sends a simple POST to the server with no arguments.

  1. function donothing($lang, $arglist=false) {
  2.     return false;
  3. }

Defines the function donothing which is called by the URL /deleteaudio of the site. donothing does nothing and always returns a plain header 200 Ok , i. e. deleting an audio file is simulated.

Test
  1. <?php $debug=true; ?>

Setting $debug to true gives access in the console of the navigator to the variable audioplayer, the instance of the AudioPlayer. If $debug is false, all the code in JavaScript is protected by a closure function.

  1. <?php $upload_url='/uploadaudio'; ?>
  2. <?php $delete_url='/deleteaudio'; ?>
  3. <?php $chunksize=100000; ?>

Defines the URLs of the functions to save and to delete an audio on the server, the size of the data blocks sent to the server.

  1. <?php $id=uniqid('id'); ?>

Defines the identifier of the <div> which surrounds the HTML of the audio player.

  1. <div id="<?php echo $id; ?>" class="noprint">
  2. <audio controls preload="metadata">
  3. <source src="/files/sounds/thanatos.mp3" type="audio/mpeg" />
  4. <source src="/files/sounds/thanatos.ogg" type="audio/ogg" />
  5. </audio>
  6. </div>

Defines a <audio> element whose parameters <source> will used to configure the audio player. This element will be hidden by the code in JavaScript.

  1. <?php head('javascript', '/objectivejs/Objective.js'); ?>
  2. <?php head('javascript', '/objectivejs/Responder.js'); ?>
  3. <?php head('javascript', '/objectivejs/View.js'); ?>
  4. <?php head('javascript', '/objectivejs/AudioPlayer.js'); ?>

Includes the code of all the necessary classes.

  1. AudioPlayer.prototype.createWidget = function() {
  2.     const htmlaudiocontrols = [
  3.         '<span class="audiocontrols">',
  4.         '<span class="audioplay"><i class="fa fa-3x fa-play-circle"></i></span>',
  5.         '<span class="audiopause"><i class="fa fa-3x fa-pause-circle"></i></span>',
  6.         '<input class="audiobar" type="range" min="0" max="100" step="1" value="0"/>',
  7.         '<span class="audiotime">00:00:00</span>',
  8.         '<span class="audioloop"><i class="fa fa-sm fa-sync-alt"></i></span>',
  9.         '</span>'
  10.     ].join('\n');
  11.  
  12.     const htmlmediacontrols = [
  13.         '<span class="mediacontrols">',
  14.         '<span class="recordstart"><i class="fa fa-lg fa-fw fa-microphone"></i></span>',
  15.         '<span class="recordstop"><i class="fa fa-lg fa-fw fa-microphone-alt"></i></span>',
  16.         '<span class="fileload"><i class="fa fa-lg fa-fw fa-file-audio"></i></span>',
  17.         '<span class="audiodelete"><i class="fa fa-lg fa-fw fa-trash"></i></span>',
  18.         '<span class="audioupload"><i class="fa fa-lg fa-fw fa-file-export"></i></span>',
  19.         '<span class="mediastatus"></span>',
  20.         '</span>',
  21.         '<input class="mediafile" type="file" accept="audio/*"/>'
  22.     ].join('\n');
  23.  
  24.     const html='<div class="ojs_audio">' + '\n' + htmlaudiocontrols + '\n' + htmlmediacontrols + '\n' + '</div>';
  25.  
  26.     let template = document.createElement('template');
  27.  
  28.     template.innerHTML = html;
  29.  
  30.     let widget = template.content.children[0];
  31.  
  32.     this.setWidget(widget);
  33.  
  34.     return this;
  35. }

Defines the method createWidget of an instance of AudioPlayer. NOTE: This example uses the icons from Font Awesome. Adapt it to your development environment.

The HTML of the interface can also be created directly in the document and passed to setWidget. See AudioPlaylist.

  1. <?php if (!$debug): ?>
  2. (function() {
  3. <?php endif; ?>

Isolates all the code in JavaScript in a closure function if $debug is false.

  1.     const options = {
  2.         recorder: true,
  3.         load: true,
  4.         draganddrop: true,
  5.         deleteURL: '<?php echo $delete_url; ?>',
  6.         uploadURL: '<?php echo $upload_url; ?>',
  7.         chunksize: <?php echo $chunksize; ?>
  8.     }
  9.  
  10.     const audioplayer = new AudioPlayer(options);

Configures and creates an instance of AudioPlayer. NOTE: Play with the options to see how the interface is configured. Comment out widgets in createWidget to change the look of the player.

  1.     const container = document.querySelector('#<?php echo $id; ?>');

Retrieves the <div> which surrounds the HTML of the program.

  1.     const audio = container.querySelector('audio');
  2.  
  3.     audioplayer.setFromAudio(audio);

Configures the audio source of the player with the original <audio> element.

  1.     audioplayer.createManagedWidget(container).resetWidget();

Creates and displays the interface of the audio player.

  1.     audio.hidden = true;

Hides the original <audio> element.

  1. <?php if (!$debug): ?>
  2. })();
  3. <?php endif; ?>

Closes the function which isolates the code in JavaScript if $debug is false.

SEE ALSO

View, AudioPlaylist, Wall, Write data on a server

Comments

Your comment:
[p] [b] [i] [u] [s] [quote] [pre] [br] [code] [url] [email] strip help 2000

Enter a maximum of 2000 characters.
Improve the presentation of your text with the following formatting tags:
[p]paragraph[/p], [b]bold[/b], [i]italics[/i], [u]underline[/u], [s]strike[/s], [quote]citation[/quote], [pre]as is[/pre], [br]line break,
[url]http://www.izend.org[/url], [url=http://www.izend.org]site[/url], [email]izend@izend.org[/email], [email=izend@izend.org]izend[/email],
[code]command[/code], [code=language]source code in c, java, php, html, javascript, xml, css, sql, bash, dos, make, etc.[/code].