2015-12-16 00:30:14 -05:00
/ * *
* fMP4 remuxer
* /
import Event from '../events' ;
import { logger } from '../utils/logger' ;
import MP4 from '../remux/mp4-generator' ;
import { ErrorTypes , ErrorDetails } from '../errors' ;
class MP4Remuxer {
constructor ( observer ) {
this . observer = observer ;
this . ISGenerated = false ;
this . PES2MP4SCALEFACTOR = 4 ;
this . PES _TIMESCALE = 90000 ;
this . MP4 _TIMESCALE = this . PES _TIMESCALE / this . PES2MP4SCALEFACTOR ;
}
2016-02-24 22:15:07 -05:00
get passthrough ( ) {
return false ;
}
2015-12-16 00:30:14 -05:00
destroy ( ) {
}
insertDiscontinuity ( ) {
this . _initPTS = this . _initDTS = this . nextAacPts = this . nextAvcDts = undefined ;
}
switchLevel ( ) {
this . ISGenerated = false ;
}
2016-02-01 12:02:17 -05:00
remux ( audioTrack , videoTrack , id3Track , textTrack , timeOffset , contiguous ) {
2015-12-16 00:30:14 -05:00
// generate Init Segment if needed
if ( ! this . ISGenerated ) {
this . generateIS ( audioTrack , videoTrack , timeOffset ) ;
}
2016-03-27 19:22:53 -04:00
if ( this . ISGenerated ) {
//logger.log('nb AVC samples:' + videoTrack.samples.length);
if ( videoTrack . samples . length ) {
this . remuxVideo ( videoTrack , timeOffset , contiguous ) ;
}
//logger.log('nb AAC samples:' + audioTrack.samples.length);
if ( audioTrack . samples . length ) {
this . remuxAudio ( audioTrack , timeOffset , contiguous ) ;
}
2015-12-16 00:30:14 -05:00
}
//logger.log('nb ID3 samples:' + audioTrack.samples.length);
if ( id3Track . samples . length ) {
this . remuxID3 ( id3Track , timeOffset ) ;
}
2016-02-01 12:02:17 -05:00
//logger.log('nb ID3 samples:' + audioTrack.samples.length);
if ( textTrack . samples . length ) {
this . remuxText ( textTrack , timeOffset ) ;
}
2015-12-16 00:30:14 -05:00
//notify end of parsing
this . observer . trigger ( Event . FRAG _PARSED ) ;
}
generateIS ( audioTrack , videoTrack , timeOffset ) {
var observer = this . observer ,
audioSamples = audioTrack . samples ,
videoSamples = videoTrack . samples ,
2016-02-24 22:15:07 -05:00
pesTimeScale = this . PES _TIMESCALE ,
tracks = { } ,
data = { tracks : tracks , unique : false } ,
computePTSDTS = ( this . _initPTS === undefined ) ,
initPTS , initDTS ;
2015-12-16 00:30:14 -05:00
2016-02-24 22:15:07 -05:00
if ( computePTSDTS ) {
initPTS = initDTS = Infinity ;
}
if ( audioTrack . config && audioSamples . length ) {
2016-03-18 13:28:45 -04:00
audioTrack . timescale = audioTrack . audiosamplerate ;
// MP4 duration (track duration in seconds multiplied by timescale) is coded on 32 bits
// we know that each AAC sample contains 1024 frames....
// in order to avoid overflowing the 32 bit counter for large duration, we use smaller timescale (timescale/gcd)
// we just need to ensure that AAC sample duration will still be an integer (will be 1024/gcd)
if ( audioTrack . timescale * audioTrack . duration > Math . pow ( 2 , 32 ) ) {
let greatestCommonDivisor = function ( a , b ) {
if ( ! b ) {
return a ;
}
return greatestCommonDivisor ( b , a % b ) ;
} ;
audioTrack . timescale = audioTrack . audiosamplerate / greatestCommonDivisor ( audioTrack . audiosamplerate , 1024 ) ;
}
logger . log ( 'audio mp4 timescale :' + audioTrack . timescale ) ;
2016-02-24 22:15:07 -05:00
tracks . audio = {
container : 'audio/mp4' ,
codec : audioTrack . codec ,
initSegment : MP4 . initSegment ( [ audioTrack ] ) ,
metadata : {
channelCount : audioTrack . channelCount
}
} ;
if ( computePTSDTS ) {
// remember first PTS of this demuxing context. for audio, PTS + DTS ...
initPTS = initDTS = audioSamples [ 0 ] . pts - pesTimeScale * timeOffset ;
2015-12-16 00:30:14 -05:00
}
2016-02-24 22:15:07 -05:00
}
if ( videoTrack . sps && videoTrack . pps && videoSamples . length ) {
2016-03-18 13:28:45 -04:00
videoTrack . timescale = this . MP4 _TIMESCALE ;
2016-02-24 22:15:07 -05:00
tracks . video = {
container : 'video/mp4' ,
codec : videoTrack . codec ,
initSegment : MP4 . initSegment ( [ videoTrack ] ) ,
metadata : {
width : videoTrack . width ,
height : videoTrack . height
2015-12-16 00:30:14 -05:00
}
2016-02-24 22:15:07 -05:00
} ;
if ( computePTSDTS ) {
initPTS = Math . min ( initPTS , videoSamples [ 0 ] . pts - pesTimeScale * timeOffset ) ;
initDTS = Math . min ( initDTS , videoSamples [ 0 ] . dts - pesTimeScale * timeOffset ) ;
2015-12-16 00:30:14 -05:00
}
2016-02-24 22:15:07 -05:00
}
2016-03-27 19:22:53 -04:00
if ( Object . keys ( tracks ) . length ) {
2016-02-24 22:15:07 -05:00
observer . trigger ( Event . FRAG _PARSING _INIT _SEGMENT , data ) ;
this . ISGenerated = true ;
if ( computePTSDTS ) {
this . _initPTS = initPTS ;
this . _initDTS = initDTS ;
2015-12-16 00:30:14 -05:00
}
2016-03-27 19:22:53 -04:00
} else {
observer . trigger ( Event . ERROR , { type : ErrorTypes . MEDIA _ERROR , details : ErrorDetails . FRAG _PARSING _ERROR , fatal : false , reason : 'no audio/video samples found' } ) ;
2015-12-16 00:30:14 -05:00
}
}
remuxVideo ( track , timeOffset , contiguous ) {
2016-04-20 14:51:47 -04:00
var view ,
offset = 8 ,
2015-12-16 00:30:14 -05:00
pesTimeScale = this . PES _TIMESCALE ,
pes2mp4ScaleFactor = this . PES2MP4SCALEFACTOR ,
2016-04-20 14:51:47 -04:00
avcSample ,
mp4Sample ,
mp4SampleLength ,
unit ,
2015-12-16 00:30:14 -05:00
mdat , moof ,
2016-04-20 14:51:47 -04:00
firstPTS , firstDTS , lastDTS ,
pts , dts , ptsnorm , dtsnorm ,
flags ,
samples = [ ] ;
2015-12-16 00:30:14 -05:00
/ * c o n c a t e n a t e t h e v i d e o d a t a a n d c o n s t r u c t t h e m d a t i n p l a c e
( need 8 more bytes to fill length and mpdat type ) * /
mdat = new Uint8Array ( track . len + ( 4 * track . nbNalu ) + 8 ) ;
2016-04-20 14:51:47 -04:00
view = new DataView ( mdat . buffer ) ;
2015-12-16 00:30:14 -05:00
view . setUint32 ( 0 , mdat . byteLength ) ;
mdat . set ( MP4 . types . mdat , 4 ) ;
2016-04-20 14:51:47 -04:00
while ( track . samples . length ) {
avcSample = track . samples . shift ( ) ;
mp4SampleLength = 0 ;
2015-12-16 00:30:14 -05:00
// convert NALU bitstream to MP4 format (prepend NALU with size field)
while ( avcSample . units . units . length ) {
2016-04-20 14:51:47 -04:00
unit = avcSample . units . units . shift ( ) ;
2016-01-13 15:58:12 -05:00
view . setUint32 ( offset , unit . data . byteLength ) ;
offset += 4 ;
mdat . set ( unit . data , offset ) ;
offset += unit . data . byteLength ;
2015-12-16 00:30:14 -05:00
mp4SampleLength += 4 + unit . data . byteLength ;
}
2016-04-20 14:51:47 -04:00
pts = avcSample . pts - this . _initDTS ;
dts = avcSample . dts - this . _initDTS ;
// ensure DTS is not bigger than PTS
dts = Math . min ( pts , dts ) ;
//logger.log(`Video/PTS/DTS:${Math.round(pts/90)}/${Math.round(dts/90)}`);
// if not first AVC sample of video track, normalize PTS/DTS with previous sample value
// and ensure that sample duration is positive
if ( lastDTS !== undefined ) {
ptsnorm = this . _PTSNormalize ( pts , lastDTS ) ;
dtsnorm = this . _PTSNormalize ( dts , lastDTS ) ;
var sampleDuration = ( dtsnorm - lastDTS ) / pes2mp4ScaleFactor ;
if ( sampleDuration <= 0 ) {
logger . log ( ` invalid sample duration at PTS/DTS: ${ avcSample . pts } / ${ avcSample . dts } : ${ sampleDuration } ` ) ;
sampleDuration = 1 ;
2016-03-18 13:28:45 -04:00
}
2016-04-20 14:51:47 -04:00
mp4Sample . duration = sampleDuration ;
2016-04-16 12:51:35 -04:00
} else {
2016-04-20 14:51:47 -04:00
let nextAvcDts , delta ;
if ( contiguous ) {
nextAvcDts = this . nextAvcDts ;
} else {
nextAvcDts = timeOffset * pesTimeScale ;
}
// first AVC sample of video track, normalize PTS/DTS
ptsnorm = this . _PTSNormalize ( pts , nextAvcDts ) ;
dtsnorm = this . _PTSNormalize ( dts , nextAvcDts ) ;
delta = Math . round ( ( dtsnorm - nextAvcDts ) / 90 ) ;
// if fragment are contiguous, detect hole/overlapping between fragments
if ( contiguous ) {
if ( delta ) {
if ( delta > 1 ) {
logger . log ( ` AVC: ${ delta } ms hole between fragments detected,filling it ` ) ;
} else if ( delta < - 1 ) {
logger . log ( ` AVC: ${ ( - delta ) } ms overlapping between fragments detected ` ) ;
}
// set DTS to next DTS
dtsnorm = nextAvcDts ;
// offset PTS as well, ensure that PTS is smaller or equal than new DTS
ptsnorm = Math . max ( ptsnorm - delta , dtsnorm ) ;
logger . log ( ` Video/PTS/DTS adjusted: ${ ptsnorm } / ${ dtsnorm } ,delta: ${ delta } ` ) ;
}
}
// remember first PTS of our avcSamples, ensure value is positive
firstPTS = Math . max ( 0 , ptsnorm ) ;
firstDTS = Math . max ( 0 , dtsnorm ) ;
2015-12-16 00:30:14 -05:00
}
//console.log('PTS/DTS/initDTS/normPTS/normDTS/relative PTS : ${avcSample.pts}/${avcSample.dts}/${this._initDTS}/${ptsnorm}/${dtsnorm}/${(avcSample.pts/4294967296).toFixed(3)}');
2016-04-20 14:51:47 -04:00
mp4Sample = {
2015-12-16 00:30:14 -05:00
size : mp4SampleLength ,
2016-04-20 14:51:47 -04:00
duration : 0 ,
cts : ( ptsnorm - dtsnorm ) / pes2mp4ScaleFactor ,
2015-12-16 00:30:14 -05:00
flags : {
isLeading : 0 ,
isDependedOn : 0 ,
hasRedundancy : 0 ,
2016-04-20 14:51:47 -04:00
degradPrio : 0
2015-12-16 00:30:14 -05:00
}
2016-04-20 14:51:47 -04:00
} ;
flags = mp4Sample . flags ;
if ( avcSample . key === true ) {
// the current sample is a key frame
flags . dependsOn = 2 ;
flags . isNonSync = 0 ;
} else {
flags . dependsOn = 1 ;
flags . isNonSync = 1 ;
}
samples . push ( mp4Sample ) ;
lastDTS = dtsnorm ;
2015-12-16 00:30:14 -05:00
}
2016-04-20 14:51:47 -04:00
var lastSampleDuration = 0 ;
if ( samples . length >= 2 ) {
lastSampleDuration = samples [ samples . length - 2 ] . duration ;
mp4Sample . duration = lastSampleDuration ;
}
// next AVC sample DTS should be equal to last sample DTS + last sample duration
this . nextAvcDts = dtsnorm + lastSampleDuration * pes2mp4ScaleFactor ;
2015-12-16 00:30:14 -05:00
track . len = 0 ;
track . nbNalu = 0 ;
2016-04-20 14:51:47 -04:00
if ( samples . length && navigator . userAgent . toLowerCase ( ) . indexOf ( 'chrome' ) > - 1 ) {
flags = samples [ 0 ] . flags ;
2015-12-16 00:30:14 -05:00
// chrome workaround, mark first sample as being a Random Access Point to avoid sourcebuffer append issue
// https://code.google.com/p/chromium/issues/detail?id=229412
2016-01-13 15:58:12 -05:00
flags . dependsOn = 2 ;
flags . isNonSync = 0 ;
2015-12-16 00:30:14 -05:00
}
2016-04-20 14:51:47 -04:00
track . samples = samples ;
2015-12-16 00:30:14 -05:00
moof = MP4 . moof ( track . sequenceNumber ++ , firstDTS / pes2mp4ScaleFactor , track ) ;
track . samples = [ ] ;
this . observer . trigger ( Event . FRAG _PARSING _DATA , {
2016-02-24 22:15:07 -05:00
data1 : moof ,
data2 : mdat ,
2015-12-16 00:30:14 -05:00
startPTS : firstPTS / pesTimeScale ,
2016-04-20 14:51:47 -04:00
endPTS : ( ptsnorm + pes2mp4ScaleFactor * lastSampleDuration ) / pesTimeScale ,
2015-12-16 00:30:14 -05:00
startDTS : firstDTS / pesTimeScale ,
2016-01-13 15:58:12 -05:00
endDTS : this . nextAvcDts / pesTimeScale ,
2015-12-16 00:30:14 -05:00
type : 'video' ,
2016-04-20 14:51:47 -04:00
nb : samples . length
2015-12-16 00:30:14 -05:00
} ) ;
}
remuxAudio ( track , timeOffset , contiguous ) {
var view ,
2016-01-13 15:58:12 -05:00
offset = 8 ,
2015-12-16 00:30:14 -05:00
pesTimeScale = this . PES _TIMESCALE ,
2016-02-03 18:00:01 -05:00
mp4timeScale = track . timescale ,
pes2mp4ScaleFactor = pesTimeScale / mp4timeScale ,
2016-03-18 13:28:45 -04:00
expectedSampleDuration = track . timescale * 1024 / track . audiosamplerate ,
2015-12-16 00:30:14 -05:00
aacSample , mp4Sample ,
unit ,
mdat , moof ,
firstPTS , firstDTS , lastDTS ,
pts , dts , ptsnorm , dtsnorm ,
2016-01-13 15:58:12 -05:00
samples = [ ] ,
samples0 = [ ] ;
2016-02-03 18:00:01 -05:00
track . samples . sort ( function ( a , b ) {
return ( a . pts - b . pts ) ;
2016-01-13 15:58:12 -05:00
} ) ;
2016-02-03 18:00:01 -05:00
samples0 = track . samples ;
2016-01-13 15:58:12 -05:00
while ( samples0 . length ) {
aacSample = samples0 . shift ( ) ;
2015-12-16 00:30:14 -05:00
unit = aacSample . unit ;
pts = aacSample . pts - this . _initDTS ;
dts = aacSample . dts - this . _initDTS ;
2016-01-18 14:07:26 -05:00
//logger.log(`Audio/PTS:${Math.round(pts/90)}`);
2016-01-13 15:58:12 -05:00
// if not first sample
2015-12-16 00:30:14 -05:00
if ( lastDTS !== undefined ) {
ptsnorm = this . _PTSNormalize ( pts , lastDTS ) ;
dtsnorm = this . _PTSNormalize ( dts , lastDTS ) ;
2016-02-03 18:00:01 -05:00
// let's compute sample duration.
2016-03-18 13:28:45 -04:00
// sample Duration should be close to expectedSampleDuration
2015-12-16 00:30:14 -05:00
mp4Sample . duration = ( dtsnorm - lastDTS ) / pes2mp4ScaleFactor ;
2016-03-18 13:28:45 -04:00
if ( Math . abs ( mp4Sample . duration - expectedSampleDuration ) > expectedSampleDuration / 10 ) {
// more than 10% diff between sample duration and expectedSampleDuration .... lets log that
2016-04-20 14:51:47 -04:00
logger . log ( ` invalid AAC sample duration at PTS ${ Math . round ( pts / 90 ) } ,should be 1024,found : ${ Math . round ( mp4Sample . duration * track . audiosamplerate / track . timescale ) } ` ) ;
2015-12-16 00:30:14 -05:00
}
2016-03-18 13:28:45 -04:00
// always adjust sample duration to avoid av sync issue
mp4Sample . duration = expectedSampleDuration ;
dtsnorm = expectedSampleDuration * pes2mp4ScaleFactor + lastDTS ;
2015-12-16 00:30:14 -05:00
} else {
2016-03-18 13:28:45 -04:00
let nextAacPts , delta ;
if ( contiguous ) {
nextAacPts = this . nextAacPts ;
} else {
nextAacPts = timeOffset * pesTimeScale ;
}
2015-12-16 00:30:14 -05:00
ptsnorm = this . _PTSNormalize ( pts , nextAacPts ) ;
dtsnorm = this . _PTSNormalize ( dts , nextAacPts ) ;
delta = Math . round ( 1000 * ( ptsnorm - nextAacPts ) / pesTimeScale ) ;
2016-04-20 14:51:47 -04:00
// if fragment are contiguous, detect hole/overlapping between fragments
if ( contiguous ) {
2015-12-16 00:30:14 -05:00
// log delta
if ( delta ) {
2016-01-13 15:58:12 -05:00
if ( delta > 0 ) {
2015-12-16 00:30:14 -05:00
logger . log ( ` ${ delta } ms hole between AAC samples detected,filling it ` ) ;
2016-04-20 14:51:47 -04:00
// if we have frame overlap, overlapping for more than half a frame duration
2016-01-25 15:28:29 -05:00
} else if ( delta < - 12 ) {
2016-01-13 15:58:12 -05:00
// drop overlapping audio frames... browser will deal with it
logger . log ( ` ${ ( - delta ) } ms overlapping between AAC samples detected, drop frame ` ) ;
track . len -= unit . byteLength ;
continue ;
2015-12-16 00:30:14 -05:00
}
2016-04-20 14:51:47 -04:00
// set PTS/DTS to next PTS/DTS
2015-12-16 00:30:14 -05:00
ptsnorm = dtsnorm = nextAacPts ;
}
}
// remember first PTS of our aacSamples, ensure value is positive
firstPTS = Math . max ( 0 , ptsnorm ) ;
firstDTS = Math . max ( 0 , dtsnorm ) ;
2016-02-01 12:02:17 -05:00
if ( track . len > 0 ) {
/ * c o n c a t e n a t e t h e a u d i o d a t a a n d c o n s t r u c t t h e m d a t i n p l a c e
( need 8 more bytes to fill length and mdat type ) * /
mdat = new Uint8Array ( track . len + 8 ) ;
view = new DataView ( mdat . buffer ) ;
view . setUint32 ( 0 , mdat . byteLength ) ;
mdat . set ( MP4 . types . mdat , 4 ) ;
} else {
// no audio samples
return ;
}
2015-12-16 00:30:14 -05:00
}
2016-01-13 15:58:12 -05:00
mdat . set ( unit , offset ) ;
offset += unit . byteLength ;
2015-12-16 00:30:14 -05:00
//console.log('PTS/DTS/initDTS/normPTS/normDTS/relative PTS : ${aacSample.pts}/${aacSample.dts}/${this._initDTS}/${ptsnorm}/${dtsnorm}/${(aacSample.pts/4294967296).toFixed(3)}');
mp4Sample = {
size : unit . byteLength ,
cts : 0 ,
duration : 0 ,
flags : {
isLeading : 0 ,
isDependedOn : 0 ,
hasRedundancy : 0 ,
degradPrio : 0 ,
dependsOn : 1 ,
}
} ;
samples . push ( mp4Sample ) ;
lastDTS = dtsnorm ;
}
2016-01-13 15:58:12 -05:00
var lastSampleDuration = 0 ;
var nbSamples = samples . length ;
2015-12-16 00:30:14 -05:00
//set last sample duration as being identical to previous sample
2016-01-13 15:58:12 -05:00
if ( nbSamples >= 2 ) {
lastSampleDuration = samples [ nbSamples - 2 ] . duration ;
mp4Sample . duration = lastSampleDuration ;
}
if ( nbSamples ) {
// next aac sample PTS should be equal to last sample PTS + duration
this . nextAacPts = ptsnorm + pes2mp4ScaleFactor * lastSampleDuration ;
//logger.log('Audio/PTS/PTSend:' + aacSample.pts.toFixed(0) + '/' + this.nextAacDts.toFixed(0));
track . len = 0 ;
track . samples = samples ;
moof = MP4 . moof ( track . sequenceNumber ++ , firstDTS / pes2mp4ScaleFactor , track ) ;
track . samples = [ ] ;
this . observer . trigger ( Event . FRAG _PARSING _DATA , {
2016-02-24 22:15:07 -05:00
data1 : moof ,
data2 : mdat ,
2016-01-13 15:58:12 -05:00
startPTS : firstPTS / pesTimeScale ,
endPTS : this . nextAacPts / pesTimeScale ,
startDTS : firstDTS / pesTimeScale ,
endDTS : ( dtsnorm + pes2mp4ScaleFactor * lastSampleDuration ) / pesTimeScale ,
type : 'audio' ,
nb : nbSamples
} ) ;
2015-12-16 00:30:14 -05:00
}
}
remuxID3 ( track , timeOffset ) {
var length = track . samples . length , sample ;
// consume samples
if ( length ) {
for ( var index = 0 ; index < length ; index ++ ) {
sample = track . samples [ index ] ;
// setting id3 pts, dts to relative time
// using this._initPTS and this._initDTS to calculate relative time
sample . pts = ( ( sample . pts - this . _initPTS ) / this . PES _TIMESCALE ) ;
sample . dts = ( ( sample . dts - this . _initDTS ) / this . PES _TIMESCALE ) ;
}
this . observer . trigger ( Event . FRAG _PARSING _METADATA , {
samples : track . samples
} ) ;
}
track . samples = [ ] ;
timeOffset = timeOffset ;
}
2016-02-01 12:02:17 -05:00
remuxText ( track , timeOffset ) {
track . samples . sort ( function ( a , b ) {
return ( a . pts - b . pts ) ;
} ) ;
var length = track . samples . length , sample ;
// consume samples
if ( length ) {
for ( var index = 0 ; index < length ; index ++ ) {
sample = track . samples [ index ] ;
// setting text pts, dts to relative time
// using this._initPTS and this._initDTS to calculate relative time
sample . pts = ( ( sample . pts - this . _initPTS ) / this . PES _TIMESCALE ) ;
}
this . observer . trigger ( Event . FRAG _PARSING _USERDATA , {
samples : track . samples
} ) ;
}
track . samples = [ ] ;
timeOffset = timeOffset ;
}
2015-12-16 00:30:14 -05:00
_PTSNormalize ( value , reference ) {
var offset ;
if ( reference === undefined ) {
return value ;
}
if ( reference < value ) {
// - 2^33
offset = - 8589934592 ;
} else {
// + 2^33
offset = 8589934592 ;
}
/ * P T S i s 3 3 b i t ( f r o m 0 t o 2 ^ 3 3 - 1 )
if diff between value and reference is bigger than half of the amplitude ( 2 ^ 32 ) then it means that
PTS looping occured . fill the gap * /
while ( Math . abs ( value - reference ) > 4294967296 ) {
value += offset ;
}
return value ;
}
}
export default MP4Remuxer ;