Skip to content

Instantly share code, notes, and snippets.

@jasonbyrne
Last active October 19, 2018 06:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jasonbyrne/822b0842b4229eb99cda62b8486a3812 to your computer and use it in GitHub Desktop.
Save jasonbyrne/822b0842b4229eb99cda62b8486a3812 to your computer and use it in GitHub Desktop.
Generate an iframe only manifest from a video file with ffprobe and Node
// 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";
}
}
@jasonbyrne
Copy link
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment