73

AudioPlayer

Une instance de AudioPlayer peut jouer une audio à partir d'un serveur, charger une audio à partir d'un fichier sur le disque local et la jouer, enregistrer une audio avec un microphone et la jouer, envoyer un fichier audio chargé ou enregistré à un serveur, détruire un fichier audio sur un serveur. Toutes les fonctionnalités sont optionnelles.

RAPPEL : La disposition et le style d'une interface sont à la discrétion du programmeur. Aucun modèle graphique n'est imposé. Coder l'apparence d'une instance de AudioPlayer prend quelques instants. Les exemples sur cette page utilisent les icônes de Font Awesome.

Coordonner un lecteur audio avec une liste de pistes gérées par une instance de AudioPlaylist ne prend que quelques lignes de code.

Objective
  • Responder
    • View
      • AudioPlayer

Appuyez sur le bouton lecture pour démarrer la lecture de l'audio. Appuyez sur le bouton pause pour la mettre en pause. Déplacez le glisseur ou cliquez dans la barre de progression pour avancer ou reculer dans l'audio. Activez ou désactivez la lecture en boucle.

Cliquez sur le bouton de chargement pour charger un fichier audio à partir du disque local. Sélectionnez un fichier MP3, OGG ou WAV. NOTE : La taille maximum du fichier est configurée à 10 Mo. Ouvrez un dossier contenant des audios avec l'explorateur de votre système de fichiers. Faites glisser et déposer un fichier MP3, OGG ou WAV sur le lecteur. NOTE : Lire des fichiers AAC et AC3 est aléatoire. Essayez de lire un WEBM et différents conteneurs vidéo.

Cliquez sur le microphone pour commencer l'enregistrement d'une audio. Cliquez sur le microphone ouvert pour arrêter l'enregistrement.

Dès que vous avez chargé ou enregistré une audio, vous pouvez cliquer sur le bouton de téléchargement pour envoyer le fichier au serveur. Le fichier est transféré par blocs de 100 ko. La progression est affichée par un pourcentage. En cas d'erreur, le bouton de téléchargement vire au rouge. NOTE : Sur ce serveur, vous ne pouvez sauvegarder que des fichiers MP3 ou OGG.

Cliquez sur la poubelle pour détruire le fichier sur le serveur. Si le serveur retourne une erreur, la poubelle vire au rouge.

NOTE : Dans cet exemple d'implémentation, l'écriture des données et la destruction du fichier sont simulées par le serveur.

Pour lancer le chargement d'une audio à partir d'un fichier avec un bouton, le code peut exécuter la méthode loadAudio du lecteur quand le bouton est cliqué. IMPORTANT : Pour des raisons de sécurité, loadAudio doit être appelée en réponse à une interaction de l'utilisateur.

  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>

Dans la console du navigateur, affichez la durée de l'audio :

audioplayer.duration

Changez l'audio dans le lecteur :

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

ou

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

Lancez la lecture de l'audio :

audioplayer.play()

Mettez-la en pause :

audioplayer.pause()

Détruisez l'interface :

audioplayer.destroyWidget()

Redémarrez l'audio :

audioplayer.replay()

Créez et montrez l'interface puis réinitialisez-la :

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;

La classe AudioPlayer hérite de la classe View. Le paramètre options du constructeur est un objet qui configure les options recorder, load, dragandrop, deleteURL, uploadURL et chunksize. Si recorder vaut true, l'instance accepte de gérer l'enregistrement d'une audio avec le microphone du navigateur. Si load vaut true, l'instance accepte de charger un fichier audio à partir du disque local en réponse à une interaction de l'utilisateur. Si draganddrop vaut true, l'instance accepte de charger un fichier audio que l'utilisateur a déposé sur l'interface. deleteURL est une chaîne de caractères qui spécifie l'URL du POST envoyé au serveur pour détruire une audio. uploadURL est une chaîne de caractères qui spécifie l'URL du POST envoyé au serveur pour télécharger une audio. chunksize spécifie la taille des blocs de données envoyés au serveur, 100 ko par défaut.

Si le navigateur ne peut pas jouer une audio du type audio/ogg ou audio/mpeg, le constructeur provoque une erreur TypeError.

Si une des options recorder, load ou dragandrop ne vaut pas true, l'instance ne pourra jamais télécharger une audio et l'option uploadURL est mise à false.

Si chunksize est défini et n'est pas un entier, le constructeur provoque une erreur TypeError. Si chunksize est un nombre inférieur à 10000, le constructeur provoque une erreur RangeError.

Si recorder vaut true, le constructeur vérifie si le navigateur peut enregistrer une audio au format audio/ogg, audio/mpeg ou audio/webm et le mémorise. Sinon, l'option recorder est mise à false. NOTE : L'enregistreur est créé en réponse à la première demande d'enregistrement.

  1.     this._uploadable = false;
  2.  
  3.     this._autoplay = false;
  4.  
  5.     this._error = null;

duration est un accesseur qui retourne la durée en millisecondes de l'audio de this, 0 par défaut.

  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 est un accesseur qui retourne ou change la position en millisecondes de l'audio de this. Si this est en train d'enregistrer ou de télécharger l'audio, changer la position de l'audio est ignoré.

  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 est un accesseur qui retourne ou change l'URL de l'audio de this. Si this est en train d'enregistrer ou de télécharger l'audio, changer l'URL de l'audio est ignoré.

  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);
Serveur

L'audio est copiée dans l'espace disque du serveur en réponse à un POST par une fonction qui en extrait le type MIME et la taille du fichier, le bloc de données qui est transféré, sa taille et sa position dans le fichier. Un bloc de données est encodé en BASE64.

