From 845df06a635f41e2ba8656967ea79b71ffcffe54 Mon Sep 17 00:00:00 2001 From: StefansArya Date: Wed, 1 Sep 2021 21:45:31 +0700 Subject: [PATCH] Update to v1.3.1 --- README.md | 12 +- dist/SFMediaStream.js | 2627 --------------------------------- dist/SFMediaStream.min.js | 9 - dist/SFMediaStream.min.js.map | 1 - package.json | 3 +- 5 files changed, 9 insertions(+), 2643 deletions(-) delete mode 100644 dist/SFMediaStream.js delete mode 100644 dist/SFMediaStream.min.js delete mode 100644 dist/SFMediaStream.min.js.map diff --git a/README.md b/README.md index be660e2..ffeae41 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,13 @@ The default configuration is intended for newer browser. If you want to build 2- ## Install with CDN link You can download minified js from this repository or use this CDN link -`` +`` -If you want to use the old release, please modify the `latest` into a specific version. +If you want to use the different version, please modify the `v1` into a specific version. And include it on your project ```js +// Prefixed with Scarlets when imported with CDN link var presenter = new ScarletsMediaPresenter(...); var streamer = new ScarletsAudioStreamer(...); ``` @@ -24,15 +25,16 @@ var streamer = new ScarletsAudioStreamer(...); ## Install with NPM `npm i sfmediastream` -And include it on your project +This is for web bundler like Webpack or Browserify, and can't be used as a library for Node.js. If you want to use this recorder/effect/plugin for Node.js, the I think it may be possible by using headless browser like Puppeteer. + ```js const {MediaPresenter, AudioStreamer, ...} = require('sfmediastream'); var presenter = new MediaPresenter(...); var streamer = new AudioStreamer(...); ``` -## Adding retro-compatibility -In case of the browser doesn't support some codec like audio/wav, audio/webm, or audio/ogg you can to add [opus-media-recorder](https://github.com/kbumsik/opus-media-recorder) before using the library. +## Adding retro-compatibility +In case of the browser doesn't support some codec like `audio/wav`, `audio/webm`, or `audio/ogg` you can to add [opus-media-recorder](https://github.com/kbumsik/opus-media-recorder) before using the library. Safari browser actually is partially supported by using this polyfill. It able to stream audio, but is not playable by the browser. diff --git a/dist/SFMediaStream.js b/dist/SFMediaStream.js deleted file mode 100644 index 836a6a1..0000000 --- a/dist/SFMediaStream.js +++ /dev/null @@ -1,2627 +0,0 @@ -/* - ScarletsFiction MediaStream Library - - HTML5 media streamer library for playing music, video, playlist, - or even live streaming microphone & camera with node server - https://github.com/ScarletsFiction/SFMediaStream -*/ -(function(global, factory){ - if(typeof exports === 'object' && typeof module !== 'undefined'){ - module.exports = {}; - factory(module.exports, window, true); - } - else factory(global, window); -}(this || window, (function(global, window, moduleMode){'use strict'; -// ===== Module Init ===== - -// Initialize global data -var ScarletsMedia = { - audioContext: false, // Created after user gesture - - // Get Audio Node from HTML5's audio tag - getElementAudioNode:function(elem){ - elem.crossOrigin = 'anonymous'; - return this.audioContext.createMediaElementSource(elem); - }, - - // videoContext: window.VideoContext ? new VideoContext() : false, - - // Still underdevelopment: https://github.com/bbc/VideoContext - getElementVideoNode:function(elem){ - elem.crossOrigin = 'anonymous'; - return null; - } -}; - -var ScarletsMediaEffect = {}; -var audioCodecs = { - webm:['opus', 'vorbis'], - mp4:['mp4a.67', 'mp4a.40.29', 'mp4a.40.5', 'mp4a.40.2', 'mp3'], - ogg:['opus', 'vorbis'], // This may not work on mobile -}; -var videoCodecs = { - webm:['vp8,opus', 'vp8,vorbis'], - mp4:['mp4v.20.8,mp4a.40.2', 'mp4v.20.240,mp4a.40.2', 'avc1.42E01E,mp4a.40.2', 'avc1.58A01E,mp4a.40.2', 'avc1.64001E,mp4a.40.2'], - '3gpp':['mp4v.20.8,samr'], - ogg:['dirac,vorbis', 'theora,vorbis'], // This may not work on mobile -}; - -var waitingUnlock = []; -var userInteracted = false; - -// Unlock mobile media security -(function(){ - const AudioContext = window.AudioContext || window.webkitAudioContext; - if(!AudioContext) return console.error("`AudioContext` was not available"); - ScarletsMedia.audioContext = new AudioContext(); - - var mobileMediaUnlock = function(e){ - var emptyBuffer = ScarletsMedia.audioContext.createBuffer(1, 1, 22050); - var source = ScarletsMedia.audioContext.createBufferSource(); - source.buffer = emptyBuffer; - source.connect(ScarletsMedia.audioContext.destination); - - source.onended = function(){ - source.disconnect(0); - source = emptyBuffer = null; - - removeListener(); - } - - // Play the empty buffer. - if(!source.start) source.noteOn(0); - else source.start(0); - ScarletsMedia.audioContext.resume(); - } - - function removeListener(){ - document.removeEventListener('touchstart', mobileMediaUnlock, true); - document.removeEventListener('touchend', mobileMediaUnlock, true); - document.removeEventListener('click', mobileMediaUnlock, true); - - for (var i = 0; i < waitingUnlock.length; i++) { - waitingUnlock[i](); - } - - waitingUnlock.length = 0; - } - - document.addEventListener('touchstart', mobileMediaUnlock, true); - document.addEventListener('touchend', mobileMediaUnlock, true); - document.addEventListener('click', mobileMediaUnlock, true); -})(); -// Minimum 3 bufferElement -var ScarletsAudioStreamer = function(chunksDuration){ - if(!chunksDuration) chunksDuration = 1000; - var chunksSeconds = chunksDuration/1000; - - var scope = this; - - scope.debug = false; - scope.playing = false; - scope.latency = 0; - scope.mimeType = null; - scope.bufferElement = []; - - scope.onStop = null; - - scope.audioContext = ScarletsMedia.audioContext; - scope.outputNode = false; // Set this to a connectable Audio Node - - // If the outputNode is not set, then the audio will be outputted directly - var directAudioOutput = true; - - var bufferHeader = false; - var mediaBuffer = false; - - var audioElement = scope.element = new Audio(); - var audioNode = scope.audioContext.createMediaElementSource(audioElement); - - // ToDo: we may need to try to recreate the element if error happen - // Or reducing the extra latency - audioElement.addEventListener('error', function(e){ - console.error(e.target.error); - }); - - scope.connect = function(node){ - if(directAudioOutput === true){ - directAudioOutput = false; - audioNode.disconnect(); - } - - scope.outputNode = scope.audioContext.createGain(); - scope.outputNode.connect(node); - audioNode.connect(node); - } - - scope.disconnect = function(node){ - scope.outputNode.disconnect(node); - directAudioOutput = true; - - audioNode.disconnect(node); - audioNode.connect(scope.audioContext.destination); - } - - scope.stop = function(){ - mediaBuffer.stop(); - scope.playing = false; - scope.buffering = false; - if (scope.onStop) scope.onStop(); - } - - scope.setBufferHeader = function(packet){ - if(!packet.data){ - bufferHeader = false; - return; - } - - var arrayBuffer = packet.data; - scope.mimeType = packet.mimeType; - - if(mediaBuffer !== false) - mediaBuffer.stop(); - else audioNode.connect(scope.audioContext.destination); - - mediaBuffer = new MediaBuffer(scope.mimeType, chunksDuration, arrayBuffer); - bufferHeader = new Uint8Array(arrayBuffer); - - audioElement.src = scope.objectURL = mediaBuffer.objectURL; - - // Get buffer noise length - scope.audioContext.decodeAudioData(arrayBuffer.slice(0), function(audioBuffer){ - // headerDuration = audioBuffer.duration; - noiseLength = audioBuffer.getChannelData(0).length; - }); - } - - // ===== For handling WebAudio ===== - function createBufferSource(){ - var temp = scope.audioContext.createBufferSource(); - temp.onended = function(){ - this.stop(); - this.disconnect(); - } - return temp; - } - - var addBufferHeader = function(arrayBuffer){ - var finalBuffer = new Uint8Array(bufferHeader.byteLength + arrayBuffer.byteLength); - finalBuffer.set(bufferHeader, 0); - finalBuffer.set(new Uint8Array(arrayBuffer), bufferHeader.byteLength); - return finalBuffer.buffer; - } - - var noiseLength = 0; - function cleanNoise(buffer){ - var frameCount = buffer.getChannelData(0).length - noiseLength; - if(frameCount === 0) return false; - - var channelLength = buffer.numberOfChannels; - var newBuffer = scope.audioContext.createBuffer(channelLength, frameCount, buffer.sampleRate); - - for (var i = 0; i < channelLength; i++) { - newBuffer.getChannelData(i).set(buffer.getChannelData(i).subarray(noiseLength)); - } - - return newBuffer; - } - - function webAudioBufferInsert(index, buffer){ - scope.bufferElement[index] = createBufferSource(); - buffer = cleanNoise(buffer); - - if(buffer === false) return false; - scope.bufferElement[index].buffer = buffer; - - if(scope.outputNode && scope.outputNode.context && directAudioOutput === false) - scope.bufferElement[index].connect(scope.outputNode); - - else // Direct output to destination - scope.bufferElement[index].connect(scope.audioContext.destination); - return true; - } - - // ===== Realtime Playing ===== - // Play audio immediately after received - - scope.playStream = function(){ - scope.playing = true; - } - - var bufferElementIndex = 0; - scope.realtimeBufferPlay = function(packet){ - if(scope.playing === false) return; - - var arrayBuffer = packet[0]; - var streamingTime = packet[1]; - - if(scope.debug) console.log("Receiving data", arrayBuffer.byteLength); - if(arrayBuffer.byteLength === 0) return; - - scope.latency = (Number(String(Date.now()).slice(-5, -3)) - streamingTime) + chunksSeconds + scope.audioContext.baseLatency; - - var index = bufferElementIndex; - bufferElementIndex++; - if(bufferElementIndex > 2) - bufferElementIndex = 0; - - scope.audioContext.decodeAudioData(addBufferHeader(arrayBuffer), function(buffer){ - if(webAudioBufferInsert(index, buffer) === false) - return; - - scope.bufferElement[index].start(0); - }); - } - - // ====== Synchronous Playing ====== - // Play next audio when last audio was finished - - scope.receiveBuffer = function(packet){ - if(scope.playing === false || !mediaBuffer.append) return; - - var arrayBuffer = packet[0]; - var streamingTime = packet[1]; - - mediaBuffer.append(arrayBuffer); - - if(audioElement.paused) - audioElement.play(); - - scope.latency = (Number(String(Date.now()).slice(-5, -3)) - streamingTime) + scope.audioContext.baseLatency + chunksSeconds; - if(scope.debug) console.log("Total latency: "+scope.latency); - } -} - -var BufferHeader = { - "audio/webm;codecs=opus": "GkXfo59ChoEBQveBAULygQRC84EIQoKEd2VibUKHgQRChYECGFOAZwH/////////FUmpZpkq17GDD0JATYCGQ2hyb21lV0GGQ2hyb21lFlSua7+uvdeBAXPFh7o5nyc1kHqDgQKGhkFfT1BVU2Oik09wdXNIZWFkAQIAAIC7AAAAAADhjbWERzuAAJ+BAmJkgSAfQ7Z1Af/////////ngQCjjIEAAID/A//+//7//qM=" -}; - -function getBufferHeader(type) { - if (!window.chrome && type === "audio/webm;codecs=opus" ) { - // this header is only for chrome based brosers - return false; - } - - var buff = BufferHeader[type]; - if(buff === void 0) return false; - - if(buff.constructor === Blob) - return buff; - - buff = atob(buff); - - var UInt = new Uint8Array(buff.length); - for (var i = 0; i < buff.length; i++) - UInt[i] = buff.charCodeAt(i); - - return BufferHeader[type] = new Blob([UInt]); -} -ScarletsMedia.convert = { - // Converts a MIDI pitch number to frequency. - // midi = 0 ~ 127 - midiToFreq:function (midi) { - if(midi <= -1500) return 0; - else if(midi > 1499) return 3.282417553401589e+38; - else return 440.0 * Math.pow(2, (Math.floor(midi) - 69) / 12.0); - }, - - // Converts frequency to MIDI pitch. - freqToMidi:function(freq){ - if(freq > 0) - return Math.floor(Math.log(freq/440.0) / Math.LN2 * 12 + 69); - else return -1500; - }, - - // Converts power to decibel. Note that it is off by 100dB to make it - powerToDb:function(power){ - if (power <= 0) - return 0; - else { - var db = 100 + 10.0 / Math.LN10 * Math.log(power); - if(db < 0) return 0; - return db; - } - }, - - // Converts decibel to power - dbToPower:function(db){ - if (db <= 0) return 0; - else { - if (db > 870) db = 870; - return Math.exp(Math.LN10 * 0.1 * (db - 100.0)); - } - }, - - // Converts amplitude to decibel. - ampToDb:function(lin){ - return 20.0 * (lin > 0.00001 ? (Math.log(lin) / Math.LN10) : -5.0); - }, - - // Converts decibel to amplitude - dbToAmp:function(db) { - return Math.pow(10.0, db / 20.0); - }, - - // Converts MIDI velocity to amplitude - velToAmp:function (velocity) { - return velocity / 127; - }, -} -var MediaBuffer = function(mimeType, chunksDuration, bufferHeader){ - var scope = this; - scope.source = new MediaSource(); - scope.objectURL = URL.createObjectURL(scope.source); - - var removing = false; - var totalTime = 0; // miliseconds - var sourceBuffer = null; - var buffers = []; - - scope.source.onsourceopen = function(){ - sourceBuffer = scope.source.addSourceBuffer(mimeType); - sourceBuffer.mode = 'sequence'; - sourceBuffer.appendBuffer(bufferHeader); - - sourceBuffer.onerror = function(e){ - console.error("SourceBuffer error:", e); - } - - sourceBuffer.onupdateend = function(){ - if(removing){ - removing = false; - totalTime = 10000; - - // 0 ~ 10 seconds - sourceBuffer.remove(0, 10); - return; - } - - if(!sourceBuffer.updating && buffers.length !== 0) - startAppending(buffers.shift()); - }; - }; - - function startAppending(buffer){ - sourceBuffer.appendBuffer(buffer); - totalTime += chunksDuration; - // console.log(totalTime, buffer); - } - - scope.source.onerror = function(e){ - console.error("MediaSource error:", e); - } - - scope.append = function(arrayBuffer){ - if(sourceBuffer === null) - return false; - - if (!sourceBuffer.updating && sourceBuffer.buffered.length === 2) - // The problem of accessing to 'sourceBuffer.buffered' is that after you append data, the SourceBuffer instance becomes temporarily unusable while it's working. - // During this time, the SourceBuffer's updating property will be set to true, so it's easy to check for. - console.log('something wrong'); - - if(totalTime >= 20000) - removing = true; - - if(!sourceBuffer.updating) - startAppending(arrayBuffer); - else - buffers.push(arrayBuffer); - - return totalTime/1000; - } - - scope.stop = function(){ - if(sourceBuffer.updating) - sourceBuffer.abort(); - - if(scope.source.readyState === "open") - scope.source.endOfStream(); - } -} -// https://www.w3schools.com/tags/ref_av_dom.asp -// https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement -var ScarletsMediaPlayer = function(element){ - // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Media_events - var self = this; - - if(element === void 0) - element = 'audio'; - - if(element.constructor === String){ - if(element !== 'audio' && element !== 'video') - return console.error('Supported player is "audio" or "video"'); - - element = document.createElement(element); - document.body.appendChild(element); - } - - var propertyLinker = ['autoplay', 'loop', 'buffered', 'buffered', 'controller', 'currentTime', 'currentSrc', 'duration', 'ended', 'error', 'readyState', 'networkState', 'paused', 'played', 'seekable', 'seeking']; - - // Get element audio for output node - var audioOutputNode = false; - Object.defineProperty(self, 'audioOutput', { - get: function(){ - if(!audioOutputNode) - audioOutputNode = ScarletsMedia.getElementAudioNode(element); - - return audioOutputNode; - }, - enumerable: true - }); - - if(element.tagName.toLowerCase() === 'video'){ - propertyLinker = propertyLinker.concat(['poster', 'height', 'width']); - - // Get element video for output node - var videoOutputNode = false; - Object.defineProperty(self, 'videoOutput', { - get: function(){ - if(!videoOutputNode) - videoOutputNode = ScarletsMedia.getElementVideoNode(element); - - return videoOutputNode; - }, - enumerable: true - }); - } - - // Reference element function - self.load = function(){ - element.load(); - } - - self.canPlayType = function(){ - element.canPlayType(); - } - - // Reference element property - for (var i = 0; i < propertyLinker.length; i++) { - ScarletsMedia.extra.objectPropertyLinker(self, element, propertyLinker[i]); - } - - self.preload = true; - element.preload = 'metadata'; - element.crossorigin = 'anonymous'; - self.audioFadeEffect = true; - - self.speed = function(set){ - if(set === undefined) return element.defaultPlaybackRate; - element.defaultPlaybackRate = element.playbackRate = set; - } - - self.mute = function(set){ - if(set === undefined) return element.muted; - element.defaultMuted = element.muted = set; - } - - self.stop = function(){ - self.pause(); - self.currentTime = 0; - } - - var volume = 1; - self.volume = function(set){ - if(set === undefined) return volume; - element.volume = volume = set; - } - - var stillWaiting = false; - function play(successCallback, errorCallback){ - element.play().then(function(){ - stillWaiting = false; - if(successCallback) successCallback(); - }).catch(function(e){ - if(errorCallback) errorCallback(e); - else{ - // If user haven't interacted with the page - // and media play was requested, let's pending it - if(userInteracted === false){ - if(stillWaiting === false){ - waitingUnlock.push(function(){ - play(successCallback, errorCallback); - }); - } - return; - } - - console.error(e); - } - }); - } - - self.play = function(successCallback, errorCallback){ - if(!element.paused){ - if(successCallback) successCallback(); - return; - } - if(self.audioFadeEffect){ - element.volume = 0; - play(successCallback, errorCallback); - ScarletsMedia.extra.fadeNumber(0, volume, 0.02, 400, function(num){ - element.volume = num; - }, successCallback); - return; - } - - play(successCallback, errorCallback); - } - - self.pause = function(callback){ - if(element.paused){ - if(callback) callback(); - return; - } - if(self.audioFadeEffect){ - ScarletsMedia.extra.fadeNumber(volume, 0, -0.02, 400, function(num){ - element.volume = num; - }, function(){ - element.pause(); - if(callback) callback(); - }); - return; - } - element.pause(); - if(callback) callback(); - } - - self.prepare = function(links, callback, force){ - // Stop playing media - if(!force && !element.paused) - return self.pause(function(){ - self.prepare(links, callback, true); - }); - - var temp = element.querySelectorAll('source'); - for (var i = temp.length - 1; i >= 0; i--) { - temp[i].remove(); - } - - if(self.preload && callback){ - self.once('canplay', callback); - self.once('error', function(){ - self.off('canplay', callback); - }); - } - - if(typeof links === 'string') - element.insertAdjacentHTML('beforeend', ``); - else{ - temp = ''; - for (var i = 0; i < links.length; i++) { - temp += ``; - } - element.insertAdjacentHTML('beforeend', temp); - } - - // Preload data - if(self.preload) - element.load(); - - else if(callback) - callback(); - } - - var eventRegistered = {}; - function eventTrigger(e){ - for (var i = 0; i < eventRegistered[e.type].length; i++) { - eventRegistered[e.type][i](e, self); - } - } - - // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Media_events - self.on = function(eventName, callback){ - var name = eventName.toLowerCase(); - if(eventRegistered[name] === undefined){ - element.addEventListener(eventName, eventTrigger, true); - eventRegistered[name] = []; - } - eventRegistered[name].push(callback); - return self; - } - - self.off = function(eventName, callback){ - var name = eventName.toLowerCase(); - if(eventRegistered[name] === undefined){ - element.removeEventListener(eventName, callback, true); - return; - } - - if(!callback) - eventRegistered[name].splice(0); - else - eventRegistered[name].splice(eventRegistered[name].indexOf(callback), 1); - - if(eventRegistered[name].length === 0){ - eventRegistered[name] = undefined; - element.removeEventListener(eventName, eventTrigger, true); - } - return self; - } - - self.once = function(eventName, callback){ - element.addEventListener(eventName, callback, {once:true}); - return self; - } - - self.destroy = function(){ - for(var key in eventRegistered){ - self.off(key); - } - self.playlist.list.splice(0); - self.playlist.original.splice(0); - for(var key in self){ - delete self[key]; - } - self = null; - - element.pause(); - element.innerHTML = ''; - } - - var playlistInitialized = false; - function internalPlaylistEvent(){ - if(playlistInitialized) return; - playlistInitialized = true; - - self.on('ended', function(){ - if(self.playlist.currentIndex < self.playlist.list.length - 1) - self.playlist.next(true); - else if(self.playlist.loop) - self.playlist.play(0); - }); - } - - function playlistTriggerEvent(name){ - if(!eventRegistered[name]) return; - for (var i = 0; i < eventRegistered[name].length; i++) { - eventRegistered[name][i](self, self.playlist, self.playlist.currentIndex); - } - } - - self.playlist = { - currentIndex:0, - list:[], - original:[], - loop:false, - shuffled:false, - - // lists = [{yourProperty:'', stream:['main.mp3', 'fallback.ogg', ..]}, ...] - reload(lists){ - this.original = lists; - this.shuffle(this.shuffled); - internalPlaylistEvent(); - }, - - // obj = {yourProperty:'', stream:['main.mp3', 'fallback.ogg']} - add(obj){ - this.original.push(obj); - this.shuffle(this.shuffled); - internalPlaylistEvent(); - }, - - // index from 'original' property - remove(index){ - this.original.splice(index, 1); - this.shuffle(this.shuffled); - }, - - next(autoplay){ - this.currentIndex++; - if(this.currentIndex >= this.list.length){ - if(this.loop) - this.currentIndex = 0; - else{ - this.currentIndex--; - return; - } - } - - if(autoplay) - this.play(this.currentIndex); - else playlistTriggerEvent('playlistchange'); - }, - - previous(autoplay){ - this.currentIndex--; - if(this.currentIndex < 0){ - if(this.loop) - this.currentIndex = this.list.length - 1; - else{ - this.currentIndex++; - return; - } - } - - if(autoplay) - this.play(this.currentIndex); - else playlistTriggerEvent('playlistchange'); - }, - - play(index){ - this.currentIndex = index; - playlistTriggerEvent('playlistchange'); - - var src = this.list[index].stream; - if(self.currentSrc === src) - self.play(); - else self.prepare(this.list[index].stream, function(){ - self.play(); - }); - }, - - shuffle(set){ - if(set === true){ - var j, x, i; - for (i = this.list.length - 1; i > 0; i--) { - j = Math.floor(Math.random() * (i + 1)); - x = this.list[i]; - this.list[i] = this.list[j]; - this.list[j] = x; - } - } - else this.list = this.original.slice(0); - - this.shuffled = set; - } - }; -} -// options = mediaDevices.getUserMedia({thisData}) -// latency = 0ms is not possible (minimum is 70ms, or depend on computer performance) -var ScarletsMediaPresenter = function(options, latency){ - var scope = this; - if(!latency) latency = 1000; - - // The options are optional - //var options = { - // mediaStream: new MediaStream(), // For custom media stream - // element: document.querySelector(...), // Record