update components

This commit is contained in:
Luke Pulverenti 2016-02-01 12:02:17 -05:00
parent 2a72f0256e
commit 048ba20590
26 changed files with 1377 additions and 136 deletions

View file

@ -60,11 +60,12 @@ class LevelController extends EventHandler {
// only keep level with supported audio/video codecs
levels = levels.filter(function(level) {
var checkSupported = function(codec) { return MediaSource.isTypeSupported(`video/mp4;codecs=${codec}`);};
var checkSupportedAudio = function(codec) { return MediaSource.isTypeSupported(`audio/mp4;codecs=${codec}`);};
var checkSupportedVideo = function(codec) { return MediaSource.isTypeSupported(`video/mp4;codecs=${codec}`);};
var audioCodec = level.audioCodec, videoCodec = level.videoCodec;
return (!audioCodec || checkSupported(audioCodec)) &&
(!videoCodec || checkSupported(videoCodec));
return (!audioCodec || checkSupportedAudio(audioCodec)) &&
(!videoCodec || checkSupportedVideo(videoCodec));
});
if(levels.length) {

View file

@ -1076,7 +1076,7 @@ class MSEMediaController extends EventHandler {
logger.log(`selected A/V codecs for sourceBuffers:${audioCodec},${videoCodec}`);
// create source Buffer and link them to MediaSource
if (audioCodec) {
sb = this.sourceBuffer.audio = this.mediaSource.addSourceBuffer(`video/mp4;codecs=${audioCodec}`);
sb = this.sourceBuffer.audio = this.mediaSource.addSourceBuffer(`audio/mp4;codecs=${audioCodec}`);
sb.addEventListener('updateend', this.onsbue);
sb.addEventListener('error', this.onsbe);
}

View file

@ -0,0 +1,69 @@
/*
* Timeline Controller
*/
import Event from '../events';
import EventHandler from '../event-handler';
import CEA708Interpreter from '../utils/cea-708-interpreter';
class TimelineController extends EventHandler {
constructor(hls) {
super(hls, Event.MEDIA_ATTACHING,
Event.MEDIA_DETACHING,
Event.FRAG_PARSING_USERDATA,
Event.MANIFEST_LOADING,
Event.FRAG_LOADED);
this.hls = hls;
this.config = hls.config;
if (this.config.enableCEA708Captions)
{
this.cea708Interpreter = new CEA708Interpreter();
}
}
destroy() {
EventHandler.prototype.destroy.call(this);
}
onMediaAttaching(data) {
var media = this.media = data.media;
this.cea708Interpreter.attach(media);
}
onMediaDetaching() {
this.cea708Interpreter.detach();
}
onManifestLoading()
{
this.lastPts = Number.POSITIVE_INFINITY;
}
onFragLoaded(data)
{
var pts = data.frag.start; //Number.POSITIVE_INFINITY;
// if this is a frag for a previously loaded timerange, remove all captions
// TODO: consider just removing captions for the timerange
if (pts <= this.lastPts)
{
this.cea708Interpreter.clear();
}
this.lastPts = pts;
}
onFragParsingUserdata(data) {
// push all of the CEA-708 messages into the interpreter
// immediately. It will create the proper timestamps based on our PTS value
for (var i=0; i<data.samples.length; i++)
{
this.cea708Interpreter.push(data.samples[i].pts, data.samples[i].bytes);
}
}
}
export default TimelineController;

View file

@ -72,6 +72,12 @@ var DemuxerWorker = function (self) {
var objData = {event: event, samples: data.samples};
self.postMessage(objData);
});
observer.on(Event.FRAG_PARSING_USERDATA, function(event, data) {
var objData = {event: event, samples: data.samples};
self.postMessage(objData);
});
};
export default DemuxerWorker;

View file

@ -101,6 +101,11 @@ class Demuxer {
samples: ev.data.samples
});
break;
case Event.FRAG_PARSING_USERDATA:
this.hls.trigger(Event.FRAG_PARSING_USERDATA, {
samples: ev.data.samples
});
break;
default:
this.hls.trigger(ev.data.event, ev.data.data);
break;

View file

