65

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. NOTE: On this server, you can only save MP3 or OGG files.

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 class="noprint"><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.         let maxfilesize = options.maxfilesize;
  41.  
  42.         if (maxfilesize === undefined)
  43.             maxfilesize = 1000000;
  44.         else if (!Number.isInteger(maxfilesize))
  45.             throw new TypeError();
  46.         else if (maxfilesize < 100000)
  47.             throw new RangeError();
  48.  
  49.         this._maxfilesize = maxfilesize;
  50.     }
  51.  
  52.     View.call(this);
  53.  
  54.     if (recorder) {
  55.         if (MediaRecorder.isTypeSupported('audio/ogg'))
  56.             this._mediatype = 'audio/ogg';
  57.         else if (MediaRecorder.isTypeSupported('audio/mpeg'))
  58.             this._mediatype = 'audio/mpeg';
  59.         else if (MediaRecorder.isTypeSupported('audio/webm'))
  60.             this._mediatype = 'audio/webm';
  61.         else
  62.             this._mediatype = null;
  63.  
  64.         recorder = this._mediatype ? true : false;
  65.     }
  66.  
  67.     this._recorder = recorder;
  68.     this._mediarecorder = null;
  69.  
  70.     this._mediablob = null;
  71.  
  72.     this._load = load;
  73.     this._draganddrop = draganddrop;
  74.  
  75.     this._deleteURL = deleteURL;
  76.     this._uploadURL = uploadURL;
  77.  
  78.     this._playWidget = null;
  79.     this._pauseWidget = null;
  80.     this._barWidget = null;
  81.     this._timeWidget = null;
  82.     this._loopWidget = null;
  83.  
  84.     this._loadWidget = null;
  85.     this._fileWidget = null;
  86.  
  87.     this._deleteWidget = null;
  88.     this._uploadWidget = null;
  89.     this._statusWidget = null;
  90.  
  91.     this._recordStartWidget = null;
  92.     this._recordStopWidget = null;
  93.  
  94.     this._seeking = false;
  95.     this._uploading = false;

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.     this._uploadable = false;
  2.  
  3.     this._autoplay = false;
  4.  
  5.     this._error = null;

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

  1.  
  2. AudioPlayer.prototype = Object.create(View.prototype);
  3.  
  4. Object.defineProperty(AudioPlayer.prototype, 'constructor', { value: AudioPlayer, enumerable: false, writable: true });
  5.  
  6. Object.defineProperty(AudioPlayer.prototype, 'duration', {
  7.     get:    function() {
  8.         return this._audio.src ? Math.floor(this._audio.duration * 1000) : 0;
  9.     }
  10. });
  11.  
  12. Object.defineProperty(AudioPlayer.prototype, 'currentTime', {
  13.     get:    function() {
  14.         return this._audio.src ? Math.floor(this._audio.currentTime * 1000) : 0;

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.     set:    function(ms) {
  2.         if (this._recording || this._uploading)
  3.             return;
  4.  
  5.         if (!this._audio.src)
  6.             return;
  7.  
  8.         this._audio.currentTime = ms / 1000;
  9.     }
  10. });
  11.  
  12. Object.defineProperty(AudioPlayer.prototype, 'src', {
  13.     get:    function() {
  14.         return this._audio.src;
  15.     },
  16.     set:    function(url) {
  17.         if (this._recording || this._uploading)
  18.             return;
  19.  
  20.         this._autoplay = this.playing && url;
  21.  
  22.         if (this._audio.src)
  23.             URL.revokeObjectURL(this._audio.src);
  24.  
  25.         if (url)
  26.             this._audio.src = url;
  27.         else {
  28.             this._audio.removeAttribute('src');

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

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.

Server

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('TMP_DIR', ROOT_DIR . DIRECTORY_SEPARATOR . 'tmp');
  2.  
  3. define('AUDIO_MAX_SIZE', 10*1000000);

Defines TMP_DIR - the folder where the audio file is saved - to the name of the subfolder tmp in the root folder of the site and AUDIO_MAX_SIZE - the maximum size of an audio - to 10 MB.

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

Defines the function uploadaudio which is called by the URL /uploadaudio of the site. The arguments $lang and $arglist are not used. The content returned by this action is independent of the user's language. No argument or parameter is expected from the URL.

  1.     $maxfilesize=AUDIO_MAX_SIZE;
  2.  
  3.     $filetypes=array('audio/mpeg', 'audio/mp3', 'audio/ogg', 'video/ogg');

Initializes $maxfilesize to AUDIO_MAX_SIZE and $filetypes to the list of supported MIME types.

  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 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 (!$type or !in_array($type, $filetypes)) {
  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.     goto trashfile;

On this server, writing the file is simulated. To check that the audio has been copied properly by listening to it, comment out this instruction and adapt the rest of the code to save the file in a folder which is writable by the server.

  1.     $name='sound';
  2.  
  3.     switch ($type) {
  4.         case 'audio/mpeg':
  5.         case 'audio/mp3':
  6.             $fname = $name . '.mp3';
  7.             break;
  8.         case 'audio/ogg':
  9.         case 'video/ogg':
  10.             $fname = $name . '.ogg';
  11.             break;
  12.         default:
  13.             goto badrequest;
  14.     }
  15.  
  16.     $file = TMP_DIR . DIRECTORY_SEPARATOR . $fname;

Defines the name of the audio file. The extension of the file name depends on the MIME type of the audio.

  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. trashfile:
  2.     return false;

Returns a plain header 200 Ok without saving the data.

  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. REMINDER: The function head of the iZend library adds a tag such as <script src="/objectivejs/Objective.js"></script> to the <head> section of the document in HTML. Adapt the code to your development environment.

  1. AudioPlayer.prototype.createWidget = function() {
  2.     const htmlaudiocontrols = [
  3.         '<span class="audiocontrols">',
  4.         '<span class="audioplay"><i class="fas fa-3x fa-play-circle"></i></span>',
  5.         '<span class="audiopause"><i class="fas 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="fas 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="fas fa-lg fa-fw fa-microphone"></i></span>',
  15.         '<span class="recordstop"><i class="fas fa-lg fa-fw fa-microphone-alt"></i></span>',
  16.         '<span class="fileload"><i class="fas fa-lg fa-fw fa-file-audio"></i></span>',
  17.         '<span class="audiodelete"><i class="fas fa-lg fa-fw fa-trash"></i></span>',
  18.         '<span class="audioupload"><i class="fas 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].