From 37b01102bb76ca1e86054f88cac1c0ac8b7cd97c Mon Sep 17 00:00:00 2001 From: Gary Katsevman Date: Tue, 10 Dec 2013 15:48:37 -0500 Subject: [PATCH] Add m3u8.pegjs. Generate the parser file via 'npm run peg'. There is a simple test script in test/pegtest.js. The test can be run via 'npm run testpeg' which will generate a new copy of the parser and then run the test file. --- package.json | 8 +- src/m3u8/m3u8.pegjs | 269 ++++++++++++++++++++++++++++++++++++++++++++ test/pegtest.js | 6 + 3 files changed, 281 insertions(+), 2 deletions(-) create mode 100644 src/m3u8/m3u8.pegjs create mode 100644 test/pegtest.js diff --git a/package.json b/package.json index 6610c3f4..0ca7de2b 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,10 @@ }, "license": "Apache 2", "scripts": { - "test": "grunt qunit" + "test": "grunt qunit", + "prepublish": "npm run peg", + "peg": "pegjs src/m3u8/m3u8.pegjs src/m3u8/m3u8-generated.js", + "testpeg": "npm run peg && node test/pegtest.js" }, "devDependencies": { "grunt-contrib-jshint": "~0.6.0", @@ -15,7 +18,8 @@ "grunt-contrib-uglify": "~0.2.0", "grunt-contrib-watch": "~0.4.0", "grunt-contrib-clean": "~0.4.0", - "grunt": "~0.4.1" + "grunt": "~0.4.1", + "pegjs": "~0.7.0" }, "dependencies": { "video.js": "~4.2.2", diff --git a/src/m3u8/m3u8.pegjs b/src/m3u8/m3u8.pegjs new file mode 100644 index 00000000..948437a6 --- /dev/null +++ b/src/m3u8/m3u8.pegjs @@ -0,0 +1,269 @@ +/***** Start *****/ +start + = tags:tags+ .* { + var obj = {}; + tags.forEach(function(tag) { for (var p in tag) { obj[p] = tag[p]; }}); + return obj; + } + +tags + = tag:m3uTag _ { return tag; } + / tag:extinfTag _ { return tag; } + / tag:targetDurationTag _ { return tag; } + / tag:mediaSequenceTag _ { return tag; } + / tag:keyTag _ { return tag; } + / tag:programDateTimeTag _ { return tag; } + / tag:allowCacheTag _ { return tag; } + / tag:playlistTypeTag _ { return tag; } + / tag:endlistTag _ { return tag; } + / tag:mediaTag _ { return tag; } + / tag:streamInfTag _ { return tag; } + / tag:discontinuityTag _ { return tag; } + / tag:discontinuitySequenceTag _ { return tag; } + / tag:iframesOnlyTag _ { return tag; } + / tag:mapTag _ { return tag; } + / tag:iframeStreamInf _ { return tag; } + / tag:startTag _ { return tag; } + / tag:versionTag _ { return tag; } + +/***** Tags *****/ + +m3uTag + = tag:"#EXTM3U" { return {openTag: true}; } + +extinfTag + = tag:'#EXTINF' ":" duration:number "," _ title:text? _ byteRange:byteRangeTag? file:mediaFile { + var fileObj = {}; + fileObj[file] = { + byteRange: byteRange, + title: title, + duration: duration, + file: file + }; + return fileObj; + } + +byteRangeTag + = tag:"EXT-X-BYTERANGE" ":" length:int ("@" offset:int)? { return {length: length, offset: offset}; } + +targetDurationTag + = tag:"#EXT-X-TARGETDURATION" ":" seconds:int { return {targetDuration: seconds}; } + +mediaSequenceTag + = tag:'#EXT-X-MEDIA-SEQUENCE' ":" sequenceNumber:int { return {mediaSequence: sequenceNumber}; } + +keyTag + = tag:'#EXT-X-KEY' ":" attrs:keyAttributes { return {key: attrs}; } + +programDateTimeTag + = tag:'#EXT-X-PROGRAM-DATE-TIME' ":" date:date + +allowCacheTag + = tag:'#EXT-X-ALLOW-CACHE' ":" answer:answer { return {allowCache: answer}; } + +playlistTypeTag + = tag:'#EXT-X-PLAYLIST-TYPE' ":" type:playlistType { return {playlistType: type}; } + +endlistTag + = tag:'#EXT-X-ENDLIST' { return {closeTag: true}; } + +mediaTag + = tag:'#EXT-MEDIA' ":" attrs:mediaAttributes { return {media: attrs}; } + +streamInfTag + = tag:'#EXT-X-STREAM-INF' ":" attrs:streamInfAttrs _ file:mediaFile { + var fileObj = {}; + fileObj[file] = { + attributes: attrs, + file: file + }; + return fileObj; + } + +discontinuityTag + = tag:'#EXT-X-DISCONTINUITY' + +discontinuitySequenceTag + = tag:'#EXT-X-DISCONTINUITY-SEQUENCE' ":" sequence:int { return {discontinuitySequence: sequence}; } + +iframesOnlyTag + = tag:'#EXT-X-I-FRAMES-ONLY' + +mapTag + = tag:'#EXT-X-MAP' ":" attrs:mapAttributes { return {map: attrs}; } + +iframeStreamInf + = tag:'#EXT-X-I-FRAME-STREAM-INF' ":" attrs:iframeStreamAttrs { return {iframeStream: attrs}; } + +startTag + = tag:'EXT-X-START' ":" attrs:startAttributes { return {start: attrs}; } + +versionTag + = tag:'#EXT-X-VERSION' ":" version:int { return {version: version}; } + +/***** Helpers *****/ + +mediaFile + = file:[ -~]+ { return file.join(''); } + +keyAttributes + = attrs:keyAttribute+ + +keyAttribute + = "METHOD" "=" method:keyMethod + / "URI" "=" uri:quotedString + / "IV" "=" iv:hexint + / "KEYFORMAT" "=" keyFormat:quotedString + / "KEYFORMATVERSIONS" "=" keyFormatVersions:quotedString + +keyMethod + = "NONE" + / "AES-128" + / "SAMPLE-AES" + +mediaAttributes + = attrs:mediaAttribute+ + +mediaAttribute + = "TYPE" "=" type:enumeratedString + / "URI" "=" uri:quotedString + / "GROUP-ID" "=" groupId:quotedString + / "LANGUAGE" "=" langauge:quotedString + / "ASSOC-LANGUAGE" "=" assocLanguage:quotedString + / "NAME" "=" name:quotedString + / "DEFAULT" "=" default:answer + / "AUTOSELECT" "="autoselect:answer + / "FORCE" "=" force:answer + / "INSTREAM-ID" "=" instreamId:quotedString + / "CHARACTERISTICS" "=" characteristics:quotedString + +streamInfAttrs + = attrs:streamInfAttr+ + +streamInfAttr + = streamInfSharedAttr + / "AUDIO" "=" audio:quotedString + / "SUBTITLES" "=" subtitles:quotedString + / "CLOSED-CAPTION" "=" captions:"NONE" + / "CLOSED-CAPTION" "=" captions:quotedString + +streamInfSharedAttr + = "BANDWIDTH" "=" bandwidth:int + / "CODECS" "=" codec:quotedString + / "RESOLUTION" "=" resolution:resolution + / "VIDEO" "=" video:quotedString + +mapAttributes + = attrs:mapAttribute+ + +mapAttribute + = "URI" "=" uri:quotedString + / "BYTERANGE" "=" byteRange:quotedString + +iframeStreamAttrs + = iframeStreamAttr+ + +iframeStreamAttr + = streamInfSharedAttr + / "URI" "=" uri:quotedString + +startAttributes + = attrs:startAttribute+ + +startAttribute + = "TIME-OFFSET" "=" timeOffset:number + / "PRECISE" "=" precise:answer + +answer "answer" + = "YES" + / "NO" + +playlistType + = "EVENT" + / "VOD" + +/***** Date *****/ + +date "date" + = year:year "-" month:month "-" day:day "T" time:time timezone:timezone + +year "year" + = digit digit digit digit + +month "month" + = [01] digit + +day "day" + = [0-3] digit + +time "time" + = [0-2] digit ":" [0-5] digit ":" [0-5] digit "." digit+ + / [0-2] digit ":" [0-5] digit ":" [0-5] digit + / [0-2] digit ":" [0-5] digit + +timezone "timezone" + = [+-] [0-2] digit ":" [0-5] digit + / "Z" + +/***** Numbers *****/ + +number "number" + = parts:(int frac) _ { return parseFloat(parts.join('')); } + / parts:(int) _ { return parts; } + +resolution + = int "x" int + +int + = first:digit19 rest:digits { return parseInt(first + rest.join(''), 10); } + / digit:digit { return parseInt(digit, 10); } + / neg:"-" first:digit19 rest:digits { return parseInt(neg + first + rest.join(''), 10); } + / neg:"-" digit { return parseInt(neg + digit, 10); } + +hexint + = "0x" hexDigits:hexDigit+ { return '0x' + hexDigits.join(''); } + / "0X" hexDigits:hexDigit+ { return '0x' + hexDigits.join(''); } + +frac + = dec:"." digits:digits { return parseFloat(dec + digits.join('')); } + +digits + = digit+ + +digit + = [0-9] + +digit19 + = [1-9] + +hexDigit + = [0-9a-fA-F] + +/***** Text *****/ + +enumeratedString + = chars:enumeratedChar+ _ { return chars.join('') } + +quotedString + = '"' '"' _ { return ""; } + / '"' chars:quotedChar+ '"' _ { return chars.join(''); } + +enumeratedChar + = [^'" \n\t\r] + / [a-zA-Z0-9] + +quotedChar + = [^\r\n"] + / char:char + +text "text" + = text:char+ { return text.join(''); } + +char "char" + = [ -~] + +_ "whitespace" + = whitespace* + +whitespace + = [ \t\n\r] diff --git a/test/pegtest.js b/test/pegtest.js new file mode 100644 index 00000000..8aff8c79 --- /dev/null +++ b/test/pegtest.js @@ -0,0 +1,6 @@ +var fs = require('fs'); +var path = require('path'); +var manifest = fs.readFileSync(__dirname + '/fixtures/prog_index.m3u8').toString(); +var parser = require('../src/m3u8/m3u8-generated.js'); +var parsed = parser.parse(manifest); +console.log(parsed);