Dans iZend, le fichier uploadaudio.php s'installe directement dans le dossier actions. Pour activer l'URL /uploadaudio dans un site web, une entrée est ajoutée dans le fichier aliases.inc à la liste des URLs communes à toutes les langues.

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

Adaptez l'installation de cette fonction à votre environnement de développement.

  1. define('TMP_DIR', ROOT_DIR . DIRECTORY_SEPARATOR . 'tmp');
  2.  
  3. define('AUDIO_MAX_SIZE', 10*1000000);

Définit TMP_DIR - le dossier dans lequel le fichier de l'audio est sauvegardé - au nom du sous-dossier tmp dans le dossier racine du site et AUDIO_MAX_SIZE - la taille maximum d'une audio - à 10 Mo.

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

Définit la fonction uploadaudio qui est appelée par l'URL /uploadaudio du site. Les arguments $lang et $arglist ne sont pas utilisés. Le contenu retourné par cette action est indépendant du langage de l'utilisateur. Aucun argument ou paramètre n'est attendu dans l'URL.

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

Initialise $maxfilesize à AUDIO_MAX_SIZE et $filetypes à la liste de types MIME supportés.

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

Initialise les variables des arguments du 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.     }

Extrait les arguments du POST. Le bloc de données encodé en BASE64 est décodé.

  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.     }

Vérifie les arguments du POST.

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

Vérifie la taille des données envoyées dans le POST.

  1.     goto trashfile;

Sur ce serveur, l'écriture du fichier est simulée. Pour vérifier que l'audio a bien été copiée en l'écoutant, commentez cette instruction et adaptez le reste du code pour enregistrer le fichier dans un dossier accessible en écriture par le serveur.

  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;

Définit le nom du fichier de l'audio. L'extension du nom du fichier dépend du type MIME de l'audio.

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

Ouvre le fichier de l'audio en mode binaire. Si le POST contient le premier bloc de données, le fichier est créé.

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

Déplace le pointeur du fichier à la position du bloc de données.

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

Écrit le bloc de données.

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

Retourne un simple entête 200 Ok si plus de blocs de données sont attendus.

  1.     return false;

Retourne un simple entête 200 Ok si tous les blocs de données ont été sauvegardés. NOTE : Le code laisse de la place pour effectuer un traitement sur le fichier comme une conversion en différents formats avec FFmpeg.

  1. trashfile:
  2.     return false;

Retourne un simple entête 200 Ok sans enregistrer les données.

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

Retourne un simple entête 400 Bad Request si un des éléments du POST est invalide.

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

Retourne un simple entête 500 Internal Error si l'audio n'a pas pu être correctement sauvegardée.

Pour demander au serveur de détruire une audio, le lecteur envoie un simple POST au serveur sans arguments.

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

Définit la fonction donothing qui est appelée par l'URL /deleteaudio du site. donothing ne fait rien et retourne toujours un simple entête 200 Ok, i. e. détruire un fichier audio est simulé.

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

Mettre $debug à true permet d'accéder dans la console du navigateur à la variable audioplayer, l'instance de AudioPlayer. Si $debug vaut false, tout le code en JavaScript est protégé par une fonction de fermeture.

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

Définit les URL de la sauvegarde et de la destruction d'une audio sur le serveur, la taille des blocs de données envoyés au serveur.

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

Définit l'identifiant de la <div> qui encadre le HTML du lecteur audio.

  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>

Définit un élément <audio> dont les paramètres <source> serviront à configurer le lecteur audio. Cet élément sera caché par le code en 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'); ?>

Inclut le code de toutes les classes nécessaires. RAPPEL : La fonction head de la librairie iZend ajoute une balise telle que <script src="/objectivejs/Objective.js"></script> à la section <head> du document HTML. Adaptez le code à votre environnement de développement.

  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. }

Définit la méthode createWidget d'une instance de AudioPlayer. NOTE : Cet exemple utilise les icônes de Font Awesome. Adaptez-le à votre environnement de développement.

Le HTML de l'interface peut aussi être créé directement dans le document et passé à setWidget. Voir AudioPlaylist.

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

Isole tout le code en JavaScript dans une fonction de fermeture si $debug vaut 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);

Configure et crée une instance de AudioPlayer. NOTE : Jouez avec les options pour voir comment l'interface est configurée. Mettez en commentaire des widgets dans createWidget pour changer l'apparence du lecteur.

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

Récupère la <div> qui encadre le HTML du programme.

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

Configure la source audio du lecteur avec l'élément <audio> d'origine.

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

Crée et affiche l'interface du lecteur audio.

  1.     audio.hidden = true;

Cache l'élément <audio> d'origine.

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

Ferme la fonction qui isole le code en JavaScript si $debug vaut false.

VOIR AUSSI

View, AudioPlaylist, Wall, Écrire des données sur un serveur

Commentaires

Votre commentaire :
[p] [b] [i] [u] [s] [quote] [pre] [br] [code] [url] [email] strip aide 2000

Entrez un maximum de 2000 caractères.
Améliorez la présentation de votre texte avec les balises de formatage suivantes :
[p]paragraphe[/p], [b]gras[/b], [i]italique[/i], [u]souligné[/u], [s]barré[/s], [quote]citation[/quote], [pre]tel quel[/pre], [br]à la ligne,
[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]commande[/code], [code=langage]code source en c, java, php, html, javascript, xml, css, sql, bash, dos, make, etc.[/code].