1const cacheActiveFrameList = new Map(); 2 3Promise.create = function () { 4 let resolve = null; 5 let reject = null; 6 const promise = new Promise((res, rej) => { 7 resolve = res; 8 reject = rej; 9 }); 10 11 promise.resolve = resolve; 12 promise.reject = reject; 13 return promise; 14}; 15 16window.ActiveFrame = class ActiveFrame { 17 file = null; 18 manifest = null; 19 data = null; 20 decoder = null; 21 frame = null; 22 desideredFrame = 0; 23 enabled = true; 24 framesByTimestamp = new Map(); 25 frameProcessed = null; 26 27 constructor(file, { 28 process = () => {}, 29 // texture = null, 30 hardwareAcceleration = 'prefer-hardware' 31 }) { 32 this.loading = Promise.create(); 33 this.process = process; 34 // this.texture = texture; 35 this.hardwareAcceleration = hardwareAcceleration; 36 37 this.file = file; 38 this.init(); 39 } 40 41 async init() { 42 cacheActiveFrameList.set(this.file, this.loadBinary(this.file)); 43 44 const loading = await cacheActiveFrameList.get(this.file); 45 const { manifest, data } = loading; 46 47 this.manifest = manifest; 48 this.data = data; 49 50 this.manifest.frames.forEach(frame => { 51 // preallocate data view for faster access 52 frame.data = new Uint8Array(this.data, frame.o, frame.l); 53 this.framesByTimestamp.set(frame.t, frame.i); 54 }); 55 56 await this.initDecoder(); 57 this.loading.resolve(); 58 } 59 60 async loadBinary(file) { 61 const res = await fetch(file); 62 const fullBuffer = await res.arrayBuffer(); 63 64 // Last 4 bytes = LE uint32 offset where JSON starts 65 const footer = new DataView(fullBuffer, fullBuffer.byteLength - 4); 66 const manifestOffset = footer.getUint32(0, true); 67 68 const manifestBytes = new Uint8Array(fullBuffer, manifestOffset, fullBuffer.byteLength - 4 - manifestOffset); 69 const manifest = JSON.parse(new TextDecoder().decode(manifestBytes)); 70 71 return { 72 manifest, 73 data: fullBuffer 74 }; 75 } 76 77 decodeDescription(description) { 78 const binaryString = atob(description); 79 const bytes = new Uint8Array(binaryString.length); 80 for (let i = 0; i < binaryString.length; i++) { 81 bytes[i] = binaryString.charCodeAt(i); 82 } 83 return bytes; 84 } 85 86 async initDecoder() { 87 const baseConfig = { 88 codec: this.manifest.codec, 89 codedWidth: this.manifest.width, 90 codedHeight: this.manifest.height, 91 colorSpace: { 92 primaries: 'bt709', 93 transfer: 'bt709', 94 matrix: 'bt709', 95 fullRange: false 96 }, 97 description: this.decodeDescription(this.manifest.description) 98 }; 99 100 const candidates = [ 101 { ...baseConfig, hardwareAcceleration: this.hardwareAcceleration, optimizeForLatency: true }, 102 { ...baseConfig, hardwareAcceleration: this.hardwareAcceleration }, 103 { ...baseConfig, optimizeForLatency: true }, 104 { ...baseConfig } 105 ]; 106 107 this.config = null; 108 109 for (const candidate of candidates) { 110 const support = await VideoDecoder.isConfigSupported(candidate); 111 if (support.supported) { 112 this.config = candidate; 113 break; 114 } 115 } 116 117 if (!this.config) { 118 throw new Error('Decoder not supported'); 119 } 120 121 this.decoder = new VideoDecoder({ 122 output: this.outputFrame.bind(this), 123 error: (e) => { 124 console.log(this.file); 125 console.log(this.config); 126 console.error('Decoder error:', e); 127 } 128 }); 129 130 // this.decoder.reset(); 131 this.decoder.configure(this.config); 132 } 133 134 async outputFrame(frame) { 135 if (!this.enabled) { 136 frame.close(); 137 return; 138 }; 139 140 const timestampToFrameId = this.framesByTimestamp.get(frame.timestamp); 141 142 if (this.desideredFrame !== timestampToFrameId) { 143 frame.close(); 144 return; 145 } 146 147 this.frame = timestampToFrameId; 148 149 if (this.process) { 150 await this.process(frame); 151 } 152 153 // if (this.texture) { 154 // this.texture.image = frame; 155 // Texture.renderer.manualUpdateDynamic(this.texture); 156 // } 157 158 this.frameProcessed = timestampToFrameId; 159 160 frame.close(); 161 } 162
163 setFrame(desideredFrame) { 164 if (!this.manifest) return; 165 if (!this.enabled) return; 166 167 desideredFrame = Math.round(Number(desideredFrame)); 168 const maxFrame = Math.max(0, this.manifest.totalFrames - 1); 169 desideredFrame = Math.min(Math.max(desideredFrame, 0), maxFrame); 170 this.desideredFrame = desideredFrame; 171 172 if (this.desideredFrame === this.frame) return; 173 if (this.desideredFrame === this._pendingFrame) return; 174 175 this._pendingFrame = desideredFrame; 176 177 const frameMeta = this.manifest.frames[this.desideredFrame]; 178 179 if (!frameMeta) { 180 return; 181 } 182 183 const isSequential = this.frame !== null 184 && this.desideredFrame === this.frame + 1 185 && frameMeta.ty === 'delta'; 186 187 if (isSequential) { 188 this.decoder.decode(new EncodedVideoChunk({ 189 type: frameMeta.ty, 190 timestamp: frameMeta.t, 191 data: frameMeta.data 192 })); 193 return; 194 } 195 196 if (this.decoder.decodeQueueSize > 0 || this.decoder.state !== 'configured') { 197 this.decoder.reset(); 198 this.decoder.configure(this.config); 199 } 200 201 if (frameMeta.ty === 'key') { 202 this.decoder.decode(new EncodedVideoChunk({ 203 type: frameMeta.ty, 204 timestamp: frameMeta.t, 205 data: frameMeta.data 206 })); 207 208 } else { 209 let keyFrame = null; 210 for (let i = this.desideredFrame; i >= 0; i--) { 211 const f = this.manifest.frames[i]; 212 if (f.ty === 'key') { 213 keyFrame = f; 214 break; 215 } 216 } 217 218 if (!keyFrame || !keyFrame.data) { 219 console.error('No key frame found'); 220 return; 221 } 222 223 this.decoder.decode(new EncodedVideoChunk({ 224 type: keyFrame.ty, 225 timestamp: keyFrame.t, 226 data: keyFrame.data 227 })); 228 229 for (let i = keyFrame.i + 1; i <= this.desideredFrame; i++) { 230 const f = this.manifest.frames[i]; 231 if (f.ty === 'delta') { 232 this.decoder.decode(new EncodedVideoChunk({ 233 type: f.ty, 234 timestamp: f.t, 235 data: f.data 236 })); 237 } else { 238 break; 239 } 240 } 241 } 242 } 243 244 destroy() { 245 cacheActiveFrameList.delete(this.file); 246 this.stop(); 247 this.decoder.close(); 248 this.decoder = null; 249 this.data = null; 250 this.manifest = null; 251 this.file = null; 252 // this.texture?.destroy?.(); 253 // this.texture = null; 254 this.process = null; 255 this.frameProcessed = null; 256 this.enabled = false; 257 this.framesByTimestamp.clear(); 258 } 259}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.