209 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			209 lines
		
	
	
		
			5.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /**
 | |
|  * Constructs a single-track, ISO BMFF media segment from H264 data
 | |
|  * events. The output of this stream can be fed to a SourceBuffer
 | |
|  * configured with a suitable initialization segment.
 | |
|  * @param track {object} track metadata configuration
 | |
|  * @param options {object} transmuxer options object
 | |
|  * @param options.alignGopsAtEnd {boolean} If true, start from the end of the
 | |
|  *        gopsToAlignWith list when attempting to align gop pts
 | |
|  */
 | |
| 'use strict';
 | |
| 
 | |
| var Stream = require('../utils/stream.js');
 | |
| var mp4 = require('../mp4/mp4-generator.js');
 | |
| var trackInfo = require('../mp4/track-decode-info.js');
 | |
| var frameUtils = require('../mp4/frame-utils');
 | |
| var VIDEO_PROPERTIES = require('../constants/video-properties.js');
 | |
| 
 | |
| var VideoSegmentStream = function(track, options) {
 | |
|   var
 | |
|     sequenceNumber = 0,
 | |
|     nalUnits = [],
 | |
|     frameCache = [],
 | |
|     // gopsToAlignWith = [],
 | |
|     config,
 | |
|     pps,
 | |
|     segmentStartPts = null,
 | |
|     segmentEndPts = null,
 | |
|     gops,
 | |
|     ensureNextFrameIsKeyFrame = true;
 | |
| 
 | |
|   options = options || {};
 | |
| 
 | |
|   VideoSegmentStream.prototype.init.call(this);
 | |
| 
 | |
|   this.push = function(nalUnit) {
 | |
|     trackInfo.collectDtsInfo(track, nalUnit);
 | |
|     if (typeof track.timelineStartInfo.dts === 'undefined') {
 | |
|       track.timelineStartInfo.dts = nalUnit.dts;
 | |
|     }
 | |
| 
 | |
|     // record the track config
 | |
|     if (nalUnit.nalUnitType === 'seq_parameter_set_rbsp' && !config) {
 | |
|       config = nalUnit.config;
 | |
|       track.sps = [nalUnit.data];
 | |
| 
 | |
|       VIDEO_PROPERTIES.forEach(function(prop) {
 | |
|         track[prop] = config[prop];
 | |
|       }, this);
 | |
|     }
 | |
| 
 | |
|     if (nalUnit.nalUnitType === 'pic_parameter_set_rbsp' &&
 | |
|         !pps) {
 | |
|       pps = nalUnit.data;
 | |
|       track.pps = [nalUnit.data];
 | |
|     }
 | |
| 
 | |
|     // buffer video until flush() is called
 | |
|     nalUnits.push(nalUnit);
 | |
|   };
 | |
| 
 | |
|   this.processNals_ = function(cacheLastFrame) {
 | |
|     var i;
 | |
| 
 | |
|     nalUnits = frameCache.concat(nalUnits);
 | |
| 
 | |
|     // Throw away nalUnits at the start of the byte stream until
 | |
|     // we find the first AUD
 | |
|     while (nalUnits.length) {
 | |
|       if (nalUnits[0].nalUnitType === 'access_unit_delimiter_rbsp') {
 | |
|         break;
 | |
|       }
 | |
|       nalUnits.shift();
 | |
|     }
 | |
| 
 | |
|     // Return early if no video data has been observed
 | |
|     if (nalUnits.length === 0) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     var frames = frameUtils.groupNalsIntoFrames(nalUnits);
 | |
| 
 | |
|     if (!frames.length) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // note that the frame cache may also protect us from cases where we haven't
 | |
|     // pushed data for the entire first or last frame yet
 | |
|     frameCache = frames[frames.length - 1];
 | |
| 
 | |
|     if (cacheLastFrame) {
 | |
|       frames.pop();
 | |
|       frames.duration -= frameCache.duration;
 | |
|       frames.nalCount -= frameCache.length;
 | |
|       frames.byteLength -= frameCache.byteLength;
 | |
|     }
 | |
| 
 | |
|     if (!frames.length) {
 | |
|       nalUnits = [];
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     this.trigger('timelineStartInfo', track.timelineStartInfo);
 | |
| 
 | |
|     if (ensureNextFrameIsKeyFrame) {
 | |
|       gops = frameUtils.groupFramesIntoGops(frames);
 | |
| 
 | |
|       if (!gops[0][0].keyFrame) {
 | |
|         gops = frameUtils.extendFirstKeyFrame(gops);
 | |
| 
 | |
|         if (!gops[0][0].keyFrame) {
 | |
|           // we haven't yet gotten a key frame, so reset nal units to wait for more nal
 | |
|           // units
 | |
|           nalUnits = ([].concat.apply([], frames)).concat(frameCache);
 | |
|           frameCache = [];
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         frames = [].concat.apply([], gops);
 | |
|         frames.duration = gops.duration;
 | |
|       }
 | |
|       ensureNextFrameIsKeyFrame = false;
 | |
|     }
 | |
| 
 | |
|     if (segmentStartPts === null) {
 | |
|       segmentStartPts = frames[0].pts;
 | |
|       segmentEndPts = segmentStartPts;
 | |
|     }
 | |
| 
 | |
|     segmentEndPts += frames.duration;
 | |
| 
 | |
|     this.trigger('timingInfo', {
 | |
|       start: segmentStartPts,
 | |
|       end: segmentEndPts
 | |
|     });
 | |
| 
 | |
|     for (i = 0; i < frames.length; i++) {
 | |
|       var frame = frames[i];
 | |
| 
 | |
|       track.samples = frameUtils.generateSampleTableForFrame(frame);
 | |
| 
 | |
|       var mdat = mp4.mdat(frameUtils.concatenateNalDataForFrame(frame));
 | |
| 
 | |
|       trackInfo.clearDtsInfo(track);
 | |
|       trackInfo.collectDtsInfo(track, frame);
 | |
| 
 | |
|       track.baseMediaDecodeTime = trackInfo.calculateTrackBaseMediaDecodeTime(
 | |
|         track, options.keepOriginalTimestamps);
 | |
| 
 | |
|       var moof = mp4.moof(sequenceNumber, [track]);
 | |
| 
 | |
|       sequenceNumber++;
 | |
| 
 | |
|       track.initSegment = mp4.initSegment([track]);
 | |
| 
 | |
|       var boxes = new Uint8Array(moof.byteLength + mdat.byteLength);
 | |
| 
 | |
|       boxes.set(moof);
 | |
|       boxes.set(mdat, moof.byteLength);
 | |
| 
 | |
|       this.trigger('data', {
 | |
|         track: track,
 | |
|         boxes: boxes,
 | |
|         sequence: sequenceNumber,
 | |
|         videoFrameDts: frame.dts,
 | |
|         videoFramePts: frame.pts
 | |
|       });
 | |
|     }
 | |
| 
 | |
|     nalUnits = [];
 | |
|   };
 | |
| 
 | |
|   this.resetTimingAndConfig_ = function() {
 | |
|     config = undefined;
 | |
|     pps = undefined;
 | |
|     segmentStartPts = null;
 | |
|     segmentEndPts = null;
 | |
|   };
 | |
| 
 | |
|   this.partialFlush = function() {
 | |
|     this.processNals_(true);
 | |
|     this.trigger('partialdone', 'VideoSegmentStream');
 | |
|   };
 | |
| 
 | |
|   this.flush = function() {
 | |
|     this.processNals_(false);
 | |
|     // reset config and pps because they may differ across segments
 | |
|     // for instance, when we are rendition switching
 | |
|     this.resetTimingAndConfig_();
 | |
|     this.trigger('done', 'VideoSegmentStream');
 | |
|   };
 | |
| 
 | |
|   this.endTimeline = function() {
 | |
|     this.flush();
 | |
|     this.trigger('endedtimeline', 'VideoSegmentStream');
 | |
|   };
 | |
| 
 | |
|   this.reset = function() {
 | |
|     this.resetTimingAndConfig_();
 | |
|     frameCache = [];
 | |
|     nalUnits = [];
 | |
|     ensureNextFrameIsKeyFrame = true;
 | |
|     this.trigger('reset');
 | |
|   };
 | |
| };
 | |
| 
 | |
| VideoSegmentStream.prototype = new Stream();
 | |
| 
 | |
| module.exports = VideoSegmentStream;
 | 
