
11 changed files with 706 additions and 6 deletions
-
154src/m3u8/m3u8-parser.js
-
111src/m3u8/m3u8-tag-types.js
-
17src/m3u8/m3u8.js
-
51src/manifest-controller.js
-
70src/segment-controller.js
-
9test/manifest/brightcove_playlist_m3u8.js
-
57test/manifest/playlist.m3u8
-
57test/manifest/playlistM3U8data.js
-
23test/video-js-hls.html
-
154test/video-js-hls_test.js
-
9video-js-hls.iml
@ -0,0 +1,154 @@ |
|||
(function(window) { |
|||
var M3U8 = window.videojs.hls.M3U8; |
|||
|
|||
window.videojs.hls.M3U8Parser = function() { |
|||
|
|||
var self = this; |
|||
var tagTypes = window.videojs.hls.m3u8TagType; |
|||
var lines = []; |
|||
var data; |
|||
|
|||
self.getTagType = function( lineData ) { |
|||
for ( var s in tagTypes ) |
|||
{ |
|||
if (lineData.indexOf(tagTypes[s]) == 0) |
|||
{ |
|||
return tagTypes[s]; |
|||
} |
|||
} |
|||
} |
|||
|
|||
self.getTagValue = function ( lineData ) { |
|||
for ( var s in tagTypes ) |
|||
{ |
|||
if (lineData.indexOf(tagTypes[s]) == 0) |
|||
{ |
|||
return lineData.substr(tagTypes[s].length); |
|||
} |
|||
} |
|||
} |
|||
|
|||
self.parse = function( rawDataString ) { |
|||
data = new M3U8(); |
|||
|
|||
if( rawDataString != undefined && rawDataString.toString().length > 0 ) |
|||
{ |
|||
lines = rawDataString.split('\n'); |
|||
|
|||
lines.forEach( |
|||
function(value,index) { |
|||
switch( self.getTagType(value) ) |
|||
{ |
|||
case tagTypes.EXTM3U: |
|||
data.hasValidM3UTag = (index == 0); |
|||
if(!data.hasValidM3UTag) |
|||
{ |
|||
data.invalidReasons.push("Invalid EXTM3U Tag"); |
|||
} |
|||
break; |
|||
|
|||
case tagTypes.DISCONTINUITY: |
|||
break; |
|||
|
|||
case tagTypes.PLAYLIST_TYPE: |
|||
if(self.getTagValue(value) == "VOD" || self.getTagValue(value) == "EVENT") |
|||
{ |
|||
data.playlistType = self.getTagValue(value); |
|||
data.isPlaylist = true; |
|||
} else { |
|||
data.invalidReasons.push("Invalid Playlist Type Value"); |
|||
} |
|||
break; |
|||
|
|||
case tagTypes.EXTINF: |
|||
var segment = {url: "unknown", byterange: -1, targetDuration: data.targetDuration }; |
|||
|
|||
if( self.getTagType(lines[index+1]) == tagTypes.BYTERANGE ) |
|||
{ |
|||
segment.byterange = self.getTagValue(lines[index+1]).split('@'); |
|||
segment.url = lines[index+2]; |
|||
} else |
|||
{ |
|||
segment.url = lines[index+1]; |
|||
} |
|||
|
|||
data.mediaItems.push(segment); |
|||
|
|||
break; |
|||
|
|||
case tagTypes.STREAM_INF: |
|||
var rendition = {}; |
|||
var attributes = value.substr(tagTypes.STREAM_INF.length).split(','); |
|||
|
|||
attributes.forEach(function(attr_value,attr_index) { |
|||
if(isNaN(attr_value.split('=')[1])){ |
|||
rendition[attr_value.split('=')[0].toLowerCase()] = attr_value.split('=')[1]; |
|||
|
|||
if(rendition[attr_value.split('=')[0].toLowerCase()].split('x').length = 2) |
|||
{ |
|||
rendition.resolution = { |
|||
width: Number(rendition[attr_value.split('=')[0].toLowerCase()].split('x')[0]), |
|||
height: Number(rendition[attr_value.split('=')[0].toLowerCase()].split('x')[1]) |
|||
} |
|||
} |
|||
|
|||
} else { |
|||
rendition[attr_value.split('=')[0].toLowerCase()] = Number(attr_value.split('=')[1]); |
|||
} |
|||
}); |
|||
|
|||
|
|||
if( self.getTagType(lines[index+1]) == tagTypes.BYTERANGE ) |
|||
{ |
|||
rendition.byterange = self.getTagValue(lines[index+1]).split('@'); |
|||
rendition.url = lines[index+2]; |
|||
} else |
|||
{ |
|||
rendition.url = lines[index+1]; |
|||
} |
|||
|
|||
data.isPlaylist = true; |
|||
data.playlistItems.push(rendition); |
|||
break; |
|||
|
|||
case tagTypes.TARGETDURATION: |
|||
data.targetDuration = Number(self.getTagValue(value).split(',')[0]); |
|||
break; |
|||
|
|||
case tagTypes.ZEN_TOTAL_DURATION: |
|||
data.totalDuration = self.getTagValue(value); |
|||
break; |
|||
|
|||
case tagTypes.VERSION: |
|||
data.version = Number(self.getTagValue(value)); |
|||
break; |
|||
|
|||
case tagTypes.MEDIA_SEQUENCE: |
|||
data.mediaSequence = parseInt(self.getTagValue(value)); |
|||
break; |
|||
|
|||
case tagTypes.ALLOW_CACHE: |
|||
if(self.getTagValue(value) == "YES" || self.getTagValue(value) == "NO") |
|||
{ |
|||
data.allowCache = self.getTagValue(value); |
|||
} else { |
|||
data.invalidReasons.push("Invalid ALLOW_CACHE Value"); |
|||
} |
|||
break; |
|||
|
|||
case tagTypes.ENDLIST: |
|||
data.hasEndTag = true; |
|||
break; |
|||
} |
|||
} |
|||
) |
|||
} else { |
|||
data.invalidReasons.push("Empty Manifest"); |
|||
} |
|||
|
|||
return data; |
|||
|
|||
}; |
|||
}; |
|||
|
|||
})(this); |
@ -0,0 +1,111 @@ |
|||
window.videojs.hls.m3u8TagType = { |
|||
/* |
|||
* Derived from V8: http://tools.ietf.org/html/draft-pantos-http-live-streaming-08
|
|||
*/ |
|||
|
|||
/** |
|||
* Identifies manifest as Extended M3U - must be present on first line! |
|||
*/ |
|||
EXTM3U:"#EXTM3U", |
|||
|
|||
/** |
|||
* Specifies duration. |
|||
* Syntax: #EXTINF:<duration>,<title> |
|||
* Example: #EXTINF:10, |
|||
*/ |
|||
EXTINF:"#EXTINF:", |
|||
|
|||
/** |
|||
* Indicates that a media segment is a sub-range of the resource identified by its media URI. |
|||
* Syntax: #EXT-X-BYTERANGE:<n>[@o] |
|||
*/ |
|||
BYTERANGE:"#EXT-X-BYTERANGE:", |
|||
|
|||
/** |
|||
* Specifies the maximum media segment duration - applies to entire manifest. |
|||
* Syntax: #EXT-X-TARGETDURATION:<s> |
|||
* Example: #EXT-X-TARGETDURATION:10 |
|||
*/ |
|||
TARGETDURATION:"#EXT-X-TARGETDURATION:", |
|||
|
|||
/** |
|||
* Specifies the sequence number of the first URI in a manifest. |
|||
* Syntax: #EXT-X-MEDIA-SEQUENCE:<i> |
|||
* Example: #EXT-X-MEDIA-SEQUENCE:50 |
|||
*/ |
|||
MEDIA_SEQUENCE:"#EXT-X-MEDIA-SEQUENCE:", |
|||
|
|||
/** |
|||
* Specifies a method by which media segments can be decrypted, if encryption is present. |
|||
* Syntax: #EXT-X-KEY:<attribute-list> |
|||
* Note: This is likely irrelevant in the context of the Flash Player. |
|||
*/ |
|||
KEY:"#EXT-X-KEY:", |
|||
|
|||
/** |
|||
* Associates the first sample of a media segment with an absolute date and/or time. Applies only to the next media URI. |
|||
* Syntax: #EXT-X-PROGRAM-DATE-TIME:<YYYY-MM-DDThh:mm:ssZ> |
|||
* Example: #EXT-X-PROGRAM-DATE-TIME:2010-02-19T14:54:23.031+08:00 |
|||
*/ |
|||
PROGRAM_DATE_TIME:"#EXT-X-PROGRAM-DATE-TIME:", |
|||
|
|||
/** |
|||
* Indicates whether the client MAY or MUST NOT cache downloaded media segments for later replay. |
|||
* Syntax: #EXT-X-ALLOW-CACHE:<YES|NO> |
|||
* Note: This is likely irrelevant in the context of the Flash Player. |
|||
*/ |
|||
ALLOW_CACHE:"#EXT-X-ALLOW_CACHE:", |
|||
|
|||
/** |
|||
* Provides mutability information about the manifest. |
|||
* Syntax: #EXT-X-PLAYLIST-TYPE:<EVENT|VOD> |
|||
*/ |
|||
PLAYLIST_TYPE:"#EXT-X-PLAYLIST-TYPE:", |
|||
|
|||
/** |
|||
* Indicates that no more media segments will be added to the manifest. May occur ONCE, anywhere in the mainfest file. |
|||
*/ |
|||
ENDLIST:"#EXT-X-ENDLIST", |
|||
|
|||
/** |
|||
* Used to relate Playlists that contain alternative renditions of the same content. |
|||
* Syntax: #EXT-X-MEDIA:<attribute-list> |
|||
*/ |
|||
MEDIA:"#EXT-X-MEDIA:", |
|||
|
|||
/** |
|||
* Identifies a media URI as a Playlist file containing a multimedia presentation and provides information about that presentation. |
|||
* Syntax: #EXT-X-STREAM-INF:<attribute-list> |
|||
* <URI> |
|||
*/ |
|||
STREAM_INF:"#EXT-X-STREAM-INF:", |
|||
|
|||
/** |
|||
* Indicates an encoding discontinuity between the media segment that follows it and the one that preceded it. |
|||
*/ |
|||
DISCONTINUITY:"#EXT-X-DISCONTINUITY", |
|||
|
|||
/** |
|||
* Indicates that each media segment in the manifest describes a single I-frame. |
|||
*/ |
|||
I_FRAMES_ONLY:"#EXT-X-I-FRAMES-ONLY", |
|||
|
|||
/** |
|||
* Identifies a manifest file containing the I-frames of a multimedia presentation. It stands alone, in that it does not apply to a particular URI in the manifest. |
|||
* Syntax: #EXT-X-I-FRAME-STREAM-INF:<attribute-list> |
|||
*/ |
|||
I_FRAME_STREAM_INF:"#EXT-X-I-FRAME-STREAM-INF:", |
|||
|
|||
/** |
|||
* Indicates the compatibility version of the Playlist file. |
|||
* Syntax: #EXT-X-VERSION:<n> |
|||
*/ |
|||
VERSION:"#EXT-X-VERSION:", |
|||
|
|||
/** |
|||
* Indicates the total duration as reported by Zencoder. |
|||
* Syntax: #ZEN-TOTAL-DURATION:<n> |
|||
*/ |
|||
ZEN_TOTAL_DURATION: "#ZEN-TOTAL-DURATION:" |
|||
|
|||
}; |
@ -0,0 +1,17 @@ |
|||
(function(window) { |
|||
window.videojs.hls.M3U8 = function() { |
|||
this.allowCache = "NO"; |
|||
this.playlistItems = []; |
|||
this.mediaItems = []; |
|||
this.iFrameItems = []; |
|||
this.invalidReasons = []; |
|||
this.hasValidM3UTag = false; |
|||
this.hasEndTag = false; |
|||
this.targetDuration = -1; |
|||
this.totalDuration = -1; |
|||
this.isPlaylist = false; |
|||
this.playlistType = ""; |
|||
this.mediaSequence = -1; |
|||
this.version = -1; |
|||
} |
|||
})(this); |
@ -0,0 +1,51 @@ |
|||
(function(window) { |
|||
var M3U8 = window.videojs.hls.M3U8; |
|||
var M3U8Parser = window.videojs.hls.M3U8Parser; |
|||
|
|||
window.videojs.hls.ManifestController = function(){ |
|||
|
|||
var self = this; |
|||
var parser; |
|||
var data; |
|||
|
|||
var onDataCallback; |
|||
var onErrorCallback; |
|||
var onUpdateCallback; |
|||
|
|||
self.loadManifest = function ( manifestUrl, onDataCallback, onErrorCallback, onUpdateCallback ) { |
|||
self.onDataCallback = onDataCallback; |
|||
self.onErrorCallback = onErrorCallback; |
|||
self.onUpdateCallback = onUpdateCallback; |
|||
|
|||
vjs.get(manifestUrl, self.onManifestLoadComplete, self.onManifestLoadError); |
|||
}; |
|||
|
|||
self.parseManifest = function ( dataAsString ) { |
|||
self.parser = new M3U8Parser(); |
|||
self.data = self.parser.parse( dataAsString ); |
|||
|
|||
return self.data; |
|||
}; |
|||
|
|||
self.onManifestLoadComplete = function(response) { |
|||
var output = self.parseManifest(response); |
|||
|
|||
if(self.onDataCallback != undefined) |
|||
{ |
|||
self.onDataCallback(output); |
|||
} |
|||
}; |
|||
|
|||
self.onManifestLoadError = function(err) { |
|||
if(err) |
|||
{ |
|||
console.log(err.message); |
|||
} |
|||
|
|||
if(self.onErrorCallback != undefined) |
|||
{ |
|||
onErrorCallback((err != undefined) ? err : null); |
|||
} |
|||
}; |
|||
} |
|||
})(this); |
@ -0,0 +1,70 @@ |
|||
(function(window) { |
|||
|
|||
var SegmentParser = window.videojs.hls.SegmentParser; |
|||
|
|||
window.videojs.hls.SegmentController = function(){ |
|||
|
|||
var self = this; |
|||
var url; |
|||
var parser; |
|||
var requestTimestamp; |
|||
var responseTimestamp; |
|||
var data; |
|||
|
|||
var onDataCallback; |
|||
var onErrorCallback; |
|||
var onUpdateCallback; |
|||
|
|||
self.loadSegment = function ( segmentUrl, onDataCallback, onErrorCallback, onUpdateCallback ) { |
|||
self.url = segmentUrl; |
|||
self.onDataCallback = onDataCallback; |
|||
self.onErrorCallback = onErrorCallback; |
|||
self.onUpdateCallback = onUpdateCallback; |
|||
self.requestTimestamp = new Date().getTime(); |
|||
|
|||
vjs.get(segmentUrl, self.onSegmentLoadComplete, self.onSegmentLoadError); |
|||
}; |
|||
|
|||
self.parseSegment = function ( incomingData ) { |
|||
// Add David's code later //
|
|||
|
|||
self.data = {}; |
|||
self.data.url = self.url; |
|||
self.data.isCached = false; |
|||
self.data.requestTimestamp = self.requestTimestamp; |
|||
self.data.responseTimestamp = self.responseTimestamp; |
|||
self.data.byteLength = incomingData.byteLength; |
|||
self.data.isCached = ( parseInt(self.responseTimestamp - self.requestTimestamp) < 75 ); |
|||
self.data.throughput = self.calculateThroughput(self.data.byteLength, self.requestTimestamp ,self.responseTimestamp) |
|||
|
|||
return self.data; |
|||
}; |
|||
|
|||
self.calculateThroughput = function(dataAmount, startTime, endTime) { |
|||
return Math.round(dataAmount/(endTime-startTime)*1000)*8; |
|||
} |
|||
|
|||
self.onSegmentLoadComplete = function(response) { |
|||
self.responseTimestamp = new Date().getTime(); |
|||
|
|||
var output = self.parseSegment(response); |
|||
|
|||
if(self.onDataCallback != undefined) |
|||
{ |
|||
self.onDataCallback(output); |
|||
} |
|||
}; |
|||
|
|||
self.onSegmentLoadError = function(err) { |
|||
if(err) |
|||
{ |
|||
console.log(err.message); |
|||
} |
|||
|
|||
if(self.onErrorCallback != undefined) |
|||
{ |
|||
onErrorCallback((err != undefined) ? err : null); |
|||
} |
|||
}; |
|||
} |
|||
})(this); |
@ -0,0 +1,9 @@ |
|||
window.brightcove_playlist_data = '#EXTM3U\n'+ |
|||
'#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=240000,RESOLUTION=396x224\n'+ |
|||
'http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686811001&videoId=1824650741001\n'+ |
|||
'#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=40000\n'+ |
|||
'http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824683759001&videoId=1824650741001\n'+ |
|||
'#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=440000,RESOLUTION=396x224\n'+ |
|||
'http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824686593001&videoId=1824650741001\n'+ |
|||
'#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1928000,RESOLUTION=960x540\n'+ |
|||
'http://c.brightcove.com/services/mobile/streaming/index/rendition.m3u8?assetId=1824687660001&videoId=1824650741001' |
@ -0,0 +1,57 @@ |
|||
#EXTM3U |
|||
#EXT-X-TARGETDURATION:10 |
|||
#EXT-X-VERSION:4 |
|||
#EXT-X-MEDIA-SEQUENCE:0 |
|||
#EXT-X-PLAYLIST-TYPE:VOD |
|||
#EXTINF:10, |
|||
#EXT-X-BYTERANGE:522828@0 |
|||
hls_450k_video.ts |
|||
#EXTINF:10, |
|||
#EXT-X-BYTERANGE:587500@522828 |
|||
hls_450k_video.ts |
|||
#EXTINF:10, |
|||
#EXT-X-BYTERANGE:713084@1110328 |
|||
hls_450k_video.ts |
|||
#EXTINF:10, |
|||
#EXT-X-BYTERANGE:476580@1823412 |
|||
hls_450k_video.ts |
|||
#EXTINF:10, |
|||
#EXT-X-BYTERANGE:535612@2299992 |
|||
hls_450k_video.ts |
|||
#EXTINF:10, |
|||
#EXT-X-BYTERANGE:207176@2835604 |
|||
hls_450k_video.ts |
|||
#EXTINF:10, |
|||
#EXT-X-BYTERANGE:455900@3042780 |
|||
hls_450k_video.ts |
|||
#EXTINF:10, |
|||
#EXT-X-BYTERANGE:657248@3498680 |
|||
hls_450k_video.ts |
|||
#EXTINF:10, |
|||
#EXT-X-BYTERANGE:571708@4155928 |
|||
hls_450k_video.ts |
|||
#EXTINF:10, |
|||
#EXT-X-BYTERANGE:485040@4727636 |
|||
hls_450k_video.ts |
|||
#EXTINF:10, |
|||
#EXT-X-BYTERANGE:709136@5212676 |
|||
hls_450k_video.ts |
|||
#EXTINF:10, |
|||
#EXT-X-BYTERANGE:730004@5921812 |
|||
hls_450k_video.ts |
|||
#EXTINF:10, |
|||
#EXT-X-BYTERANGE:456276@6651816 |
|||
hls_450k_video.ts |
|||
#EXTINF:10, |
|||
#EXT-X-BYTERANGE:468684@7108092 |
|||
hls_450k_video.ts |
|||
#EXTINF:10, |
|||
#EXT-X-BYTERANGE:444996@7576776 |
|||
hls_450k_video.ts |
|||
#EXTINF:10, |
|||
#EXT-X-BYTERANGE:331444@8021772 |
|||
hls_450k_video.ts |
|||
#EXTINF:1.4167, |
|||
#EXT-X-BYTERANGE:44556@8353216 |
|||
hls_450k_video.ts |
|||
#EXT-X-ENDLIST |
@ -0,0 +1,57 @@ |
|||
window.playlistData = '#EXTM3U\n'+ |
|||
'#EXT-X-TARGETDURATION:10\n' + |
|||
'#EXT-X-VERSION:4\n' + |
|||
'#EXT-X-MEDIA-SEQUENCE:0\n' + |
|||
'#EXT-X-PLAYLIST-TYPE:VOD\n' + |
|||
'#EXTINF:10,\n' + |
|||
'#EXT-X-BYTERANGE:522828@0\n' + |
|||
'hls_450k_video.ts\n' + |
|||
'#EXTINF:10,\n' + |
|||
'#EXT-X-BYTERANGE:587500@522828\n' + |
|||
'hls_450k_video.ts\n' + |
|||
'#EXTINF:10,\n' + |
|||
'#EXT-X-BYTERANGE:713084@1110328\n' + |
|||
'hls_450k_video.ts\n' + |
|||
'#EXTINF:10,\n' + |
|||
'#EXT-X-BYTERANGE:476580@1823412\n' + |
|||
'hls_450k_video.ts\n' + |
|||
'#EXTINF:10,\n' + |
|||
'#EXT-X-BYTERANGE:535612@2299992\n' + |
|||
'hls_450k_video.ts\n' + |
|||
'#EXTINF:10,\n' + |
|||
'#EXT-X-BYTERANGE:207176@2835604\n' + |
|||
'hls_450k_video.ts\n' + |
|||
'#EXTINF:10,\n' + |
|||
'#EXT-X-BYTERANGE:455900@3042780\n' + |
|||
'hls_450k_video.ts\n' + |
|||
'#EXTINF:10,\n' + |
|||
'#EXT-X-BYTERANGE:657248@3498680\n' + |
|||
'hls_450k_video.ts\n' + |
|||
'#EXTINF:10,\n' + |
|||
'#EXT-X-BYTERANGE:571708@4155928\n' + |
|||
'hls_450k_video.ts\n' + |
|||
'#EXTINF:10,\n' + |
|||
'#EXT-X-BYTERANGE:485040@4727636\n' + |
|||
'hls_450k_video.ts\n' + |
|||
'#EXTINF:10,\n' + |
|||
'#EXT-X-BYTERANGE:709136@5212676\n' + |
|||
'hls_450k_video.ts\n' + |
|||
'#EXTINF:10,\n' + |
|||
'#EXT-X-BYTERANGE:730004@5921812\n' + |
|||
'hls_450k_video.ts\n' + |
|||
'#EXTINF:10,\n' + |
|||
'#EXT-X-BYTERANGE:456276@6651816\n' + |
|||
'hls_450k_video.ts\n' + |
|||
'#EXTINF:10,\n' + |
|||
'#EXT-X-BYTERANGE:468684@7108092\n' + |
|||
'hls_450k_video.ts' + |
|||
'#EXTINF:10,\n' + |
|||
'#EXT-X-BYTERANGE:444996@7576776\n' + |
|||
'hls_450k_video.ts\n' + |
|||
'#EXTINF:10,\n' + |
|||
'#EXT-X-BYTERANGE:331444@8021772\n' + |
|||
'hls_450k_video.ts\n' + |
|||
'#EXTINF:1.4167,\n' + |
|||
'#EXT-X-BYTERANGE:44556@8353216\n' + |
|||
'hls_450k_video.ts\n' + |
|||
'#EXT-X-ENDLIST'; |
@ -0,0 +1,9 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<module type="WEB_MODULE" version="4"> |
|||
<component name="NewModuleRootManager" inherit-compiler-output="true"> |
|||
<exclude-output /> |
|||
<content url="file://$MODULE_DIR$" /> |
|||
<orderEntry type="sourceFolder" forTests="false" /> |
|||
</component> |
|||
</module> |
|||
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue