Generate an iframe only manifest from a video file with ffprobe and Node
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Work in progress to convert this to TypeScript/Node: | |
// https://gist.github.com/biomancer/8d139177f520b9dd3495 | |
import { exec } from "child_process"; | |
/** | |
* | |
*/ | |
export class IframeByteRange { | |
public packetTime: number; | |
public packetPosition: number; | |
public packetSize: number; | |
public duration: number; | |
constructor(time: number, pos: number, size: number) { | |
this.packetTime = time; | |
this.packetPosition = pos; | |
this.packetSize = size; | |
this.duration = 0; | |
} | |
} | |
/** | |
* | |
*/ | |
export class IframeOnlyManifestGenerator { | |
protected inputPath: string; | |
protected fileName: string; | |
protected iframes: IframeByteRange[] = [] | |
protected totalDuration: number = 0; | |
protected totalSize: number = 0; | |
protected callback: Function = function () { }; | |
// https://gist.github.com/biomancer/8d139177f520b9dd3495 | |
//protected cmd: string = 'ffprobe -show_frames -select_streams v -of compact -show_entries packet=pts_time,codec_type,pos:frame=pict_type,pkt_pts_time,pkt_size,pkt_pos -i '; | |
// PBS used this: | |
// ffprobe -print_format json -show_packets -show_frames | |
protected cmd: string = 'ffprobe -show_frames -select_streams v -of compact -i '; | |
public constructor(inputPath: string) { | |
this.inputPath = inputPath; | |
this.fileName = inputPath.split('/').pop() || ""; | |
} | |
/** | |
* Translate the output into the byte ranges | |
* | |
* Example line: | |
* frame|pkt_pts_time=1.483333|pkt_pos=564|pkt_size=10655|pict_type=I | |
* | |
* @param lines | |
*/ | |
protected parseProbeOutput(lines: string[]) { | |
let me = this; | |
this.totalDuration = 0; | |
this.totalSize = 0; | |
lines.forEach(function (line: string, index: number) { | |
if (line.length) { | |
// Turn this into JSON | |
let json: any = {}; | |
let fields: string[] = line.split('|'); | |
fields.forEach(function (field: string) { | |
let arr: string[] = field.split('='); | |
json[arr[0]] = arr[1]; | |
}); | |
/** | |
* Output looks like this: | |
* { frame: undefined, | |
media_type: 'video', | |
stream_index: '0', | |
key_frame: '1', | |
pkt_pts: '13258500', | |
pkt_pts_time: '147.316667', | |
pkt_dts: '13258500', | |
pkt_dts_time: '147.316667', | |
best_effort_timestamp: '13258500', | |
best_effort_timestamp_time: '147.316667', | |
pkt_duration: '3750', | |
pkt_duration_time: '0.041667', | |
pkt_pos: '7831704', | |
pkt_size: '27179', | |
width: '360', | |
height: '240', | |
pix_fmt: 'yuv420p', | |
sample_aspect_ratio: '1:1', | |
pict_type: 'I', | |
coded_picture_number: '3500', | |
display_picture_number: '0', | |
interlaced_frame: '0', | |
top_field_first: '0', | |
repeat_pict: '0', | |
color_range: 'unknown', | |
color_space: 'unknown', | |
color_primaries: 'unknown', | |
color_transfer: 'unknown', | |
chroma_location: 'left' } | |
*/ | |
// Now add it ot the array | |
let thisIframe: IframeByteRange = new IframeByteRange( | |
json.pkt_pts_time, | |
json.pkt_pos, | |
// According to serveral sources the result of ffprobe is 188 bytes less | |
// than that off what Apple recommends, so 188 bytes is somewhat abitrarily added | |
// https://open.pbs.org/how-pbs-is-enabling-apples-trick-play-mode-85635372a5db | |
// https://github.com/pbs/iframe-playlist-generator/blob/master/iframeplaylistgenerator/generator.py | |
// https://gist.github.com/biomancer/8d139177f520b9dd3495 | |
(json.pkt_size + 188) | |
); | |
let lastIFrame: IframeByteRange = me.iframes[index - 1]; | |
me.iframes.push(thisIframe); | |
// Update duration of last packet | |
if (lastIFrame) { | |
lastIFrame.duration = parseFloat((thisIframe.packetTime - lastIFrame.packetTime).toFixed(5)); | |
// Temporarily set this current frame to the same as previous (for last frame) | |
thisIframe.duration = lastIFrame.duration; | |
} | |
} | |
}); | |
// Loop through the result, because some of the things like duration change along the way | |
this.iframes.forEach(function (iframe: IframeByteRange) { | |
// Add up totals | |
me.totalDuration += iframe.duration; | |
me.totalSize += iframe.packetSize; | |
}); | |
this.callback(this.iframes, this.toManfiest()); | |
} | |
/** | |
* Create the m3u8 text | |
*/ | |
protected toManfiest(): string { | |
let me = this; | |
let out: string = "#EXTM3U\n" + | |
"#EXT-X-VERSION:4\n" + | |
"#EXT-X-MEDIA-SEQUENCE: 0\n" + | |
"#EXT-X-TARGETDURATION:10\n" + | |
"#EXT-X-PLAYLIST-TYPE: VOD\n" + | |
"#EXT-X-I-FRAMES-ONLY\n"; | |
this.iframes.forEach(function (iframe: IframeByteRange) { | |
out += "#EXTINF:" + iframe.duration + "\n" + | |
"#EXT-X-BYTERANGE:" + iframe.packetSize + "@" + iframe.packetPosition + "\n" + | |
me.fileName + "\n"; | |
}); | |
out += "#EXT-X-ENDLIST\n"; | |
return out; | |
} | |
/** | |
* Start processing the specified input | |
* | |
* @param callback | |
*/ | |
public start(callback: Function) { | |
this.callback = callback; | |
let me = this; | |
let cmd: string = this.cmd + this.inputPath + ' | grep pict_type=I'; | |
console.log("Running: " + cmd); | |
exec(cmd, { maxBuffer: 1024 * 50000 }, (err, stdout, stderr) => { | |
if (err) { | |
// node couldn't execute the command | |
console.log(err); | |
return; | |
} | |
me.parseProbeOutput(stdout.split("\n")); | |
}); | |
} | |
/** | |
* Get the bandwidth of the resulting iframe only playlist | |
*/ | |
protected getBandwidthInBits(): number { | |
return (this.totalDuration > 0 ? (this.totalSize / this.totalDuration) : 0) * 8; | |
} | |
/** | |
* Line to append to the master playlist for this i-frame rendition | |
*/ | |
public getMasterPlayListLine(iframeOnlyManifestFileName: String): string { | |
return '#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=' + this.getBandwidthInBits() + | |
',CODECS="avc1.4d001f",URI="' + iframeOnlyManifestFileName + '"' + "\n"; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I have not tested this yet, just threw it together for now. Will need some more work.
Only found Ruby:
https://gist.github.com/biomancer/8d139177f520b9dd3495
And Python:
https://github.com/pbs/iframe-playlist-generator
So seeing what I can learn from those projects and port it to Node.