@ -125,6 +125,15 @@ class ExpGolomb {
return this.readBits(8);
}
// ():int
readUShort() {
return this.readBits(16);
}
// ():int
readUInt() {
return this.readBits(32);
}
/**
* Advance the ExpGolomb decoder past a scaling list. The scaling
* list is optionally transmitted as part of a sequence parameter

View file

@ -23,6 +23,7 @@
this.remuxerClass = remuxerClass;
this.lastCC = 0;
this.remuxer = new this.remuxerClass(observer);
this._userData = [];
}
static probe(data) {
@ -42,6 +43,7 @@
this._avcTrack = {type: 'video', id :-1, sequenceNumber: 0, samples : [], len : 0, nbNalu : 0};
this._aacTrack = {type: 'audio', id :-1, sequenceNumber: 0, samples : [], len : 0};
this._id3Track = {type: 'id3', id :-1, sequenceNumber: 0, samples : [], len : 0};
this._txtTrack = {type: 'text', id: -1, sequenceNumber: 0, samples: [], len: 0};
this.remuxer.switchLevel();
}
@ -81,6 +83,9 @@
avcId = this._avcTrack.id,
aacId = this._aacTrack.id,
id3Id = this._id3Track.id;
// don't parse last TS packet if incomplete
len -= len % 188;
// loop through TS packets
for (start = 0; start < len; start += 188) {
if (data[start] === 0x47) {
@ -165,7 +170,7 @@
}
remux() {
this.remuxer.remux(this._aacTrack,this._avcTrack, this._id3Track, this.timeOffset, this.contiguous);
this.remuxer.remux(this._aacTrack, this._avcTrack, this._id3Track, this._txtTrack, this.timeOffset, this.contiguous);
}
destroy() {
@ -281,8 +286,10 @@
debug = false,
key = false,
length = 0,
expGolombDecoder,
avcSample,
push;
push,
i;
// no NALu found
if (units.length === 0 && samples.length > 0) {
// append pes.data to previous NAL unit
@ -298,6 +305,7 @@
//free pes.data to save up some memory
pes.data = null;
var debugString = '';
units.forEach(unit => {
switch(unit.type) {
//NDR
@ -315,11 +323,67 @@
}
key = true;
break;
//SEI
case 6:
push = true;
if(debug) {
debugString += 'SEI ';
}
expGolombDecoder = new ExpGolomb(unit.data);
// skip frameType
expGolombDecoder.readUByte();
var payloadType = expGolombDecoder.readUByte();
// TODO: there can be more than one payload in an SEI packet...
// TODO: need to read type and size in a while loop to get them all
if (payloadType === 4)
{
var payloadSize = 0;
do {
payloadSize = expGolombDecoder.readUByte();
}
while (payloadSize === 255);
var countryCode = expGolombDecoder.readUByte();
if (countryCode === 181)
{
var providerCode = expGolombDecoder.readUShort();
if (providerCode === 49)
{
var userStructure = expGolombDecoder.readUInt();
if (userStructure === 0x47413934)
{
var userDataType = expGolombDecoder.readUByte();
// Raw CEA-608 bytes wrapped in CEA-708 packet
if (userDataType === 3)
{
var firstByte = expGolombDecoder.readUByte();
var secondByte = expGolombDecoder.readUByte();
var totalCCs = 31 & firstByte;
var byteArray = [firstByte, secondByte];
for (i=0; i<totalCCs; i++)
{
// 3 bytes per CC
byteArray.push(expGolombDecoder.readUByte());
byteArray.push(expGolombDecoder.readUByte());
byteArray.push(expGolombDecoder.readUByte());
}
this._txtTrack.samples.push({type: 3, pts: pes.pts, bytes: byteArray});
}
}
}
}
}
break;
//SPS
case 7:
@ -328,7 +392,7 @@
debugString += 'SPS ';
}
if(!track.sps) {
var expGolombDecoder = new ExpGolomb(unit.data);
expGolombDecoder = new ExpGolomb(unit.data);
var config = expGolombDecoder.readSPS();
track.width = config.width;
track.height = config.height;
@ -337,7 +401,7 @@
track.duration = this.remuxer.timescale * this._duration;
var codecarray = unit.data.subarray(1, 4);
var codecstring = 'avc1.';
for (var i = 0; i < 3; i++) {
for (i = 0; i < 3; i++) {
var h = codecarray[i].toString(16);
if (h.length < 2) {
h = '0' + h;
@ -358,7 +422,7 @@
}
break;
case 9:
push = true;
push = false;
if(debug) {
debugString += 'AUD ';
}
@ -443,10 +507,6 @@
}
lastUnitStart = i;
lastUnitType = unitType;
if (unitType === 1 || unitType === 5) {
// OPTI !!! if IDR/NDR unit, consider it is last NALu
i = len;
}
state = 0;
} else {
state = 0;

View file

@ -33,6 +33,8 @@ module.exports = {
FRAG_LOADED: 'hlsFragLoaded',
// fired when Init Segment has been extracted from fragment - data: { moov : moov MP4 box, codecs : codecs found while parsing fragment}
FRAG_PARSING_INIT_SEGMENT: 'hlsFragParsingInitSegment',
// fired when parsing sei text is completed - data: { samples : [ sei samples pes ] }
FRAG_PARSING_USERDATA: 'hlsFragParsingUserdata',
// fired when parsing id3 is completed - data: { samples : [ id3 samples pes ] }
FRAG_PARSING_METADATA: 'hlsFragParsingMetadata',
// fired when moof/mdat have been extracted from fragment - data: { moof : moof MP4 box, mdat : mdat MP4 box}

View file

@ -10,6 +10,7 @@ import FragmentLoader from './loader/fragment-loader';
import AbrController from './controller/abr-controller';
import MSEMediaController from './controller/mse-media-controller';
import LevelController from './controller/level-controller';
import TimelineController from './controller/timeline-controller';
//import FPSController from './controller/fps-controller';
import {logger, enableLogs} from './utils/logger';
import XhrLoader from './utils/xhr-loader';
@ -65,7 +66,9 @@ class Hls {
fLoader: undefined,
pLoader: undefined,
abrController : AbrController,
mediaController: MSEMediaController
mediaController: MSEMediaController,
timelineController: TimelineController,
enableCEA708Captions: true
};
}
return Hls.defaultConfig;
@ -105,6 +108,7 @@ class Hls {
this.levelController = new LevelController(this);
this.abrController = new config.abrController(this);
this.mediaController = new config.mediaController(this);
this.timelineController = new config.timelineController(this);
this.keyLoader = new KeyLoader(this);
//this.fpsController = new FPSController(this);
}
@ -117,6 +121,7 @@ class Hls {
this.fragmentLoader.destroy();
this.levelController.destroy();
this.mediaController.destroy();
this.timelineController.destroy();
this.keyLoader.destroy();
//this.fpsController.destroy();
this.url = null;

View file

@ -18,10 +18,11 @@ class DummyRemuxer {
insertDiscontinuity() {
}
remux(audioTrack,videoTrack,id3Track,timeOffset) {
remux(audioTrack,videoTrack,id3Track,textTrack,timeOffset) {
this._remuxAACSamples(audioTrack,timeOffset);
this._remuxAVCSamples(videoTrack,timeOffset);
this._remuxID3Samples(id3Track,timeOffset);
this._remuxTextSamples(textTrack,timeOffset);
}
_remuxAVCSamples(track, timeOffset) {
@ -59,6 +60,17 @@ class DummyRemuxer {
//please lint
timeOffset = timeOffset;
}
_remuxTextSamples(track,timeOffset) {
var textSample,bytes;
// loop through track.samples
while (track.samples.length) {
textSample = track.samples.shift();
bytes = textSample.bytes;
}
//please lint
timeOffset = timeOffset;
}
}
export default DummyRemuxer;

View file

@ -355,16 +355,16 @@ class MP4 {
0x00, 0x48, 0x00, 0x00, // vertresolution
0x00, 0x00, 0x00, 0x00, // reserved
0x00, 0x01, // frame_count
0x13,
0x76, 0x69, 0x64, 0x65,
0x6f, 0x6a, 0x73, 0x2d,
0x63, 0x6f, 0x6e, 0x74,
0x72, 0x69, 0x62, 0x2d,
0x68, 0x6c, 0x73, 0x00,
0x12,
0x64, 0x61, 0x69, 0x6C, //dailymotion/hls.js
0x79, 0x6D, 0x6F, 0x74,
0x69, 0x6F, 0x6E, 0x2F,
0x68, 0x6C, 0x73, 0x2E,
0x6A, 0x73, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, // compressorname
0x00, 0x18, // depth = 24
0x00, 0x18, // depth = 24
0x11, 0x11]), // pre_defined = -1
avcc,
MP4.box(MP4.types.btrt, new Uint8Array([

View file

@ -32,7 +32,7 @@ class MP4Remuxer {
this.ISGenerated = false;
}
remux(audioTrack,videoTrack,id3Track,timeOffset, contiguous) {
remux(audioTrack,videoTrack,id3Track,textTrack,timeOffset, contiguous) {
// generate Init Segment if needed
if (!this.ISGenerated) {
this.generateIS(audioTrack,videoTrack,timeOffset);
@ -49,6 +49,10 @@ class MP4Remuxer {
if (id3Track.samples.length) {
this.remuxID3(id3Track,timeOffset);
}
//logger.log('nb ID3 samples:' + audioTrack.samples.length);
if (textTrack.samples.length) {
this.remuxText(textTrack,timeOffset);
}
//notify end of parsing
this.observer.trigger(Event.FRAG_PARSED);
}
@ -264,6 +268,7 @@ class MP4Remuxer {
pts = aacSample.pts;
} else {
logger.warn('dropping past audio frame');
track.len -= aacSample.unit.byteLength;
}
});
@ -309,12 +314,17 @@ class MP4Remuxer {
// remember first PTS of our aacSamples, ensure value is positive
firstPTS = Math.max(0, ptsnorm);
firstDTS = Math.max(0, dtsnorm);
/* concatenate the audio data and construct the mdat in place
(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);
if(track.len > 0) {
/* concatenate the audio data and construct the mdat in place
(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;
}
}
mdat.set(unit, offset);
offset += unit.byteLength;
@ -382,6 +392,29 @@ class MP4Remuxer {
timeOffset = timeOffset;
}
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;
}
_PTSNormalize(value, reference) {
var offset;
if (reference === undefined) {

View file

@ -0,0 +1,380 @@
/*
* CEA-708 interpreter
*/
class CEA708Interpreter {
constructor() {
}
attach(media) {
this.media = media;
this.display = [];
this.memory = [];
}
detach()
{
this.clear();
}
destroy() {
}
_createCue()
{
var VTTCue = window.VTTCue || window.TextTrackCue;
this.cue = new VTTCue(-1, -1, '');
this.cue.text = '';
this.cue.pauseOnExit = false;
// make sure it doesn't show up before it's ready
this.startTime = Number.MAX_VALUE;
// show it 'forever' once we do show it
// (we'll set the end time once we know it later)
this.cue.endTime = Number.MAX_VALUE;
this.memory.push(this.cue);
}
clear()
{
if (this._textTrack && this._textTrack.cues)
{
while (this._textTrack.cues.length > 0)
{
this._textTrack.removeCue(this._textTrack.cues[0]);
}
}
}
push(timestamp, bytes)
{
if (!this.cue)
{
this._createCue();
}
var count = bytes[0] & 31;
var position = 2;
var byte, ccbyte1, ccbyte2, ccValid, ccType;
for (var j=0; j<count; j++)
{
byte = bytes[position++];
ccbyte1 = 0x7F & bytes[position++];
ccbyte2 = 0x7F & bytes[position++];
ccValid = ((4 & byte) === 0 ? false : true);
ccType = (3 & byte);
if (ccbyte1 === 0 && ccbyte2 === 0)
{
continue;
}
if (ccValid)
{
if (ccType === 0) // || ccType === 1
{
// Standard Characters
if (0x20 & ccbyte1 || 0x40 & ccbyte1)
{
this.cue.text += this._fromCharCode(ccbyte1) + this._fromCharCode(ccbyte2);
}
// Special Characters
else if ((ccbyte1 === 0x11 || ccbyte1 === 0x19) && ccbyte2 >= 0x30 && ccbyte2 <= 0x3F)
{
// extended chars, e.g. musical note, accents
switch (ccbyte2)
{
case 48:
this.cue.text += '®';
break;
case 49:
this.cue.text += '°';
break;
case 50:
this.cue.text += '½';
break;
case 51:
this.cue.text += '¿';
break;
case 52:
this.cue.text += '™';
break;
case 53:
this.cue.text += '¢';
break;
case 54:
this.cue.text += '';
break;
case 55:
this.cue.text += '£';
break;
case 56:
this.cue.text += '♪';
break;
case 57:
this.cue.text += ' ';
break;
case 58:
this.cue.text += 'è';
break;
case 59:
this.cue.text += 'â';
break;
case 60:
this.cue.text += 'ê';
break;
case 61:
this.cue.text += 'î';
break;
case 62:
this.cue.text += 'ô';
break;
case 63:
this.cue.text += 'û';
break;
}
}
if ((ccbyte1 === 0x11 || ccbyte1 === 0x19) && ccbyte2 >= 0x20 && ccbyte2 <= 0x2F)
{
// Mid-row codes: color/underline
switch (ccbyte2)
{
case 0x20:
// White
break;
case 0x21:
// White Underline
break;
case 0x22:
// Green
break;
case 0x23:
// Green Underline
break;
case 0x24:
// Blue
break;
case 0x25:
// Blue Underline
break;
case 0x26:
// Cyan
break;
case 0x27:
// Cyan Underline
break;
case 0x28:
// Red
break;
case 0x29:
// Red Underline
break;
case 0x2A:
// Yellow
break;
case 0x2B:
// Yellow Underline
break;
case 0x2C:
// Magenta
break;
case 0x2D:
// Magenta Underline
break;
case 0x2E:
// Italics
break;
case 0x2F:
// Italics Underline
break;
}
}
if ((ccbyte1 === 0x14 || ccbyte1 === 0x1C) && ccbyte2 >= 0x20 && ccbyte2 <= 0x2F)
{
// Mid-row codes: color/underline
switch (ccbyte2)
{
case 0x20:
// TODO: shouldn't affect roll-ups...
this._clearActiveCues(timestamp);
// RCL: Resume Caption Loading
// begin pop on
break;
case 0x21:
// BS: Backspace
this.cue.text = this.cue.text.substr(0, this.cue.text.length-1);
break;
case 0x22:
// AOF: reserved (formerly alarm off)
break;
case 0x23:
// AON: reserved (formerly alarm on)
break;
case 0x24:
// DER: Delete to end of row
break;
case 0x25:
// RU2: roll-up 2 rows
//this._rollup(2);
break;
case 0x26:
// RU3: roll-up 3 rows
//this._rollup(3);
break;
case 0x27:
// RU4: roll-up 4 rows
//this._rollup(4);
break;
case 0x28:
// FON: Flash on
break;
case 0x29:
// RDC: Resume direct captioning
this._clearActiveCues(timestamp);
break;
case 0x2A:
// TR: Text Restart
break;
case 0x2B:
// RTD: Resume Text Display
break;
case 0x2C:
// EDM: Erase Displayed Memory
this._clearActiveCues(timestamp);
break;
case 0x2D:
// CR: Carriage Return
// only affects roll-up
//this._rollup(1);
break;
case 0x2E:
// ENM: Erase non-displayed memory
this._text = '';
break;
case 0x2F:
this._flipMemory(timestamp);
// EOC: End of caption
// hide any displayed captions and show any hidden one
break;
}
}
if ((ccbyte1 === 0x17 || ccbyte1 === 0x1F) && ccbyte2 >= 0x21 && ccbyte2 <= 0x23)
{
// Mid-row codes: color/underline
switch (ccbyte2)
{
case 0x21:
// TO1: tab offset 1 column
break;
case 0x22:
// TO1: tab offset 2 column
break;
case 0x23:
// TO1: tab offset 3 column
break;
}
}
else {
// Probably a pre-amble address code
}
}
}
}
}
_fromCharCode(byte)
{
switch (byte)
{
case 42:
return 'á';
case 2:
return 'á';
case 2:
return 'é';
case 4:
return 'í';
case 5:
return 'ó';
case 6:
return 'ú';
case 3:
return 'ç';
case 4:
return '÷';
case 5:
return 'Ñ';
case 6:
return 'ñ';
case 7:
return '█';
default:
return String.fromCharCode(byte);
}
}
_flipMemory(timestamp)
{
this._clearActiveCues(timestamp);
this._flushCaptions(timestamp);
}
_flushCaptions(timestamp)
{
if (!this._has708)
{
this._textTrack = this.media.addTextTrack('captions', 'English', 'en');
this._has708 = true;
}
for (var i=0; i<this.memory.length; i++)
{
this.memory[i].startTime = timestamp;
this._textTrack.addCue(this.memory[i]);
this.display.push(this.memory[i]);
}
this.memory = [];
this.cue = null;
}
_clearActiveCues(timestamp)
{
for (var i=0; i<this.display.length; i++)
{
this.display[i].endTime = timestamp;
}
this.display = [];
}
/* _rollUp(n)
{
// TODO: implement roll-up captions
}
*/
_clearBufferedCues()
{
//remove them all...
}
}
export default CEA708Interpreter;

View file

@ -46,8 +46,7 @@ var URLHelper = {
builtURL = baseURLDomain+URLHelper.buildAbsolutePath('', relativeURL.substring(1));
}
else {
var newPath = URLHelper.buildAbsolutePath(baseURLPath, relativeURL);
builtURL = baseURLDomain + newPath;
builtURL = URLHelper.buildAbsolutePath(baseURLDomain+baseURLPath, relativeURL);
}
// put the query and hash parts back