@@ -9,8 +9,6 @@ import { createFile } from "mp4box"
import type { Movie , Sample } from "mp4box"
// ── 常量 ──
/** 初始化预解码最大帧数(约 2 秒 @30fps),后续帧通过 decodeAroundPosition 按需解码 */
const MAX_INIT_FRAMES = 60
/**
* 规范化 mp4box 提取的 codec 字符串为 WebCodecs 兼容格式
@@ -111,7 +109,7 @@ class FrameQueue {
private frames : FrameEntry [ ] = [ ]
private maxSize : number
constructor ( maxSize = 5 ) {
constructor ( maxSize = 200 ) {
this . maxSize = maxSize
}
@@ -123,24 +121,36 @@ class FrameQueue {
this . frames . push ( entry )
}
/** 获取当前时间戳应显示的帧 */
/** 获取当前时间戳应显示的帧(二分查找,O(log n)) */
getCurrentFrame ( timestamp : number ) : VideoFrame | null {
let best : FrameEntry | null = null
let bestIdx = - 1
for ( let i = 0 ; i < this . frames . length ; i ++ ) {
const f = this . frames [ i ]
if ( f . pts <= timestamp + 0.01 ) {
best = f
bestIdx = i
if ( this . frames . length === 0 ) return null
const target = timestamp + 0.01
// 找到最后一个 pts <= target 的帧(右边界)
let lo = 0 ,
hi = this . frames . length - 1 ,
bestIdx = - 1
while ( lo <= hi ) {
const mid = ( lo + hi ) >> 1
if ( this . frames [ mid ] . pts <= target ) {
bestIdx = mid
lo = mid + 1
} else {
hi = mid - 1
}
}
if ( bestIdx < 0 ) return null
// 关闭并移除 bestIdx 之前的所有已播放帧
for ( let i = 0 ; i < bestIdx ; i ++ ) {
this . frames [ i ] . frame . close ( )
}
if ( bestIdx >= 0 ) {
this . frames = this . frames . slice ( bestIdx )
}
return best ? . frame ? ? null
this . frames . splice ( 0 , bestIdx )
// 此时 bestIdx 对应帧已在索引 0
return this . frames [ 0 ] ? . frame ? ? null
}
clear() {
@@ -243,9 +253,13 @@ export function useCanvasPlayer(
// ── 内部引用 ──
const decoderRef = useRef < VideoDecoder | null > ( null )
const frameQueueRef = useRef ( new FrameQueue ( 600 ) )
/** 已解码的片段索引集合,用于按需解码(先标记防重入,失败时移除允许重试) */
const decodedSegmentsRef = useRef ( new Set < number > ( ) )
const frameQueueRef = useRef ( new FrameQueue ( 200 ) )
/** 每个片段持久化解码器,避免每次新建导致关键帧错误 */
const segmentDecodersRef = useRef ( new Map < number , VideoDecoder > ( ) )
/** 每个片段已送入解码器的 sample 游标(用于续解码) */
const segmentSampleCursorRef = useRef ( new Map < number , number > ( ) )
/** 后台补充解码是否正在运行(防重入) */
const isFeedingRef = useRef ( false )
/** 解码代数计数器,seek 时递增以作废正在进行的异步解码 */
const decodeGenerationRef = useRef ( 0 )
const rafRef = useRef < number > ( 0 )
@@ -475,115 +489,91 @@ export function useCanvasPlayer(
[ segments , extractCodecDescription ] ,
)
// ── 初始化 VideoDecoder 并解码指定片段 ──
const decodeSegment = useCallback (
async ( _buffer : ArrayBuffer , meta : SegmentMeta , maxFrames? : number ) : Promise < void > = > {
if ( isDestroyedRef . current ) return
// ── 解码片段的一批帧(使用持久化解码器,支持从断点续解码) ──
/**
* @param segIdx 片段索引
* @param maxFrames 本次最多解码多少帧
*/
const decodeSegmentBatch = useCallback (
async ( segIdx : number , maxFrames : number = 60 ) : Promise < number > = > {
if ( isDestroyedRef . current ) return 0
const metas = segmentMetaRef . current
const meta = metas [ segIdx ]
if ( ! meta ) return 0
const buffer = segmentDataRef . current . get ( meta . assetId )
if ( ! buffer ) return 0
let decoderReady = false
const gen = decodeGenerationRef . current
let decoder = segmentDecodersRef . current . get ( segIdx )
let cursor = segmentSampleCursorRef . current . get ( segIdx ) ? ? 0
const samples = meta . samples
// 配置解码器(每个片段可能需要不同的 codec/分辨率 )
const decoder = new VideoDecoder ( {
output : ( frame : VideoFrame ) = > {
// 从第一帧获取实际尺寸
if ( videoDimRef . current . width === 0 || videoDimRef . current . height === 0 ) {
videoDimRef . current = { width : frame.codedWidth , height : frame.codedHeight }
console . log (
` [useCanvasPlayer] Actual frame size: ${ frame . codedWidth } x ${ frame . codedHeight } ` ,
)
}
const localTime = frame . timestamp / 1 _000_000
const globalTime = localTime + meta . globalStartTime
frameQueueRef . current . push ( {
frame ,
pts : globalTime ,
duration : ( frame . duration ? ? 0 ) / 1 _000_000 ,
// 如果还没有解码器,新建一个(从关键帧开始,不会报 key frame 错误 )
if ( ! decoder || decoder . state === "closed" ) {
decoder = new VideoDecoder ( {
output : ( frame : VideoFrame ) = > {
if ( videoDimRef . current . width === 0 || videoDimRef . current . height === 0 ) {
videoDimRef . current = { width : frame.codedWidth , height : frame.codedHeight }
}
const localTime = frame . timestamp / 1 _000_000
const globalTime = localTime + meta . globalStartTime
frameQueueRef . current . push ( {
frame ,
pts : globalTime ,
duration : ( frame . duration ? ? 0 ) / 1 _000_000 ,
} )
} ,
error : ( e : DOMException ) = > {
console . error ( ` [useCanvasPlayer] Segment ${ segIdx } decoder error: ` , e )
// 重置该片段的解码器和游标,允许重试
segmentDecodersRef . current . delete ( segIdx )
segmentSampleCursorRef . current . set ( segIdx , 0 )
} ,
} )
try {
await decoder . configure ( {
codec : meta.codec ,
. . . ( meta . description ? { description : meta.description } : { } ) ,
} )
} ,
error : ( e : DOMException ) = > {
console . error ( "[useCanvasPlayer] Decoder error callback:" , e )
const error = new Error ( ` VideoDecoder error: ${ e . message || e . name || "unknown" } ` )
} catch ( err ) {
console . error ( ` [useCanvasPlayer] Segment ${ segIdx } configure failed: ` , err )
const error = err instanceof Error ? err : new Error ( String ( err ) )
setState ( ( s ) = > ( {
. . . s ,
isBuffering : false ,
hasDecodeError : true ,
errorMessage : ` 视频解码器错误 : ${ e . message || "解码异常 " } ` ,
errorMessage : ` 视频解码失败 : ${ error . message || "不支持的编解码器 " } ` ,
} ) )
onErrorRef . current ? . ( error )
} ,
} )
return 0
}
console . log ( "[useCanvasPlayer] configure:" , {
codec : meta.codec ,
description : meta.description ,
descriptionByteLength : meta.description?.byteLength ,
videoWidth : meta.videoWidth ,
videoHeight : meta.videoHeight ,
codecCharCodes : meta.codec.split ( "" ) . map ( ( c ) = > c . charCodeAt ( 0 ) ) ,
} )
try {
await decoder . configure ( {
codec : meta.codec ,
. . . ( meta . description ? { description : meta.description } : { } ) ,
} )
decoderRef . current = decoder
decoderReady = true
// 标记缓冲结束,让 UI 开始渲染
segmentDecodersRef . current . set ( segIdx , decoder )
cursor = 0
// 标记缓冲结束
setState ( ( s ) = > ( { . . . s , isBuffering : false } ) )
} catch ( err ) {
const error = err instanceof Error ? err : new Error ( String ( err ) )
console . error (
"[useCanvasPlayer] Decoder configure failed for segment:" ,
meta . assetId ,
error ,
)
console . error ( "[useCanvasPlayer] Failed codec config:" , {
codec : meta.codec ,
descriptionByteLength : meta.description?.byteLength ,
videoWidth : meta.videoWidth ,
videoHeight : meta.videoHeight ,
} )
setState ( ( s ) = > ( {
. . . s ,
isBuffering : false ,
isReady : false ,
hasDecodeError : true ,
errorMessage : ` 视频解码失败: ${ error . message || "不支持的编解码器" } ` ,
} ) )
onErrorRef . current ? . ( error )
return
}
if ( ! decoderReady ) return
if ( decoder . state !== "configured" ) return 0
// 使用 demuxSegment 中已提取并过滤的 samples(前端切片 )
const samplesCollected = meta . samples
console . log (
` [useCanvasPlayer] Segment ${ meta . assetId } : ${ samplesCollected . length } samples to decode ` ,
)
if ( samplesCollected . length === 0 ) {
console . warn ( "[useCanvasPlayer] No samples to decode for segment" , meta . assetId )
return
}
// 送入解码器
let decodedCount = 0
let skippedCount = 0
let decodeErrors = 0
for ( const sample of samplesCollected ) {
if ( ! sample . data || isDestroyedRef . current ) {
skippedCount ++
// 从 cursor 继续喂 sample(流水线批量提交,不 await 单个 decode )
let decoded = 0
let si = cursor
while ( si < samples . length && decoded < maxFrames ) {
if ( decodeGenerationRef . current !== gen || isDestroyedRef . current ) break
if ( ( decoder . state as string ) === "closed" ) break
// 队列快满时停止,等播放消费后再补充
if ( frameQueueRef . current . size >= 180 ) break
// 解码器内部队列积压过多时暂停提交,做背压
if ( decoder . decodeQueueSize > 20 ) {
await new Promise ( ( r ) = > setTimeout ( r , 10 ) )
continue
}
if ( decoder . state === "closed" ) break
// 初始化阶段限制解码帧数,避免帧缓冲溢出
if ( maxFrames && decodedCount >= maxFrames ) {
console . log (
` [useCanvasPlayer] Segment ${ meta . assetId } : init decode limited to ${ maxFrames } frames ` ,
)
break
}
const sample = samples [ si ]
si ++
if ( ! sample . data ) continue
const chunk = new EncodedVideoChunk ( {
type : sample . is_sync ? "key" : "delta" ,
@@ -593,93 +583,64 @@ export function useCanvasPlayer(
} )
try {
await decoder . decode ( chunk ) // 修复: await 捕获异步错误
decodedCount ++
// 不 await 单个 decode,让硬件解码器流水线并行处理
decoder . decode ( chunk )
decoded ++
} catch ( e ) {
decodeErrors ++
console . warn ( ` [useCanvasPlayer] Decode chunk error ( ${ decodeErrors } ): ` , e )
// 连续 3 次解码失败,放弃当前片段并报告错误
if ( decodeErrors >= 3 ) {
console . error ( "[useCanvasPlayer] Too many decode errors, aborting segment" )
const error = new Error ( ` 视频解码连续失败 ${ decodeErrors } 次,片段: ${ meta . assetId } ` )
setState ( ( s ) = > ( {
. . . s ,
isBuffering : false ,
hasDecodeError : true ,
errorMessage : ` 视频解码失败: 连续 ${ decodeErrors } 次错误 ` ,
} ) )
onErrorRef . current ? . ( error )
break
}
console . warn ( ` [useCanvasPlayer] Segment ${ segIdx } decode error: ` , e )
break
}
}
console . log (
` [useCanvasPlayer] Segment ${ meta . assetId } : decoded ${ decodedCount } , skipped ${ skippedCount } , errors ${ decodeErrors } , decoder.state= ${ decoder . state } ` ,
)
// flush 仅在解码器状态正常时执行
if ( decoder . state === "configured" ) {
try {
await decoder . flush ( )
console . log ( ` [useCanvasPlayer] Segment ${ meta . assetId } : flush complete ` )
} catch ( e ) {
console . warn ( "[useCanvasPlayer] Decoder flush error:" , e )
}
}
cursor = si
segmentSampleCursorRef . current . set ( segIdx , cursor )
// 给解码器一点时间输出帧
await new Promise ( ( r ) = > setTimeout ( r , 16 ) )
return decoded
} ,
[ ] ,
)
// ── 后台持续补充帧 ──
/**
* 按需解码当前播放位置 ±1 个片段。
* 在渲染循环中定期调用,避免一次性解码所有片段导致环形缓冲区溢出丢帧。
* 使用"先标记再解码"模式防止并发重复解码,失败时移除标记允许重试。
* 根据当前播放时间,确保队列中有足够缓冲
* 播放循环每 200ms 调用一次
*/
const decodeAroundPosition = useCallback (
const feedFrames = useCallback (
async ( currentTime : number ) = > {
const metas = segmentMetaRef . current
if ( ! metas || metas . length === 0 ) return
if ( isFeedingRef . current ) return
isFeedingRef . current = true
try {
const metas = segmentMetaRef . current
if ( ! metas || metas . length === 0 ) return
// 记录当前代数,seek 后代数变化则中止
const gen = decodeGenerationRef . current
// 队列帧数充足时不解码(目标:保持 >= 80 帧缓冲)
if ( frameQueueRef . current . size > = 80 ) return
let targetIdx = - 1
let acc = 0
for ( let i = 0 ; i < metas . length ; i ++ ) {
const dur = metas [ i ] . globalEndTime - metas [ i ] . globalStartTime
if ( currentTime < acc + dur ) {
targetIdx = i
break
// 找到当前播放的片段
let targetIdx = 0
let acc = 0
for ( let i = 0 ; i < metas . length ; i ++ ) {
const dur = metas [ i ] . globalEndTime - metas [ i ] . globalStartTime
if ( currentTime < acc + dur ) {
targetIdx = i
break
}
acc += dur
}
acc += dur
}
if ( targetIdx === - 1 ) targetIdx = metas . length - 1
for (
let i = Math . max ( 0 , targetIdx - 1 ) ;
i <= Math . min ( metas . length - 1 , targetIdx + 1 ) ;
i ++
) {
// seek 已作废当前解码任务
if ( decodeGenerationRef . current !== gen ) return
if ( decodedSegmentsRef . current . has ( i ) ) continue
const meta = metas [ i ]
const buffer = segmentDataRef . current . get ( meta . assetId )
if ( ! buffer ) continue
// 先标记为解码中,防止下一帧渲染时重复发起解码
decodedSegmentsRef . current . add ( i )
try {
await decodeSegment ( buffer , meta , 300 )
} catch ( e ) {
// 解码失败则移除标记,允许后续重试
decodedSegmentsRef . current . delete ( i )
console . warn ( ` [useCanvasPlayer] 按需解码片段 ${ i } 失败: ` , e )
// 依次补充:当前片段 → 下一个片段 → 再下一个
for ( let offset = 0 ; offset <= 2 ; offset ++ ) {
const idx = targetIdx + offset
if ( idx >= metas . length ) break
if ( frameQueueRef . current . size >= 180 ) break
await decodeSegmentBatch ( idx , 60 )
}
// await 后再次检查代数,seek 期间不更新标记
if ( decodeGenerationRef . current != = gen ) return
} finally {
isFeedingRef . current = false
}
} ,
[ decodeSegment ] ,
[ decodeSegmentBatch ] ,
)
// ── 标题绘制 ──
@@ -696,7 +657,6 @@ export function useCanvasPlayer(
// 按 "/" 分割为多行("/" 作为手动换行符)
const lines = title . text . split ( /[//⁄∕]/ )
console . log ( "[drawTitle] 原始标题:" , JSON . stringify ( title . text ) , "分割后:" , lines )
const lineHeight = fontSize * 1.3
const totalHeight = lines . length * lineHeight
@@ -808,8 +768,8 @@ export function useCanvasPlayer(
}
return s
} )
// 按需解码当前 ±1 片段
decodeAroundPosition ( currentTime )
// 后台补充帧:队列不足时自动续解码
feedFrames ( currentTime )
}
if ( currentTime >= totalDuration ) {
@@ -818,29 +778,46 @@ export function useCanvasPlayer(
}
rafRef . current = requestAnimationFrame ( renderFrame )
} , [ canvasRef , totalDuration , titleSettings , drawTitle , computeDrawRect , decodeAroundPosition ] )
} , [ canvasRef , totalDuration , titleSettings , drawTitle , computeDrawRect , feedFrames ] )
// ── 播放控制 ──
const play = useCallback ( async ( ) = > {
if ( ! state . hasSupport || isDestroyedRef . current ) return
// 重播场景:currentTime 已回到起点但 decodedSegmentsRef 仍有旧标记
// 此时 FrameQueue 中旧帧已被淘汰,需清空标记让 decodeAroundPosition 重新解码
if ( state . currentTime <= 0.1 && decodedSegmentsRef . current . size > 0 ) {
// 重播:必须关闭旧解码器、清空队列、重置游标,从头重新解码
if ( state . currentTime >= totalDuration - 0.1 || state . currentTime <= 0.1 ) {
decodeGenerationRef . current ++
decodedSegmentsRef . current . clear ( )
// 同步清空帧缓冲,避免旧帧残留导致 getCurrentFrame 返回 null
// 关闭所有持久化解码器
for ( const d of segmentDecodersRef . current . values ( ) ) {
try {
if ( d . state !== "closed" ) d . close ( )
} catch {
/* noop */
}
}
segmentDecodersRef . current . clear ( )
segmentSampleCursorRef . current . clear ( )
frameQueueRef . current . clear ( )
playStartOffsetRef . current = 0
setState ( ( s ) = > ( { . . . s , currentTime : 0 } ) )
// 重新初始化解码
const metas = segmentMetaRef . current
const initialDecodeCount = Math . min ( metas . length , 2 )
for ( let i = 0 ; i < initialDecodeCount ; i ++ ) {
await decodeSegmentBatch ( i , 60 )
}
}
setState ( ( s ) = > ( { . . . s , isPlaying : true } ) )
playStartRef . current = performance . now ( )
playStartOffsetRef . current = state . currentTime
if ( state . currentTime < 0.1 ) {
playStartOffsetRef . current = 0
} else {
playStartOffsetRef . current = state . currentTime
}
lastProgressUpdateRef . current = 0
rafRef . current = requestAnimationFrame ( renderFrame )
// 立即触发一次按需解码,不等渲染循环 200ms 节流
decodeAroundPosition ( state . currentTime )
} , [ state . hasSupport , state . currentTime , renderFrame , decodeAroundPosition ] )
} , [ state . hasSupport , state . currentTime , totalDuration , renderFrame , decodeSegmentBatch ] )
const pause = useCallback ( ( ) = > {
setState ( ( s ) = > ( { . . . s , isPlaying : false } ) )
@@ -850,33 +827,61 @@ export function useCanvasPlayer(
const seek = useCallback (
async ( time : number ) = > {
const clampedTime = Math . max ( 0 , Math . min ( time , totalDuration ) )
decodeGenerationRef . current ++
// 关闭所有解码器、清空队列、重置游标
for ( const d of segmentDecodersRef . current . values ( ) ) {
try {
if ( d . state !== "closed" ) d . close ( )
} catch {
/* noop */
}
}
segmentDecodersRef . current . clear ( )
segmentSampleCursorRef . current . clear ( )
frameQueueRef . current . clear ( )
setState ( ( s ) = > ( { . . . s , currentTime : clampedTime } ) )
playStartOffsetRef . current = clampedTime
playStartRef . current = performance . now ( )
// seek 时递增解码代数,作废正在进行的异步 解码
decodeGenerationRef . current ++
// 清空帧队列(clear 内部会 close 所有帧)+ 清空已解码标记
frameQueueRef . current . clear ( )
decodedSegmentsRef . current . clear ( )
await decodeAroundPosition ( clampedTime )
// 找到 seek 目标片段,从该片段开始 解码
const metas = segmentMetaRef . current
let targetIdx = 0 ,
acc = 0
for ( let i = 0 ; i < metas . length ; i ++ ) {
const dur = metas [ i ] . globalEndTime - metas [ i ] . globalStartTime
if ( clampedTime < acc + dur ) {
targetIdx = i
break
}
acc += dur
}
await decodeSegmentBatch ( targetIdx , 60 )
await decodeSegmentBatch ( Math . min ( targetIdx + 1 , metas . length - 1 ) , 60 )
} ,
[ totalDuration , decodeAroundPosition ] ,
[ totalDuration , decodeSegmentBatch ] ,
)
const destroy = useCallback ( ( ) = > {
isDestroyedRef . current = true
cancelAnimationFrame ( rafRef . current )
// 关闭所有持久化解码器
for ( const d of segmentDecodersRef . current . values ( ) ) {
try {
if ( d . state !== "closed" ) d . close ( )
} catch {
/* noop */
}
}
segmentDecodersRef . current . clear ( )
segmentSampleCursorRef . current . clear ( )
if ( decoderRef . current && decoderRef . current . state !== "closed" ) {
decoderRef . current . close ( )
}
// 递增代数中止进行中的异步解码,清空帧队列(clear 内部 close 所有帧)
decodeGenerationRef . current ++
frameQueueRef . current . clear ( )
segmentDataRef . current . clear ( )
segmentMetaRef . current = [ ]
decodedSegmentsRef . current . clear ( )
} , [ ] )
// ── 预加载下一个片段的数据 ──
@@ -902,10 +907,24 @@ export function useCanvasPlayer(
}
let cancelled = false
console . log ( "[useCanvasPlayer] Init start, segments:" , segments . length )
const init = async ( ) = > {
setState ( ( s ) = > ( { . . . s , isBuffering : true } ) )
// ✅ 关键修复:重置销毁标记,允许新的 init 周期正常工作
// destroy() 在 useEffect cleanup 中被调用,将 isDestroyedRef 设为 true
// 如果不重置,后续的 loadSegment / decodeSegment 会立即 return
isDestroyedRef . current = false
// ✅ Strict Mode 修复:init 不再递增 generation
// seek() 和 play() 仍保留 generation 递增用于中止异步解码
// 重置错误状态,避免上一轮的解码错误影响新的 init 周期
setState ( ( s ) = > ( {
. . . s ,
isBuffering : true ,
hasDecodeError : false ,
errorMessage : "" ,
isReady : false ,
} ) )
console . log ( "[useCanvasPlayer] Init start v2_DIAG, segments:" , segments . length )
// 1. 加载所有片段数据
for ( const seg of segments ) {
@@ -947,32 +966,44 @@ export function useCanvasPlayer(
segmentMetaRef . current = metas
// 3. 按需解码:初始只解码 前 3 个片段,后续通过 decodeAroundPosition 动态加载
// 避免一次性全量解码导致 frameQueue 环形缓冲区旧帧被丢弃引发黑屏
decodedSegmentsRef . current . clear ( )
const initGen = decodeGenerationRef . current
const initialDecodeCount = Math . min ( metas . length , 3 )
for ( let i = 0 ; i < initialDecodeCount ; i ++ ) {
if ( cancelled ) break
// seek 或 destroy 已作废当前初始化
if ( decodeGenerationRef . current !== initGen ) break
const meta = metas [ i ]
const buffer = segmentDataRef . current . get ( meta . assetId )
if ( ! buffer ) continue
// 先标记为解码中,防止重复解码
decodedSegmentsRef . current . add ( i )
// 3. 初始化解码:关闭旧解码器, 前 2 个片段各解 60 帧
// 后续由 feedFrames 后台补充
for ( const d of segmentDecodersRef . current . values ( ) ) {
try {
await decodeSegment ( buffer , meta , MAX_INIT_FRAMES )
} catch ( e ) {
// 解码失败则移除标记,允许后续重试
decodedSegmentsRef . current . delete ( i )
console . warn ( ` [useCanvasPlayer] 初始化解码片段 ${ i } 失败: ` , e )
if ( d . state !== "closed" ) d . close ( )
} catch {
/* noop */
}
}
segmentDecodersRef . current . clear ( )
segmentSampleCursorRef . current . clear ( )
frameQueueRef . current . clear ( )
const initGen = decodeGenerationRef . current
const initialDecodeCount = Math . min ( metas . length , 2 )
console . log (
` [useCanvasPlayer] Starting init decode: ${ initialDecodeCount } segments, metas: ${ metas . length } ` ,
)
for ( let i = 0 ; i < initialDecodeCount ; i ++ ) {
if ( cancelled ) break
if ( decodeGenerationRef . current !== initGen ) break
console . log ( ` [DIAG_v2] Init decode segment ${ i } ... ` )
try {
await decodeSegmentBatch ( i , 60 )
console . log ( ` [DIAG_v2] Init decode segment ${ i } done ` )
} catch ( e ) {
console . warn ( ` [useCanvasPlayer] 初始化解码片段 ${ i } 失败: ` , e )
}
if ( cancelled ) break
}
console . log ( ` [useCanvasPlayer] Init decode finished, cancelled: ` , cancelled )
if ( ! cancelled ) {
console . log ( "[useCanvasPlayer] Init complete, isReady = true" )
console . log ( "[useCanvasPlayer] Init complete, isReady = true, duration:" , totalDuration )
setState ( ( s ) = > ( { . . . s , duration : totalDuration , isReady : true , isBuffering : false } ) )
} else {
console . warn ( "[useCanvasPlayer] Init was cancelled before completion" )
}
}