@@ -27,7 +27,10 @@ use axum::{
2727 response:: { Html , IntoResponse , Response } ,
2828 routing:: { get, post} ,
2929} ;
30- use base64:: { Engine as _, engine:: general_purpose:: URL_SAFE_NO_PAD } ;
30+ use base64:: {
31+ Engine as _,
32+ engine:: general_purpose:: { STANDARD as BASE64_STANDARD , URL_SAFE_NO_PAD } ,
33+ } ;
3134use bytes:: Bytes ;
3235use futures_util:: stream;
3336use hmac:: { Hmac , Mac } ;
@@ -93,6 +96,7 @@ const ORIGIN_PLAYBACK_CACHE_MAX_ENTRIES: usize = 512;
9396const ORIGIN_PLAYBACK_CACHE_MAX_OBJECT_BYTES : usize = 8 * 1024 * 1024 ;
9497const ORIGIN_PLAYBACK_CACHE_MEDIA_TTL : Duration = Duration :: from_secs ( 10 * 60 ) ;
9598const ORIGIN_PLAYBACK_CACHE_MANIFEST_TTL : Duration = Duration :: from_secs ( 60 ) ;
99+ const FAST_EMBED_INLINE_STARTUP_MAX_BYTES : usize = 512 * 1024 ;
96100
97101#[ derive( Clone ) ]
98102struct ApiConfig {
@@ -887,6 +891,14 @@ struct FastEmbedPlaybackSelection {
887891 url : String ,
888892}
889893
894+ #[ derive( Clone , Debug , PartialEq , Eq ) ]
895+ struct FastEmbedInlineStartup {
896+ artifact_path : String ,
897+ mime_type : String ,
898+ startup_b64 : String ,
899+ segment_urls : Vec < String > ,
900+ }
901+
890902#[ derive( Clone , Debug , PartialEq , Eq , Serialize ) ]
891903struct PlaybackPrefetchHint {
892904 artifact_path : String ,
@@ -2335,7 +2347,21 @@ async fn api_fast_embed_inner(
23352347 . as_deref ( )
23362348 . map ( |value| query_flag ( Some ( value) , true ) )
23372349 . unwrap_or ( auto_play) ;
2338- let html = render_api_fast_embed_html ( & response, & selection, auto_play, controls, muted) ;
2350+ let inline_startup = if startup == "mse" {
2351+ fast_embed_inline_startup ( state. as_ref ( ) , & response, & selection)
2352+ . await
2353+ . unwrap_or ( None )
2354+ } else {
2355+ None
2356+ } ;
2357+ let html = render_api_fast_embed_html (
2358+ & response,
2359+ & selection,
2360+ inline_startup. as_ref ( ) ,
2361+ auto_play,
2362+ controls,
2363+ muted,
2364+ ) ;
23392365 let mut rendered = Html ( html) . into_response ( ) ;
23402366 let headers = rendered. headers_mut ( ) ;
23412367 headers. insert ( header:: CACHE_CONTROL , HeaderValue :: from_static ( "no-store" ) ) ;
@@ -3584,9 +3610,11 @@ fn query_flag(value: Option<&str>, fallback: bool) -> bool {
35843610
35853611fn fast_embed_startup_mode ( value : Option < & str > ) -> & ' static str {
35863612 match value {
3613+ Some ( "mse" ) | Some ( "inline" ) | Some ( "inline-mse" ) => "mse" ,
35873614 Some ( "opener" ) => "opener" ,
35883615 Some ( "hls" ) | Some ( "native" ) => "hls" ,
3589- _ => "progressive" ,
3616+ Some ( "progressive" ) => "progressive" ,
3617+ _ => "mse" ,
35903618 }
35913619}
35923620
@@ -3608,7 +3636,7 @@ fn fast_embed_playback_selection(
36083636 }
36093637 }
36103638
3611- if startup == "progressive"
3639+ if matches ! ( startup, "mse" | "progressive" )
36123640 && let Some ( selection) = fast_embed_progressive_selection ( response)
36133641 {
36143642 return Some ( selection) ;
@@ -3702,6 +3730,107 @@ fn fast_embed_progressive_rendition(hints: &[PlaybackPrefetchHint]) -> Option<St
37023730 } )
37033731}
37043732
3733+ async fn fast_embed_inline_startup (
3734+ state : & AppState ,
3735+ response : & PlaybackBootstrapResponse ,
3736+ selection : & FastEmbedPlaybackSelection ,
3737+ ) -> Result < Option < FastEmbedInlineStartup > , AppError > {
3738+ let Some ( rendition) = hls_progressive_rendition ( & selection. artifact_path ) else {
3739+ return Ok ( None ) ;
3740+ } ;
3741+
3742+ let master_artifact = origin_playback_artifact ( & response. asset_id , "hls/master.m3u8" ) ?;
3743+ let playlist_artifact = hls_progressive_playlist_artifact ( & response. asset_id , rendition) ?;
3744+ let init_artifact = hls_progressive_init_artifact ( & response. asset_id , rendition) ?;
3745+
3746+ let ( master_part, playlist_part) = tokio:: try_join!(
3747+ origin_playback_artifact_full_bytes( state, & master_artifact) ,
3748+ origin_playback_artifact_full_bytes( state, & playlist_artifact)
3749+ ) ?;
3750+ let ( master_bytes, _, _) = master_part;
3751+ let ( playlist_bytes, _, _) = playlist_part;
3752+ let master = std:: str:: from_utf8 ( & master_bytes)
3753+ . map_err ( |_| AppError :: bad_gateway ( "invalid master playlist" ) ) ?;
3754+ let playlist = std:: str:: from_utf8 ( & playlist_bytes)
3755+ . map_err ( |_| AppError :: bad_gateway ( "invalid media playlist" ) ) ?;
3756+ let segment_names = hls_progressive_segment_names ( playlist) ;
3757+ let Some ( first_segment) = segment_names. first ( ) else {
3758+ return Ok ( None ) ;
3759+ } ;
3760+
3761+ let first_segment_artifact =
3762+ hls_progressive_segment_artifact ( & response. asset_id , rendition, first_segment) ?;
3763+ let ( init_part, first_segment_part) = tokio:: try_join!(
3764+ origin_playback_artifact_full_bytes( state, & init_artifact) ,
3765+ origin_playback_artifact_full_bytes( state, & first_segment_artifact)
3766+ ) ?;
3767+ let ( init_bytes, _, _) = init_part;
3768+ let ( first_segment_bytes, _, _) = first_segment_part;
3769+
3770+ let startup_len = init_bytes. len ( ) + first_segment_bytes. len ( ) ;
3771+ if startup_len > FAST_EMBED_INLINE_STARTUP_MAX_BYTES {
3772+ return Ok ( None ) ;
3773+ }
3774+
3775+ let mut startup = Vec :: with_capacity ( startup_len) ;
3776+ startup. extend_from_slice ( & init_bytes) ;
3777+ startup. extend_from_slice ( & first_segment_bytes) ;
3778+
3779+ let mime_type = hls_master_codecs_for_rendition ( master, rendition)
3780+ . map ( |codecs| format ! ( "video/mp4; codecs=\" {codecs}\" " ) )
3781+ . unwrap_or_else ( || "video/mp4" . to_owned ( ) ) ;
3782+ let segment_urls = segment_names
3783+ . iter ( )
3784+ . skip ( 1 )
3785+ . map ( |segment| {
3786+ artifact_url (
3787+ & state. config . playback_base_url ,
3788+ & response. asset_id ,
3789+ & format ! ( "hls/{rendition}/{segment}" ) ,
3790+ )
3791+ } )
3792+ . collect :: < Vec < _ > > ( ) ;
3793+
3794+ Ok ( Some ( FastEmbedInlineStartup {
3795+ artifact_path : format ! (
3796+ "hls/{rendition}/init_{rendition}.mp4+hls/{rendition}/{first_segment}"
3797+ ) ,
3798+ mime_type,
3799+ startup_b64 : BASE64_STANDARD . encode ( startup) ,
3800+ segment_urls,
3801+ } ) )
3802+ }
3803+
3804+ fn hls_master_codecs_for_rendition ( master : & str , rendition : & str ) -> Option < String > {
3805+ let rendition_path = format ! ( "{rendition}/index.m3u8" ) ;
3806+ let mut pending_codecs = None ;
3807+ for line in master. lines ( ) . map ( str:: trim) {
3808+ if line. starts_with ( "#EXT-X-STREAM-INF:" ) {
3809+ pending_codecs = hls_attribute_value ( line, "CODECS" ) ;
3810+ continue ;
3811+ }
3812+ if line. is_empty ( ) || line. starts_with ( '#' ) {
3813+ continue ;
3814+ }
3815+ let matches_rendition =
3816+ line == rendition_path || line. ends_with ( & format ! ( "/{rendition_path}" ) ) ;
3817+ if matches_rendition {
3818+ return pending_codecs;
3819+ }
3820+ pending_codecs = None ;
3821+ }
3822+ None
3823+ }
3824+
3825+ fn hls_attribute_value ( line : & str , name : & str ) -> Option < String > {
3826+ let attributes = line. split_once ( ':' ) ?. 1 ;
3827+ let prefix = format ! ( "{name}=\" " ) ;
3828+ let start = attributes. find ( & prefix) ? + prefix. len ( ) ;
3829+ let value = & attributes[ start..] ;
3830+ let end = value. find ( '"' ) ?;
3831+ Some ( value[ ..end] . to_owned ( ) )
3832+ }
3833+
37053834fn html_escape ( value : impl AsRef < str > ) -> String {
37063835 value
37073836 . as_ref ( )
@@ -3711,9 +3840,28 @@ fn html_escape(value: impl AsRef<str>) -> String {
37113840 . replace ( '"' , """ )
37123841}
37133842
3843+ fn script_json ( value : serde_json:: Value ) -> String {
3844+ serde_json:: to_string ( & value)
3845+ . unwrap_or_else ( |_| "null" . to_owned ( ) )
3846+ . replace ( '<' , "\\ u003c" )
3847+ }
3848+
3849+ fn inline_startup_script_json ( inline_startup : Option < & FastEmbedInlineStartup > ) -> String {
3850+ match inline_startup {
3851+ Some ( inline) => script_json ( serde_json:: json!( {
3852+ "artifactPath" : inline. artifact_path,
3853+ "mimeType" : inline. mime_type,
3854+ "startup" : inline. startup_b64,
3855+ "segmentUrls" : inline. segment_urls,
3856+ } ) ) ,
3857+ None => "null" . to_owned ( ) ,
3858+ }
3859+ }
3860+
37143861fn render_api_fast_embed_html (
37153862 response : & PlaybackBootstrapResponse ,
37163863 selection : & FastEmbedPlaybackSelection ,
3864+ inline_startup : Option < & FastEmbedInlineStartup > ,
37173865 auto_play : bool ,
37183866 controls : bool ,
37193867 muted : bool ,
@@ -3722,7 +3870,40 @@ fn render_api_fast_embed_html(
37223870 let controls_attr = if controls { " controls" } else { "" } ;
37233871 let muted_attr = if muted { " muted" } else { "" } ;
37243872 let poster_attr = "" ;
3725- let preload_link = if selection. content_type == "video/mp4" {
3873+ let selected_label = if inline_startup. is_some ( ) {
3874+ "mse_inline"
3875+ } else {
3876+ selection. label
3877+ } ;
3878+ let selected_artifact = inline_startup
3879+ . map ( |inline| inline. artifact_path . as_str ( ) )
3880+ . unwrap_or ( & selection. artifact_path ) ;
3881+ let playback_engine = if inline_startup. is_some ( ) {
3882+ "mse-inline"
3883+ } else {
3884+ "native"
3885+ } ;
3886+ let source_attrs = if inline_startup. is_some ( ) {
3887+ String :: new ( )
3888+ } else {
3889+ format ! (
3890+ r#" src="{}" type="{}""# ,
3891+ html_escape( & selection. url) ,
3892+ html_escape( & selection. content_type)
3893+ )
3894+ } ;
3895+ let preload_link = if let Some ( inline) = inline_startup {
3896+ inline
3897+ . segment_urls
3898+ . first ( )
3899+ . map ( |url| {
3900+ format ! (
3901+ r#"<link rel="preload" as="fetch" href="{}" type="video/mp4" crossorigin="use-credentials" fetchpriority="high">"# ,
3902+ html_escape( url)
3903+ )
3904+ } )
3905+ . unwrap_or_default ( )
3906+ } else if selection. content_type == "video/mp4" {
37263907 format ! (
37273908 r#"<link rel="preload" as="video" href="{}" type="{}" crossorigin="use-credentials" fetchpriority="high">"# ,
37283909 html_escape( & selection. url) ,
@@ -3731,6 +3912,13 @@ fn render_api_fast_embed_html(
37313912 } else {
37323913 String :: new ( )
37333914 } ;
3915+ let inline_startup_json = inline_startup_script_json ( inline_startup) ;
3916+ let fallback_json = script_json ( serde_json:: json!( {
3917+ "artifactPath" : selection. artifact_path,
3918+ "contentType" : selection. content_type,
3919+ "label" : selection. label,
3920+ "url" : selection. url,
3921+ } ) ) ;
37343922
37353923 format ! (
37363924 r#"<!doctype html>
@@ -3751,23 +3939,25 @@ body{{overflow:hidden}}
37513939</style>
37523940</head>
37533941<body>
3754- <main class="rend-fast" aria-label="Video player" data-rend-player-state="ready" data-rend-player-selected="{label}" data-rend-player-artifact="{artifact_path}" data-rend-ready-status="ready" data-rend-source-state="{source_state}" data-rend-playable-state="{playable_state}" data-rend-playback-engine="native " data-rend-document-start-ms="0" data-rend-bootstrap-ms="0" data-rend-asset-id="{asset_id}">
3755- <video class="rend-fast__video" src="{url}" type="{content_type}" {poster_attr}{auto_play_attr}{controls_attr}{muted_attr} playsinline preload="auto" crossorigin="use-credentials"></video>
3942+ <main class="rend-fast" aria-label="Video player" data-rend-player-state="ready" data-rend-player-selected="{label}" data-rend-player-artifact="{artifact_path}" data-rend-ready-status="ready" data-rend-source-state="{source_state}" data-rend-playable-state="{playable_state}" data-rend-playback-engine="{playback_engine} " data-rend-document-start-ms="0" data-rend-bootstrap-ms="0" data-rend-asset-id="{asset_id}">
3943+ <video class="rend-fast__video"{source_attrs} {poster_attr}{auto_play_attr}{controls_attr}{muted_attr} playsinline preload="auto" crossorigin="use-credentials"></video>
37563944<div class="rend-fast__message" role="status" aria-live="polite">Ready</div>
37573945</main>
37583946<script>
3759- (()=>{{const root=document.querySelector("[data-rend-player-state]");const video=document.querySelector("video");if(!root||!video)return;const mark=(name)=>{{if(!root.getAttribute(name))root.setAttribute(name,String(Math.round(performance.now())))}};const dims=()=>{{if(video.videoWidth)root.setAttribute("data-rend-selected-width",String(video.videoWidth));if(video.videoHeight)root.setAttribute("data-rend-selected-height",String(video.videoHeight))}};video.addEventListener("loadedmetadata",()=>{{dims();mark("data-rend-metadata-ms")}},{{once:true}});video.addEventListener("canplay",()=>{{dims();mark("data-rend-canplay-ms")}},{{once:true}});video.addEventListener("playing",()=>{{root.setAttribute("data-rend-player-state","playing");dims()}});if("requestVideoFrameCallback"in video){{video.requestVideoFrameCallback(()=>{{dims();mark("data-rend-first-frame-ms")}})}}else{{video.addEventListener("playing",()=>mark("data-rend-first-frame-ms"),{{once:true}})}}if({auto_play_js})video.play().catch(()=>{{}})}})();
3947+ (()=>{{const root=document.querySelector("[data-rend-player-state]");const video=document.querySelector("video");if(!root||!video)return;const inlineStartup={inline_startup_json};const fallback={fallback_json};const autoPlay={auto_play_js};const mark=(name)=>{{if(!root.getAttribute(name))root.setAttribute(name,String(Math.round(performance.now())))}};const dims=()=>{{if(video.videoWidth)root.setAttribute("data-rend-selected-width",String(video.videoWidth));if(video.videoHeight)root.setAttribute("data-rend-selected-height",String(video.videoHeight))}};const play=()=>{{if(autoPlay)video.play().catch(()=>{{}})}};const selection=(label,artifactPath,engine)=>{{root.setAttribute("data-rend-player-selected",label);root.setAttribute("data-rend-player-artifact",artifactPath);root.setAttribute("data-rend-playback-engine",engine);root.setAttribute("data-rend-player-state","ready")}};const bytes=(value)=>{{const binary=atob(value);const output=new Uint8Array(binary.length);for(let i=0;i<binary.length;i++)output[i]=binary.charCodeAt(i);return output}};const append=(buffer,data)=>new Promise((resolve,reject)=>{{const done=()=>{{cleanup();resolve()}};const fail=()=>{{cleanup();reject(new Error("append failed"))}};const cleanup=()=>{{buffer.removeEventListener("updateend",done);buffer.removeEventListener("error",fail)}};buffer.addEventListener("updateend",done);buffer.addEventListener("error",fail);buffer.appendBuffer(data)}});const sourceOpen=(mediaSource)=>new Promise((resolve,reject)=>{{if(mediaSource.readyState==="open"){{resolve();return}}const done=()=>{{cleanup();resolve()}};const fail=()=>{{cleanup();reject(new Error("source open failed"))}};const cleanup=()=>{{mediaSource.removeEventListener("sourceopen",done);mediaSource.removeEventListener("sourceclose",fail);mediaSource.removeEventListener("sourceended",fail)}};mediaSource.addEventListener("sourceopen",done,{{once:true}});mediaSource.addEventListener("sourceclose",fail,{{once:true}});mediaSource.addEventListener("sourceended",fail,{{once:true}})}});const startNative=()=>{{selection(fallback.label,fallback.artifactPath,"native");if(fallback.url&&video.getAttribute("src")!==fallback.url){{video.src=fallback.url;video.load()}}play()}};const startInline=async()=>{{if(!inlineStartup||!("MediaSource"in window)||!MediaSource.isTypeSupported(inlineStartup.mimeType))return false;selection("mse_inline",inlineStartup.artifactPath,"mse-inline");const mediaSource=new MediaSource();const objectUrl=URL.createObjectURL(mediaSource);video.removeAttribute("src");video.src=objectUrl;video.load();await sourceOpen(mediaSource);const sourceBuffer=mediaSource.addSourceBuffer(inlineStartup.mimeType);await append(sourceBuffer,bytes(inlineStartup.startup));play();(async()=>{{for(const url of inlineStartup.segmentUrls){{const response=await fetch(url,{{credentials:"include"}});if(!response.ok)throw new Error("segment fetch failed");await append(sourceBuffer,new Uint8Array(await response.arrayBuffer()))}}if(mediaSource.readyState==="open")mediaSource.endOfStream()}})().catch(()=>{{}});return true}};video.addEventListener("loadedmetadata",()=>{{dims();mark("data-rend-metadata-ms")}},{{once:true}});video.addEventListener("canplay",()=>{{dims();mark("data-rend-canplay-ms")}},{{once:true}});video.addEventListener("playing",()=>{{root.setAttribute("data-rend-player-state","playing");dims()}});if("requestVideoFrameCallback"in video){{video.requestVideoFrameCallback(()=>{{dims();mark("data-rend-first-frame-ms")}})}}else{{video.addEventListener("playing",()=>mark("data-rend-first-frame-ms"),{{once:true}})}}startInline().then((started)=>{{if(!started)startNative()}}).catch(()=>startNative())}})();
37603948</script>
37613949</body>
37623950</html>"# ,
37633951 asset_id = html_escape( & response. asset_id) ,
3764- artifact_path = html_escape( & selection . artifact_path ) ,
3952+ artifact_path = html_escape( selected_artifact ) ,
37653953 auto_play_js = if auto_play { "true" } else { "false" } ,
3766- content_type = html_escape( & selection. content_type) ,
3767- label = html_escape( selection. label) ,
3954+ fallback_json = fallback_json,
3955+ inline_startup_json = inline_startup_json,
3956+ label = html_escape( selected_label) ,
37683957 playable_state = html_escape( & response. playable_state) ,
3958+ playback_engine = html_escape( playback_engine) ,
3959+ source_attrs = source_attrs,
37693960 source_state = html_escape( & response. source_state) ,
3770- url = html_escape( & selection. url) ,
37713961 )
37723962}
37733963
0 commit comments