diff --git a/README.md b/README.md index 3ecc3e0d..60d059b6 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ flv.js is written in [ECMAScript 6][], transpiled into ECMAScript 5 by [Babel Co [Browserify]: http://browserify.org/ ## Features -- FLV container with H.264 + AAC codec playback +- FLV container with H.264 + AAC / MP3 codec playback - Multipart segmented video playback - HTTP FLV low latency live stream playback - FLV over WebSocket live stream playback @@ -61,6 +61,10 @@ See [cors.md](docs/cors.md) for more details. ``` +## Limitations +- MP3 audio codec is currently not working on IE11 / Edge +- HTTP FLV live stream is not currently working on all browsers, see [livestream.md](docs/livestream.md) + ## Multipart playback You only have to provide a playlist for `MediaDataSource`. See [multipart.md](docs/multipart.md) diff --git a/src/core/mse-controller.js b/src/core/mse-controller.js index 774842ea..c2aa88dd 100644 --- a/src/core/mse-controller.js +++ b/src/core/mse-controller.js @@ -46,6 +46,9 @@ class MSEController { this._isBufferFull = false; this._hasPendingEos = false; + this._requireSetMediaDuration = false; + this._pendingMediaDuration = 0; + this._pendingSourceBufferInit = []; this._mimeTypes = { video: null, @@ -162,7 +165,11 @@ class MSEController { } let is = initSegment; - let mimeType = `${is.container};codecs=${is.codec}`; + let mimeType = `${is.container}`; + if (is.codec && is.codec.length > 0) { + mimeType += `;codecs=${is.codec}`; + } + let firstInitSegment = false; Log.v(this.TAG, 'Received Initialization Segment, mimeType: ' + mimeType); @@ -195,6 +202,13 @@ class MSEController { this._doAppendSegments(); } } + if (Browser.safari && is.container === 'audio/mpeg' && is.mediaDuration > 0) { + // 'audio/mpeg' track under Safari may cause MediaElement's duration to be NaN + // Manually correct MediaSource.duration to make progress bar seekable, and report right duration + this._requireSetMediaDuration = true; + this._pendingMediaDuration = is.mediaDuration / 1000; // in seconds + this._updateMediaSourceDuration(); + } } appendMediaSegment(mediaSegment) { @@ -293,6 +307,27 @@ class MSEController { return this._idrList.getLastSyncPointBeforeDts(dts); } + _updateMediaSourceDuration() { + let sb = this._sourceBuffers; + if (this._mediaElement.readyState === 0 || this._mediaSource.readyState !== 'open') { + return; + } + if ((sb.video && sb.video.updating) || (sb.audio && sb.audio.updating)) { + return; + } + + let current = this._mediaSource.duration; + let target = this._pendingMediaDuration; + + if (target > 0 && (isNaN(current) || target > current)) { + Log.v(this.TAG, `Update MediaSource duration from ${current} to ${target}`); + this._mediaSource.duration = target; + } + + this._requireSetMediaDuration = false; + this._pendingMediaDuration = 0; + } + _doRemoveRanges() { for (let type in this._pendingRemoveRanges) { if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) { @@ -314,8 +349,29 @@ class MSEController { if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) { continue; } + if (pendingSegments[type].length > 0) { let segment = pendingSegments[type].shift(); + + if (segment.timestampOffset) { + // For MPEG audio stream in MSE, if unbuffered-seeking occurred + // We need explicitly set timestampOffset to the desired point in timeline for mpeg SourceBuffer. + let currentOffset = this._sourceBuffers[type].timestampOffset; + let targetOffset = segment.timestampOffset / 1000; // in seconds + + let delta = Math.abs(currentOffset - targetOffset); + if (delta > 0.1) { // If time delta > 100ms + Log.v(this.TAG, `Update MPEG audio timestampOffset from ${currentOffset} to ${targetOffset}`); + this._sourceBuffers[type].timestampOffset = targetOffset; + } + delete segment.timestampOffset; + } + + if (!segment.data || segment.data.byteLength === 0) { + // Ignore empty buffer + continue; + } + try { this._sourceBuffers[type].appendBuffer(segment.data); this._isBufferFull = false; @@ -392,7 +448,9 @@ class MSEController { } _onSourceBufferUpdateEnd() { - if (this._hasPendingRemoveRanges()) { + if (this._requireSetMediaDuration) { + this._updateMediaSourceDuration(); + } else if (this._hasPendingRemoveRanges()) { this._doRemoveRanges(); } else if (this._hasPendingSegments()) { this._doAppendSegments(); diff --git a/src/demux/flv-demuxer.js b/src/demux/flv-demuxer.js index 6408b3cd..17b05515 100644 --- a/src/demux/flv-demuxer.js +++ b/src/demux/flv-demuxer.js @@ -84,6 +84,21 @@ class FLVDemuxer { fps_den: 1000 }; + this._flvSoundRateTable = [5500, 11025, 22050, 44100, 48000]; + + this._mpegSamplingRates = [ + 96000, 88200, 64000, 48000, 44100, 32000, + 24000, 22050, 16000, 12000, 11025, 8000, 7350 + ]; + + this._mpegAudioV10SampleRateTable = [44100, 48000, 32000, 0]; + this._mpegAudioV20SampleRateTable = [22050, 24000, 16000, 0]; + this._mpegAudioV25SampleRateTable = [11025, 12000, 8000, 0]; + + this._mpegAudioL1BitRateTable = [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, -1]; + this._mpegAudioL2BitRateTable = [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, -1]; + this._mpegAudioL3BitRateTable = [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, -1]; + this._videoTrack = {type: 'video', id: 1, sequenceNumber: 0, samples: [], length: 0}; this._audioTrack = {type: 'audio', id: 2, sequenceNumber: 0, samples: [], length: 0}; @@ -404,10 +419,34 @@ class FLVDemuxer { return; } + let le = this._littleEndian; + let v = new DataView(arrayBuffer, dataOffset, dataSize); + + let soundSpec = v.getUint8(0); + + let soundFormat = soundSpec >>> 4; + if (soundFormat !== 2 && soundFormat !== 10) { // MP3 or AAC + this._onError(DemuxErrors.CODEC_UNSUPPORTED, 'Flv: Unsupported audio codec idx: ' + soundFormat); + return; + } + + let soundRate = 0; + let soundRateIndex = (soundSpec & 12) >>> 2; + if (soundRateIndex >= 0 && soundRateIndex <= 4) { + soundRate = this._flvSoundRateTable[soundRateIndex]; + } else { + this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid audio sample rate idx: ' + soundRateIndex); + return; + } + + let soundSize = (soundSpec & 2) >>> 1; // unused + let soundType = (soundSpec & 1); + + let meta = this._audioMetadata; let track = this._audioTrack; - if (!meta || !meta.codec) { + if (!meta) { if (this._hasAudio === false) { this._hasAudio = true; this._mediaInfo.hasAudio = true; @@ -419,92 +458,106 @@ class FLVDemuxer { meta.id = track.id; meta.timescale = this._timescale; meta.duration = this._duration; - - let le = this._littleEndian; - let v = new DataView(arrayBuffer, dataOffset, dataSize); - - let soundSpec = v.getUint8(0); - - let soundFormat = soundSpec >>> 4; - if (soundFormat !== 10) { // AAC - // TODO: support MP3 audio codec - this._onError(DemuxErrors.CODEC_UNSUPPORTED, 'Flv: Unsupported audio codec idx: ' + soundFormat); - return; - } - - let soundRate = 0; - let soundRateIndex = (soundSpec & 12) >>> 2; - - let soundRateTable = [5500, 11025, 22050, 44100, 48000]; - - if (soundRateIndex < soundRateTable.length) { - soundRate = soundRateTable[soundRateIndex]; - } else { - this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: Invalid audio sample rate idx: ' + soundRateIndex); - return; - } - - let soundSize = (soundSpec & 2) >>> 1; // unused - let soundType = (soundSpec & 1); - meta.audioSampleRate = soundRate; meta.channelCount = (soundType === 0 ? 1 : 2); - meta.refSampleDuration = Math.floor(1024 / meta.audioSampleRate * meta.timescale); - meta.codec = 'mp4a.40.5'; } - let aacData = this._parseAACAudioData(arrayBuffer, dataOffset + 1, dataSize - 1); - if (aacData == undefined) { - return; - } - - if (aacData.packetType === 0) { // AAC sequence header (AudioSpecificConfig) - if (meta.config) { - Log.w(this.TAG, 'Found another AudioSpecificConfig!'); + if (soundFormat === 10) { // AAC + let aacData = this._parseAACAudioData(arrayBuffer, dataOffset + 1, dataSize - 1); + if (aacData == undefined) { + return; } - let misc = aacData.data; - meta.audioSampleRate = misc.samplingRate; - meta.channelCount = misc.channelCount; - meta.codec = misc.codec; - meta.config = misc.config; - // The decode result of an aac sample is 1024 PCM samples - meta.refSampleDuration = Math.floor(1024 / meta.audioSampleRate * meta.timescale); - Log.v(this.TAG, 'Parsed AudioSpecificConfig'); - if (this._isInitialMetadataDispatched()) { - // Non-initial metadata, force dispatch (or flush) parsed frames to remuxer - if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) { - this._onDataAvailable(this._audioTrack, this._videoTrack); + if (aacData.packetType === 0) { // AAC sequence header (AudioSpecificConfig) + if (meta.config) { + Log.w(this.TAG, 'Found another AudioSpecificConfig!'); + } + let misc = aacData.data; + meta.audioSampleRate = misc.samplingRate; + meta.channelCount = misc.channelCount; + meta.codec = misc.codec; + meta.config = misc.config; + // The decode result of an aac sample is 1024 PCM samples + meta.refSampleDuration = Math.floor(1024 / meta.audioSampleRate * meta.timescale); + Log.v(this.TAG, 'Parsed AudioSpecificConfig'); + + if (this._isInitialMetadataDispatched()) { + // Non-initial metadata, force dispatch (or flush) parsed frames to remuxer + if (this._dispatch && (this._audioTrack.length || this._videoTrack.length)) { + this._onDataAvailable(this._audioTrack, this._videoTrack); + } + } else { + this._audioInitialMetadataDispatched = true; + } + // then notify new metadata + this._dispatch = false; + this._onTrackMetadata('audio', meta); + + let mi = this._mediaInfo; + mi.audioCodec = 'mp4a.40.' + misc.originalAudioObjectType; + mi.audioSampleRate = meta.audioSampleRate; + mi.audioChannelCount = meta.channelCount; + if (mi.hasVideo) { + if (mi.videoCodec != null) { + mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + ',' + mi.audioCodec + '"'; + } + } else { + mi.mimeType = 'video/x-flv; codecs="' + mi.audioCodec + '"'; } + if (mi.isComplete()) { + this._onMediaInfo(mi); + } + } else if (aacData.packetType === 1) { // AAC raw frame data + let dts = this._timestampBase + tagTimestamp; + let aacSample = {unit: aacData.data, dts: dts, pts: dts}; + track.samples.push(aacSample); + track.length += aacData.data.length; } else { - this._audioInitialMetadataDispatched = true; + Log.e(this.TAG, `Flv: Unsupported AAC data type ${aacData.packetType}`); } - // then notify new metadata - this._dispatch = false; - this._onTrackMetadata('audio', meta); + } else if (soundFormat === 2) { // MP3 + if (!meta.codec) { + // We need metadata for mp3 audio track, extract info from frame header + let misc = this._parseMP3AudioData(arrayBuffer, dataOffset + 1, dataSize - 1, true); + if (misc == undefined) { + return; + } + meta.audioSampleRate = misc.samplingRate; + meta.channelConfig = misc.channelCount; + meta.codec = misc.codec; + // The decode result of an mp3 sample is 1152 PCM samples + meta.refSampleDuration = Math.floor(1152 / meta.audioSampleRate * meta.timescale); + Log.v(this.TAG, 'Parsed MPEG Audio Frame Header'); - let mi = this._mediaInfo; - mi.audioCodec = 'mp4a.40.' + misc.originalAudioObjectType; - mi.audioSampleRate = meta.audioSampleRate; - mi.audioChannelCount = meta.channelCount; - if (mi.hasVideo) { - if (mi.videoCodec != null) { - mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + ',' + mi.audioCodec + '"'; + this._audioInitialMetadataDispatched = true; + this._onTrackMetadata('audio', meta); + + let mi = this._mediaInfo; + mi.audioCodec = meta.codec; + mi.audioSampleRate = meta.audioSampleRate; + mi.audioChannelCount = meta.channelCount; + mi.audioDataRate = misc.bitRate; + if (mi.hasVideo) { + if (mi.videoCodec != null) { + mi.mimeType = 'video/x-flv; codecs="' + mi.videoCodec + ',' + mi.audioCodec + '"'; + } + } else { + mi.mimeType = 'video/x-flv; codecs="' + mi.audioCodec + '"'; + } + if (mi.isComplete()) { + this._onMediaInfo(mi); } - } else { - mi.mimeType = 'video/x-flv; codecs="' + mi.audioCodec + '"'; } - if (mi.isComplete()) { - this._onMediaInfo(mi); + + // This packet is always a valid audio packet, extract it + let data = this._parseMP3AudioData(arrayBuffer, dataOffset + 1, dataSize - 1, false); + if (data == undefined) { + return; } - return; - } else if (aacData.packetType === 1) { // AAC raw frame data let dts = this._timestampBase + tagTimestamp; - let aacSample = {unit: aacData.data, dts: dts, pts: dts}; - track.samples.push(aacSample); - track.length += aacData.data.length; - } else { - Log.e(this.TAG, `Flv: Unsupported AAC data type ${aacData.packetType}`); + let mp3Sample = {unit: data, dts: dts, pts: dts}; + track.samples.push(mp3Sample); + track.length += data.length; } } @@ -532,11 +585,6 @@ class FLVDemuxer { let array = new Uint8Array(arrayBuffer, dataOffset, dataSize); let config = null; - let mpegSamplingRates = [ - 96000, 88200, 64000, 48000, 44100, 32000, - 24000, 22050, 16000, 12000, 11025, 8000, 7350 - ]; - /* Audio Object Type: 0: Null 1: AAC Main @@ -557,12 +605,12 @@ class FLVDemuxer { audioObjectType = originalAudioObjectType = array[0] >>> 3; // 4 bits samplingIndex = ((array[0] & 0x07) << 1) | (array[1] >>> 7); - if (samplingIndex < 0 || samplingIndex >= mpegSamplingRates.length) { + if (samplingIndex < 0 || samplingIndex >= this._mpegSamplingRates.length) { this._onError(DemuxErrors.FORMAT_ERROR, 'Flv: AAC invalid sampling frequency index!'); return; } - let samplingFrequence = mpegSamplingRates[samplingIndex]; + let samplingFrequence = this._mpegSamplingRates[samplingIndex]; // 4 bits let channelConfig = (array[1] & 0x78) >>> 3; @@ -634,6 +682,81 @@ class FLVDemuxer { }; } + _parseMP3AudioData(arrayBuffer, dataOffset, dataSize, requestHeader) { + if (dataSize < 4) { + Log.w(this.TAG, 'Flv: Invalid MP3 packet, header missing!'); + return; + } + + let le = this._littleEndian; + let array = new Uint8Array(arrayBuffer, dataOffset, dataSize); + let result = null; + + if (requestHeader) { + if (array[0] !== 0xFF) { + return; + } + let ver = (array[1] >>> 3) & 0x03; + let layer = (array[1] & 0x06) >> 1; + + let bitrate_index = (array[2] & 0xF0) >>> 4; + let sampling_freq_index = (array[2] & 0x0C) >>> 2; + + let channel_mode = (array[3] >>> 6) & 0x03; + let channel_count = channel_mode !== 3 ? 2 : 1; + + let sample_rate = 0; + let bit_rate = 0; + let object_type = 34; // Layer-3, listed in MPEG-4 Audio Object Types + + let codec = 'mp3'; + + switch (ver) { + case 0: // MPEG 2.5 + sample_rate = this._mpegAudioV25SampleRateTable[sampling_freq_index]; + break; + case 2: // MPEG 2 + sample_rate = this._mpegAudioV20SampleRateTable[sampling_freq_index]; + break; + case 3: // MPEG 1 + sample_rate = this._mpegAudioV10SampleRateTable[sampling_freq_index]; + break; + } + + switch (layer) { + case 1: // Layer 3 + object_type = 34; + if (bitrate_index < this._mpegAudioL3BitRateTable.length) { + bit_rate = this._mpegAudioL3BitRateTable[bitrate_index]; + } + break; + case 2: // Layer 2 + object_type = 33; + if (bitrate_index < this._mpegAudioL2BitRateTable.length) { + bit_rate = this._mpegAudioL2BitRateTable[bitrate_index]; + } + break; + case 3: // Layer 1 + object_type = 32; + if (bitrate_index < this._mpegAudioL1BitRateTable.length) { + bit_rate = this._mpegAudioL1BitRateTable[bitrate_index]; + } + break; + } + + result = { + bitRate: bit_rate, + samplingRate: sample_rate, + channelCount: channel_count, + codec: codec + }; + } else { + result = array; + } + + return result; + } + _parseVideoData(arrayBuffer, dataOffset, dataSize, tagTimestamp, tagPosition) { if (dataSize <= 1) { Log.w(this.TAG, 'Flv: Invalid video packet, missing VideoData payload!'); diff --git a/src/remux/mp4-generator.js b/src/remux/mp4-generator.js index 969eaac2..c014d13d 100644 --- a/src/remux/mp4-generator.js +++ b/src/remux/mp4-generator.js @@ -30,7 +30,7 @@ class MP4 { stco: [], stsc: [], stsd: [], stsz: [], stts: [], tfdt: [], tfhd: [], traf: [], trak: [], trun: [], trex: [], tkhd: [], - vmhd: [], smhd: [] + vmhd: [], smhd: [], '.mp3': [] }; for (let name in MP4.types) { @@ -318,12 +318,36 @@ class MP4 { // Sample description box static stsd(meta) { if (meta.type === 'audio') { + if (meta.codec === 'mp3') { + return MP4.box(MP4.types.stsd, MP4.constants.STSD_PREFIX, MP4.mp3(meta)); + } + // else: aac -> mp4a return MP4.box(MP4.types.stsd, MP4.constants.STSD_PREFIX, MP4.mp4a(meta)); } else { return MP4.box(MP4.types.stsd, MP4.constants.STSD_PREFIX, MP4.avc1(meta)); } } + static mp3(meta) { + let channelCount = meta.channelCount; + let sampleRate = meta.audioSampleRate; + + let data = new Uint8Array([ + 0x00, 0x00, 0x00, 0x00, // reserved(4) + 0x00, 0x00, 0x00, 0x01, // reserved(2) + data_reference_index(2) + 0x00, 0x00, 0x00, 0x00, // reserved: 2 * 4 bytes + 0x00, 0x00, 0x00, 0x00, + 0x00, channelCount, // channelCount(2) + 0x00, 0x10, // sampleSize(2) + 0x00, 0x00, 0x00, 0x00, // reserved(4) + (sampleRate >>> 8) & 0xFF, // Audio sample rate + (sampleRate) & 0xFF, + 0x00, 0x00 + ]); + + return MP4.box(MP4.types['.mp3'], data); + } + static mp4a(meta) { let channelCount = meta.channelCount; let sampleRate = meta.audioSampleRate; @@ -345,7 +369,7 @@ class MP4 { } static esds(meta) { - let config = meta.config; + let config = meta.config || []; let configSize = config.length; let data = new Uint8Array([ 0x00, 0x00, 0x00, 0x00, // version 0 + flags diff --git a/src/remux/mp4-remuxer.js b/src/remux/mp4-remuxer.js index 6c4866f4..81c994f7 100644 --- a/src/remux/mp4-remuxer.js +++ b/src/remux/mp4-remuxer.js @@ -58,6 +58,9 @@ class MP4Remuxer { // Workaround for IE11/Edge: Fill silent aac frame after keyframe-seeking // Make audio beginDts equals with video beginDts, in order to fix seek freeze this._fillSilentAfterSeek = (Browser.msedge || Browser.msie); + + // While only FireFox supports 'audio/mp4, codecs="mp3"', use 'audio/mpeg' for chrome, safari, ... + this._mp3UseMpegAudio = !Browser.firefox; } destroy() { @@ -134,9 +137,20 @@ class MP4Remuxer { _onTrackMetadataReceived(type, metadata) { let metabox = null; + let container = 'mp4'; + let codec = metadata.codec; + if (type === 'audio') { this._audioMeta = metadata; - metabox = MP4.generateInitSegment(metadata); + if (metadata.codec === 'mp3' && this._mp3UseMpegAudio) { + // 'audio/mpeg' for MP3 audio track + container = 'mpeg'; + codec = ''; + metabox = new Uint8Array(); + } else { + // 'audio/mp4, codecs="codec"' + metabox = MP4.generateInitSegment(metadata); + } } else if (type === 'video') { this._videoMeta = metadata; metabox = MP4.generateInitSegment(metadata); @@ -151,8 +165,9 @@ class MP4Remuxer { this._onInitSegment(type, { type: type, data: metabox.buffer, - codec: metadata.codec, - container: `${type}/mp4` + codec: codec, + container: `${type}/${container}`, + mediaDuration: metadata.duration // in timescale 1000 (milliseconds) }); } @@ -178,6 +193,9 @@ class MP4Remuxer { let dtsCorrection = undefined; let firstDts = -1, lastDts = -1, lastPts = -1; + let mpegRawTrack = this._audioMeta.codec === 'mp3' && this._mp3UseMpegAudio; + let firstSegmentAfterSeek = this._dtsBaseInited && this._audioNextDts === undefined; + let remuxSilentFrame = false; let silentFrameDuration = -1; @@ -185,16 +203,29 @@ class MP4Remuxer { return; } - let bytes = 8 + track.length; - let mdatbox = new Uint8Array(bytes); - mdatbox[0] = (bytes >>> 24) & 0xFF; - mdatbox[1] = (bytes >>> 16) & 0xFF; - mdatbox[2] = (bytes >>> 8) & 0xFF; - mdatbox[3] = (bytes) & 0xFF; + let bytes = 0; + let offset = 0; + let mdatbox = null; - mdatbox.set(MP4.types.mdat, 4); + if (mpegRawTrack) { + // allocate for raw mpeg buffer + bytes = track.length; + offset = 0; + mdatbox = new Uint8Array(bytes); + } else { + // allocate for fmp4 mdat box + bytes = 8 + track.length; + offset = 8; // size + type + mdatbox = new Uint8Array(bytes); + // size field + mdatbox[0] = (bytes >>> 24) & 0xFF; + mdatbox[1] = (bytes >>> 16) & 0xFF; + mdatbox[2] = (bytes >>> 8) & 0xFF; + mdatbox[3] = (bytes) & 0xFF; + // type field (fourCC) + mdatbox.set(MP4.types.mdat, 4); + } - let offset = 8; // size + type let mp4Samples = []; while (samples.length) { @@ -207,7 +238,9 @@ class MP4Remuxer { if (this._audioSegmentInfoList.isEmpty()) { dtsCorrection = 0; if (this._fillSilentAfterSeek && !this._videoSegmentInfoList.isEmpty()) { - remuxSilentFrame = true; + if (this._audioMeta.codec !== 'mp3') { + remuxSilentFrame = true; + } } } else { let lastSample = this._audioSegmentInfoList.getLastSampleBefore(originalDts); @@ -330,16 +363,33 @@ class MP4Remuxer { track.samples = mp4Samples; track.sequenceNumber++; - let moofbox = MP4.moof(track, firstDts); + let moofbox = null; + + if (mpegRawTrack) { + // Generate empty buffer, because useless for raw mpeg + moofbox = new Uint8Array(); + } else { + // Generate moof for fmp4 segment + moofbox = MP4.moof(track, firstDts); + } + track.samples = []; track.length = 0; - this._onMediaSegment('audio', { + let segment = { type: 'audio', data: this._mergeBoxes(moofbox, mdatbox).buffer, sampleCount: mp4Samples.length, info: info - }); + }; + + if (mpegRawTrack && firstSegmentAfterSeek) { + // For MPEG audio stream in MSE, if seeking occurred, before appending new buffer + // We need explicitly set timestampOffset to the desired point in timeline for mpeg SourceBuffer. + segment.timestampOffset = firstDts; + } + + this._onMediaSegment('audio', segment); } _generateSilentAudio(dts, frameDuration) { diff --git a/src/utils/browser.js b/src/utils/browser.js index 3a4e36ae..69f9b6c7 100644 --- a/src/utils/browser.js +++ b/src/utils/browser.js @@ -33,7 +33,7 @@ function detect() { /(opera)(?:.*version|)[ \/]([\w.]+)/.exec(ua) || /(msie) ([\w.]+)/.exec(ua) || ua.indexOf('trident') >= 0 && /(rv)(?::| )([\w.]+)/.exec(ua) || - ua.indexOf('compatible') < 0 && /(mozilla)(?:.*? rv:([\w.]+)|)/.exec(ua) || + ua.indexOf('compatible') < 0 && /(firefox)[ \/]([\w.]+)/.exec(ua) || []; let platform_match = /(ipad)/.exec(ua) ||