@@ -31,6 +31,7 @@ Options:
3131 --prefix PREFIX Public alias prefix. Defaults to REND_PUBLIC_PLAYBACK_ALIAS_PREFIX or v.
3232 --acl public-read|inherit Alias object ACL. Defaults to REND_PUBLIC_PLAYBACK_ALIAS_ACL or public-read.
3333 --public-base-url URL Optional unauthenticated GET verification base URL.
34+ --backfill-progressive Create hls/<rendition>/progressive.mp4 source objects before alias copy.
3435 --env-file PATH Load storage credentials from a specific env file.
3536 --dry-run List counts without writing aliases.
3637` ) ;
@@ -44,6 +45,7 @@ function parseArgs(argv) {
4445 prefix : process . env . REND_PUBLIC_PLAYBACK_ALIAS_PREFIX || "v" ,
4546 acl : process . env . REND_PUBLIC_PLAYBACK_ALIAS_ACL || "public-read" ,
4647 publicBaseUrl : "" ,
48+ backfillProgressive : false ,
4749 envFile : "" ,
4850 dryRun : false ,
4951 } ;
@@ -84,6 +86,10 @@ function parseArgs(argv) {
8486 index += 1 ;
8587 continue ;
8688 }
89+ if ( arg === "--backfill-progressive" ) {
90+ options . backfillProgressive = true ;
91+ continue ;
92+ }
8793 if ( arg === "--env-file" ) {
8894 options . envFile = String ( next || "" ) ;
8995 index += 1 ;
@@ -438,6 +444,7 @@ function playbackArtifactKeysForAsset(allKeys, assetId) {
438444 relative . endsWith ( ".m3u8" ) ||
439445 relative . endsWith ( ".m4s" ) ||
440446 relative . endsWith ( ".ts" ) ||
447+ relative . endsWith ( "/progressive.mp4" ) ||
441448 / \/ i n i t _ [ A - Z a - z 0 - 9 _ - ] + \. m p 4 $ / . test ( relative )
442449 ) ;
443450 } )
@@ -466,6 +473,97 @@ function cacheControlForKey(key) {
466473 return "public, max-age=31536000, immutable" ;
467474}
468475
476+ const startupRenditions = [ "360p" , "480p" , "720p" , "1080p" , "2k" , "4k" ] ;
477+
478+ function mediaSegmentNamesFromPlaylist ( playlistText ) {
479+ return playlistText
480+ . split ( / \r ? \n / )
481+ . map ( ( line ) => line . trim ( ) )
482+ . filter ( ( line ) => {
483+ if ( ! line || line . startsWith ( "#" ) ) return false ;
484+ if ( line . includes ( "/" ) || line . includes ( "\\" ) || line . includes ( ".." ) ) {
485+ return false ;
486+ }
487+ return / ^ s e g m e n t _ [ 0 - 9 ] + \. m 4 s $ / . test ( line ) ;
488+ } ) ;
489+ }
490+
491+ async function getObjectBuffer ( env , key , bucket = env . S3_BUCKET ) {
492+ const response = await s3Fetch ( env , {
493+ method : "GET" ,
494+ key,
495+ bucket,
496+ } ) ;
497+ return Buffer . from ( await response . arrayBuffer ( ) ) ;
498+ }
499+
500+ async function putObjectBuffer (
501+ env ,
502+ key ,
503+ body ,
504+ contentType ,
505+ bucket = env . S3_BUCKET ,
506+ ) {
507+ await s3Fetch ( env , {
508+ method : "PUT" ,
509+ key,
510+ bucket,
511+ headers : {
512+ "content-type" : contentType ,
513+ } ,
514+ body,
515+ } ) ;
516+ }
517+
518+ async function backfillProgressiveObjects ( env , options , assetId , allKeys ) {
519+ if ( ! options . backfillProgressive ) return allKeys ;
520+ const knownKeys = new Set ( allKeys ) ;
521+ const writtenKeys = [ ] ;
522+
523+ for ( const rendition of startupRenditions ) {
524+ const prefix = `videos/${ assetId } /hls/${ rendition } ` ;
525+ const targetKey = `${ prefix } /progressive.mp4` ;
526+ if ( knownKeys . has ( targetKey ) ) continue ;
527+
528+ const playlistKey = `${ prefix } /index.m3u8` ;
529+ const initKey = `${ prefix } /init_${ rendition } .mp4` ;
530+ if ( ! knownKeys . has ( playlistKey ) || ! knownKeys . has ( initKey ) ) continue ;
531+
532+ const playlist = String ( await getObjectBuffer ( env , playlistKey ) ) ;
533+ const segmentNames = mediaSegmentNamesFromPlaylist ( playlist ) ;
534+ const segmentKeys = segmentNames . map ( ( name ) => `${ prefix } /${ name } ` ) ;
535+ if ( ! segmentKeys . length || segmentKeys . some ( ( key ) => ! knownKeys . has ( key ) ) ) {
536+ continue ;
537+ }
538+
539+ log (
540+ `asset=${ assetId } progressive_plan rendition=${ rendition } segments=${ segmentKeys . length } dry_run=${ options . dryRun } ` ,
541+ ) ;
542+ if ( options . dryRun ) {
543+ knownKeys . add ( targetKey ) ;
544+ writtenKeys . push ( targetKey ) ;
545+ continue ;
546+ }
547+
548+ const parts = [ await getObjectBuffer ( env , initKey ) ] ;
549+ for ( const segmentKey of segmentKeys ) {
550+ parts . push ( await getObjectBuffer ( env , segmentKey ) ) ;
551+ }
552+ const body = Buffer . concat ( parts ) ;
553+ await putObjectBuffer ( env , targetKey , body , "video/mp4" ) ;
554+ knownKeys . add ( targetKey ) ;
555+ writtenKeys . push ( targetKey ) ;
556+ log (
557+ `asset=${ assetId } progressive_written rendition=${ rendition } bytes=${ body . byteLength } ` ,
558+ ) ;
559+ }
560+
561+ if ( writtenKeys . length ) {
562+ return [ ...knownKeys ] . sort ( ) ;
563+ }
564+ return allKeys ;
565+ }
566+
469567async function mapLimit ( items , limit , mapper ) {
470568 let nextIndex = 0 ;
471569 const results = [ ] ;
@@ -540,7 +638,12 @@ async function verifyPublicGet(options, assetId) {
540638
541639async function backfillAsset ( env , options , assetId ) {
542640 const sourcePrefix = `videos/${ assetId } /` ;
543- const allKeys = await listObjectKeys ( env , sourcePrefix , env . S3_BUCKET ) ;
641+ const allKeys = await backfillProgressiveObjects (
642+ env ,
643+ options ,
644+ assetId ,
645+ await listObjectKeys ( env , sourcePrefix , env . S3_BUCKET ) ,
646+ ) ;
544647 const artifactKeys = playbackArtifactKeysForAsset ( allKeys , assetId ) ;
545648 if ( ! artifactKeys . some ( ( key ) => key . endsWith ( "/hls/master.m3u8" ) ) ) {
546649 throw new Error ( `asset=${ assetId } has no hls/master.m3u8 object` ) ;
@@ -559,10 +662,13 @@ async function backfillAsset(env, options, assetId) {
559662 ) ;
560663 if ( options . dryRun ) return ;
561664
562- let copiedBytes = 0 ;
563- await mapLimit ( planned , 6 , async ( { objectKey, aliasKey } ) => {
564- copiedBytes += await putAlias ( env , options , objectKey , aliasKey ) ;
565- } ) ;
665+ const copiedSizes = await mapLimit (
666+ planned ,
667+ 6 ,
668+ async ( { objectKey, aliasKey } ) =>
669+ putAlias ( env , options , objectKey , aliasKey ) ,
670+ ) ;
671+ const copiedBytes = copiedSizes . reduce ( ( total , size ) => total + size , 0 ) ;
566672 log (
567673 `asset=${ assetId } aliases_written=${ planned . length } bytes=${ copiedBytes } ` ,
568674 ) ;
0 commit comments