Skip to content

Instantly share code, notes, and snippets.

@rmarscher
Created August 1, 2024 04:54
Show Gist options
  • Save rmarscher/03de535826a180e13ea34dbf38729e2e to your computer and use it in GitHub Desktop.
Save rmarscher/03de535826a180e13ea34dbf38729e2e to your computer and use it in GitHub Desktop.
Functions for building a webvtt preview thumbnail file generated by AWS MediaConvert - compatible with media chrome, plyr and other players
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
html,
body {
margin: 0;
padding: 0;
}
media-theme-yt {
display: block; /* expands the container if preload=none */
aspect-ratio: 16 / 9; /* set container aspect ratio if preload=none */
}
hls-video {
width: 100%; /* prevents video to expand beyond its container */
background-color: #000;
}
</style>
<script
type="module"
src="https://cdn.jsdelivr.net/npm/hls-video-element/+esm"
></script>
<script
type="module"
src="https://cdn.jsdelivr.net/npm/player.style/yt/+esm"
></script>
</head>
<body>
<main>
<!-- https://player.style is cool! -->
<media-theme-yt>
<hls-video
slot="media"
src="https://my.cloudfront.site/path/to/my/hls/files/video-filename.m3u8"
crossorigin
playsinline
>
<track
label="thumbnails"
default
kind="metadata"
src="https://my.cloudfront.site/path/to/my/hls/files/storyboard.vtt"
/>
</hls-video>
</media-theme-yt>
</main>
</body>
</html>
{
"Name": "hls-abr-160kbps-h264-aac",
"Settings": {
"TimecodeConfig": {
"Source": "ZEROBASED"
},
"OutputGroups": [
{
"Name": "File Group",
"Outputs": [
{
"ContainerSettings": {
"Container": "RAW"
},
"VideoDescription": {
"CodecSettings": {
"Codec": "FRAME_CAPTURE",
"FrameCaptureSettings": {
"FramerateNumerator": 1,
"FramerateDenominator": 5,
"MaxCaptures": 3
}
}
},
"Extension": "jpg",
"NameModifier": "-poster"
}
],
"OutputGroupSettings": {
"Type": "FILE_GROUP_SETTINGS",
"FileGroupSettings": {
"Destination": "REPLACE_WITH_POSTER_S3_FOLDER_PATH"
}
}
},
{
"Name": "Apple HLS",
"Outputs": [
{
"ContainerSettings": {
"Container": "M3U8",
"M3u8Settings": {}
},
"VideoDescription": {
"CodecSettings": {
"Codec": "H_264",
"H264Settings": {
"FramerateControl": "INITIALIZE_FROM_SOURCE",
"RateControlMode": "QVBR",
"SceneChangeDetect": "TRANSITION_DETECTION",
"QualityTuningLevel": "MULTI_PASS_HQ"
}
}
},
"AudioDescriptions": [
{
"CodecSettings": {
"Codec": "AAC",
"AacSettings": {
"Bitrate": 160000,
"CodingMode": "CODING_MODE_2_0",
"SampleRate": 48000
}
}
}
],
"OutputSettings": {
"HlsSettings": {}
}
}
],
"OutputGroupSettings": {
"Type": "HLS_GROUP_SETTINGS",
"HlsGroupSettings": {
"SegmentLength": 6,
"Destination": "REPLACE_WITH_HLS_OUTPUT_S3_FOLDER_PATH",
"MinSegmentLength": 0,
"ImageBasedTrickPlay": "ADVANCED",
"ImageBasedTrickPlaySettings": {
"ThumbnailWidth": 280,
"TileHeight": 25,
"TileWidth": 14,
"IntervalCadence": "FOLLOW_CUSTOM",
"ThumbnailInterval": 18
}
}
},
"AutomatedEncodingSettings": {
"AbrSettings": {
"MaxAbrBitrate": 8000000,
"MinAbrBitrate": 360000
}
}
}
],
"Inputs": [
{
"AudioSelectors": {
"Audio Selector 1": {
"DefaultSelection": "DEFAULT"
}
},
"VideoSelector": {},
"TimecodeSource": "ZEROBASED",
"FileInput": "REPLACE_WITH_INPUT_S3_PATH"
}
]
},
"AccelerationSettings": {
"Mode": "PREFERRED"
},
"StatusUpdateInterval": "SECONDS_15",
"Priority": 0,
"HopDestinations": []
}
// use Bun or ts-node to run without transpiling
main();
async function main() {
console.log(
generateVttTrickplay(
"https://my.cloudfront.site/path/to/my/hls/files/",
376,
280,
158,
),
);
}
export function generateVttTrickplay(
outputPath: string,
duration: number,
width = 284,
height = 160,
interval = 18,
tileX = 14,
tileY = 25,
): string {
let vtt = "WEBVTT\n\n";
const segments = Math.ceil(duration / interval);
let i = 0;
let startTime = 0;
while (i < segments) {
const x = i % tileX;
const y = Math.floor(i / tileX);
const page = Math.floor(y / tileY) + 1;
const filename = `Thumbnail_000000${page.toString().padStart(3, "0")}.jpg`;
const imageUrl = `${outputPath}${filename}`;
startTime = i * interval;
const endTime = startTime + interval;
vtt += `${secondsToTimestamp(startTime)} --> ${secondsToTimestamp(endTime)}\n`;
vtt += `${imageUrl}#xywh=${x * width},${y * height},${width},${height}\n\n`;
i += 1;
}
return vtt;
}
export function secondsToTimestamp(duration: number): string {
const seconds = Math.round(duration * 1000) / 1000;
const milliseconds = Math.floor((seconds - Math.floor(seconds)) * 1000);
const sec = Math.floor(seconds) % 60;
const minutes = Math.floor(duration / 60) % 60 || 0;
const hours = Math.floor(duration / 3600) % 60 || 0;
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${sec.toString().padStart(2, "0")}.${milliseconds.toString().padStart(3, "0")}`;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment