movi-player 0.1.3 → 0.1.5-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -3
- package/dist/core/MoviPlayer.d.ts +4 -4
- package/dist/core/MoviPlayer.d.ts.map +1 -1
- package/dist/core/MoviPlayer.js +94 -145
- package/dist/core/MoviPlayer.js.map +1 -1
- package/dist/demuxer.cjs +106 -64
- package/dist/demuxer.js +111 -69
- package/dist/element.cjs +7515 -7220
- package/dist/element.js +1157 -862
- package/dist/index.cjs +7515 -7220
- package/dist/index.js +1157 -862
- package/dist/movi-sw.js +40 -0
- package/dist/player.cjs +311 -269
- package/dist/player.js +311 -269
- package/dist/render/AudioRenderer.d.ts +5 -0
- package/dist/render/AudioRenderer.d.ts.map +1 -1
- package/dist/render/AudioRenderer.js +54 -9
- package/dist/render/AudioRenderer.js.map +1 -1
- package/dist/render/CanvasRenderer.d.ts +1 -0
- package/dist/render/CanvasRenderer.d.ts.map +1 -1
- package/dist/render/CanvasRenderer.js +46 -11
- package/dist/render/CanvasRenderer.js.map +1 -1
- package/dist/render/MoviElement.d.ts +42 -0
- package/dist/render/MoviElement.d.ts.map +1 -1
- package/dist/render/MoviElement.js +759 -158
- package/dist/render/MoviElement.js.map +1 -1
- package/dist/source/HttpSource.d.ts +1 -1
- package/dist/source/HttpSource.d.ts.map +1 -1
- package/dist/source/HttpSource.js +189 -133
- package/dist/source/HttpSource.js.map +1 -1
- package/dist/source/ThumbnailHttpSource.d.ts.map +1 -1
- package/dist/source/ThumbnailHttpSource.js +74 -39
- package/dist/source/ThumbnailHttpSource.js.map +1 -1
- package/dist/utils/service-worker.d.ts +33 -0
- package/dist/utils/service-worker.d.ts.map +1 -0
- package/dist/utils/service-worker.js +184 -0
- package/dist/utils/service-worker.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,14 +7,59 @@
|
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
[](https://caniuse.com/webcodecs)
|
|
9
9
|
|
|
10
|
-
👉 **Install:** `npm i movi-player`
|
|
11
|
-
👉 **Docs:** [mrujjwalg.github.io/movi-player](https://mrujjwalg.github.io/movi-player/)
|
|
10
|
+
👉 **Install:** `npm i movi-player`
|
|
11
|
+
👉 **Docs:** [mrujjwalg.github.io/movi-player](https://mrujjwalg.github.io/movi-player/)
|
|
12
12
|
👉 **Demo:** [movi-player-examples.vercel.app](https://movi-player-examples.vercel.app/element.html)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
### Element Module (Full UI)
|
|
15
|
+
|
|
16
|
+

|
|
17
|
+
|
|
18
|
+
👉 [View Live Demo](https://movi-player-examples.vercel.app/element.html) | [See Code Example](https://github.com/mrujjwalg/movi-player-examples/blob/main/element.html)
|
|
19
|
+
|
|
20
|
+
### Player Module (Custom UI)
|
|
21
|
+
|
|
22
|
+

|
|
23
|
+
|
|
24
|
+
👉 [View Live Demo](https://movi-player-examples.vercel.app/youtube.html) | [See Code Example](https://github.com/mrujjwalg/movi-player-examples/blob/main/youtube.html)
|
|
15
25
|
|
|
16
26
|
> 🚀 **No Server-Side Processing Required!** — All video parsing, demuxing, and decoding happens entirely in the browser using FFmpeg WASM & WebCodecs. Multiple audio/subtitle tracks are supported without any conversion or processing!
|
|
17
27
|
|
|
28
|
+
## ⚠️ Important: CORS & Headers
|
|
29
|
+
|
|
30
|
+
Because Movi-Player uses **WebAssembly** and **SharedArrayBuffer** for high-performance streaming, your server needs to support:
|
|
31
|
+
|
|
32
|
+
1. **Range Requests:** Required for seeking in large files.
|
|
33
|
+
2. **CORS Headers:** If your video is on a different domain.
|
|
34
|
+
3. **COI Headers (Optional):** For maximum performance (Zero-copy), set:
|
|
35
|
+
- `Cross-Origin-Opener-Policy: same-origin`
|
|
36
|
+
- `Cross-Origin-Embedder-Policy: require-corp`
|
|
37
|
+
|
|
38
|
+
**Can't modify server headers?** Use a **Service Worker** to inject COI headers client-side:
|
|
39
|
+
|
|
40
|
+
```javascript
|
|
41
|
+
// sw.js
|
|
42
|
+
self.addEventListener('fetch', (event) => {
|
|
43
|
+
event.respondWith(
|
|
44
|
+
fetch(event.request).then((response) => {
|
|
45
|
+
const newHeaders = new Headers(response.headers);
|
|
46
|
+
newHeaders.set('Cross-Origin-Embedder-Policy', 'require-corp');
|
|
47
|
+
newHeaders.set('Cross-Origin-Opener-Policy', 'same-origin');
|
|
48
|
+
return new Response(response.body, {
|
|
49
|
+
status: response.status,
|
|
50
|
+
statusText: response.statusText,
|
|
51
|
+
headers: newHeaders,
|
|
52
|
+
});
|
|
53
|
+
})
|
|
54
|
+
);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Register in your app
|
|
58
|
+
if ('serviceWorker' in navigator) {
|
|
59
|
+
navigator.serviceWorker.register('/sw.js');
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
18
63
|
---
|
|
19
64
|
|
|
20
65
|
## ⚡ TL;DR
|
|
@@ -199,6 +244,8 @@ fileInput.addEventListener("change", async (e) => {
|
|
|
199
244
|
|
|
200
245
|

|
|
201
246
|
|
|
247
|
+
👉 [View Live Demo](https://movi-player-examples.vercel.app/demuxer.html) | [See Code Example](https://github.com/mrujjwalg/movi-player-examples/blob/main/demuxer.html)
|
|
248
|
+
|
|
202
249
|
```typescript
|
|
203
250
|
import { Demuxer, HttpSource, FileSource } from "movi-player/demuxer";
|
|
204
251
|
|
|
@@ -245,6 +292,8 @@ Movi-Player's modular design makes it perfect for a wide range of applications:
|
|
|
245
292
|
|
|
246
293
|
### Demuxer Module (50KB)
|
|
247
294
|
|
|
295
|
+
👉 [View Live Demo](https://movi-player-examples.vercel.app/demuxer.html) | [See Code Example](https://github.com/mrujjwalg/movi-player-examples/blob/main/demuxer.html)
|
|
296
|
+
|
|
248
297
|
- **Media Asset Management**: Catalog video libraries without playing files
|
|
249
298
|
- **Video Validators**: Check uploaded files against platform requirements
|
|
250
299
|
- **HDR Detection**: Automatically tag HDR content in video pipelines
|
|
@@ -254,6 +303,10 @@ Movi-Player's modular design makes it perfect for a wide range of applications:
|
|
|
254
303
|
|
|
255
304
|
### Player Module (180KB)
|
|
256
305
|
|
|
306
|
+

|
|
307
|
+
|
|
308
|
+
👉 [View Live Demo](https://movi-player-examples.vercel.app/youtube.html) | [See Code Example](https://github.com/mrujjwalg/movi-player-examples/blob/main/youtube.html)
|
|
309
|
+
|
|
257
310
|
- **Custom Video Players**: Build branded players with custom UI
|
|
258
311
|
- **Educational Platforms**: Interactive learning videos with quiz overlays
|
|
259
312
|
- **Multi-Language Platforms**: Netflix-style audio/subtitle switching
|
|
@@ -557,6 +610,13 @@ The `<movi-player>` element supports standard video attributes plus enhancements
|
|
|
557
610
|
renderer="canvas" <!-- canvas | mse -->
|
|
558
611
|
sw="auto" <!-- auto (default) | true | false -->
|
|
559
612
|
fps="0" <!-- Custom frame rate override -->
|
|
613
|
+
gesturefs <!-- Gestures only in fullscreen -->
|
|
614
|
+
nohotkeys <!-- Disable keyboard shortcuts -->
|
|
615
|
+
startat="0" <!-- Start playback at time (seconds) -->
|
|
616
|
+
fastseek <!-- Enable ±10s skip controls -->
|
|
617
|
+
doubletap="true" <!-- Enable double-tap to seek -->
|
|
618
|
+
themecolor="#4CAF50" <!-- Custom theme color -->
|
|
619
|
+
buffersize="0" <!-- Buffer size in seconds (0=auto) -->
|
|
560
620
|
></movi-player>
|
|
561
621
|
```
|
|
562
622
|
|
|
@@ -30,6 +30,7 @@ export declare class MoviPlayer extends EventEmitter<PlayerEventMap> {
|
|
|
30
30
|
private previewInitPromise;
|
|
31
31
|
private disableAudio;
|
|
32
32
|
private muted;
|
|
33
|
+
private wasPlayingBeforeRebuffer;
|
|
33
34
|
private animationFrameId;
|
|
34
35
|
private wakeLock;
|
|
35
36
|
private seekingToKeyframe;
|
|
@@ -71,11 +72,10 @@ export declare class MoviPlayer extends EventEmitter<PlayerEventMap> {
|
|
|
71
72
|
private static readonly DEMUX_TIMEOUT;
|
|
72
73
|
private eofReached;
|
|
73
74
|
/**
|
|
74
|
-
*
|
|
75
|
-
* Clears the seek flag, synchronizes
|
|
76
|
-
* and flushes any buffered audio packets to start playback in sync.
|
|
75
|
+
* Internal handler for seek completion when first target frame is found.
|
|
76
|
+
* Clears the seek flag, synchronizes clock, and transitions to final state.
|
|
77
77
|
*/
|
|
78
|
-
private
|
|
78
|
+
private notifySeekCompletion;
|
|
79
79
|
/**
|
|
80
80
|
* Main Playback Loop
|
|
81
81
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MoviPlayer.d.ts","sourceRoot":"","sources":["../../src/core/MoviPlayer.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,YAAY,EACZ,KAAK,EACL,WAAW,EACX,cAAc,EACd,SAAS,EACT,UAAU,EACV,UAAU,EACV,aAAa,EACd,MAAM,UAAU,CAAC;AAClB,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAIL,KAAK,aAAa,EACnB,MAAM,WAAW,CAAC;AAGnB,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG9C,OAAO,EAAU,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAanD,qBAAa,UAAW,SAAQ,YAAY,CAAC,cAAc,CAAC;IAC1D,OAAO,CAAC,MAAM,CAAe;IAC7B,OAAO,CAAC,MAAM,CAA8B;IAC5C,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,OAAO,CAAwB;IAChC,YAAY,EAAE,YAAY,CAAC;IAClC,OAAO,CAAC,KAAK,CAAQ;IACrB,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,SAAS,CAA0B;IAC3C,OAAO,CAAC,QAAQ,CAAc;IAG9B,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,YAAY,CAAmB;IACvC,OAAO,CAAC,eAAe,CAAgC;IACvD,OAAO,CAAC,aAAa,CAA+B;IAGpD,OAAO,CAAC,UAAU,CAAiC;IAGnD,OAAO,CAAC,iBAAiB,CAAkC;IAC3D,OAAO,CAAC,eAAe,CAA8B;IACrD,OAAO,CAAC,iBAAiB,CAAkC;IAC3D,OAAO,CAAC,mBAAmB,CAAiB;IAC5C,OAAO,CAAC,mBAAmB,CAAkB;IAC7C,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,kBAAkB,CAA8B;IAGxD,OAAO,CAAC,YAAY,CAAkB;IACtC,OAAO,CAAC,KAAK,CAAkB;
|
|
1
|
+
{"version":3,"file":"MoviPlayer.d.ts","sourceRoot":"","sources":["../../src/core/MoviPlayer.ts"],"names":[],"mappings":"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"}
|
package/dist/core/MoviPlayer.js
CHANGED
|
@@ -47,6 +47,7 @@ export class MoviPlayer extends EventEmitter {
|
|
|
47
47
|
// Debug flag to disable audio processing
|
|
48
48
|
disableAudio = false; // Set to true to disable audio for debugging
|
|
49
49
|
muted = false; // Mute state
|
|
50
|
+
wasPlayingBeforeRebuffer = false; // Track if we were playing before entering rebuffering state
|
|
50
51
|
// Playback Loop
|
|
51
52
|
animationFrameId = null;
|
|
52
53
|
// WakeLock to prevent screen sleep during playback
|
|
@@ -136,9 +137,9 @@ export class MoviPlayer extends EventEmitter {
|
|
|
136
137
|
frame.close();
|
|
137
138
|
return;
|
|
138
139
|
}
|
|
139
|
-
// Video reached target! Clear the flag to ensure sync
|
|
140
|
+
// Video reached target! Clear the flag to ensure sync and transition to final state
|
|
140
141
|
if (this.seekTargetTime >= 0) {
|
|
141
|
-
this.
|
|
142
|
+
this.notifySeekCompletion(frameTime);
|
|
142
143
|
}
|
|
143
144
|
this.videoRenderer.queueFrame(frame);
|
|
144
145
|
}
|
|
@@ -569,38 +570,58 @@ export class MoviPlayer extends EventEmitter {
|
|
|
569
570
|
static DEMUX_TIMEOUT = 10000; // 10 seconds timeout for demux operations
|
|
570
571
|
eofReached = false;
|
|
571
572
|
/**
|
|
572
|
-
*
|
|
573
|
-
* Clears the seek flag, synchronizes
|
|
574
|
-
* and flushes any buffered audio packets to start playback in sync.
|
|
573
|
+
* Internal handler for seek completion when first target frame is found.
|
|
574
|
+
* Clears the seek flag, synchronizes clock, and transitions to final state.
|
|
575
575
|
*/
|
|
576
|
-
|
|
576
|
+
notifySeekCompletion(time) {
|
|
577
|
+
if (!this.waitingForVideoSync)
|
|
578
|
+
return;
|
|
577
579
|
const seekTarget = this.seekTargetTime;
|
|
578
580
|
this.seekTargetTime = -1;
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
581
|
+
this.waitingForVideoSync = false;
|
|
582
|
+
this.seekingToKeyframe = false; // Also clear keyframe skip flag
|
|
583
|
+
Logger.debug(TAG, `Seek completion at ${time.toFixed(3)}s (target: ${seekTarget.toFixed(3)}s)`);
|
|
584
|
+
// Sync correction: Match clock to actual video/audio start time
|
|
585
|
+
// Allow small tolerance (0.01) to ensure the frame is displayed (clock >= frameTime)
|
|
586
|
+
if (time > seekTarget + 0.01) {
|
|
587
|
+
Logger.debug(TAG, `Stream jumped ahead. Syncing clock to ${time.toFixed(3)}s.`);
|
|
588
|
+
this.clock.seek(time);
|
|
589
|
+
// Filter pending audio packets that are now too old
|
|
590
|
+
this.pendingAudioPackets = this.pendingAudioPackets.filter((p) => p.timestamp >= time - 0.05);
|
|
591
|
+
}
|
|
592
|
+
// Flush any buffered audio packets
|
|
593
|
+
if (this.pendingAudioPackets.length > 0) {
|
|
594
|
+
Logger.debug(TAG, `Flushing ${this.pendingAudioPackets.length} buffered audio packets after seek sync`);
|
|
595
|
+
for (const pkt of this.pendingAudioPackets) {
|
|
596
|
+
this.audioDecoder.decode(pkt.data, pkt.timestamp, pkt.keyframe);
|
|
588
597
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
598
|
+
this.pendingAudioPackets = [];
|
|
599
|
+
}
|
|
600
|
+
// Transition to final state
|
|
601
|
+
if (this.wasPlayingBeforeSeek) {
|
|
602
|
+
Logger.info(TAG, "Resuming playback after seek");
|
|
603
|
+
this.stateManager.setState("playing");
|
|
604
|
+
this.clock.start();
|
|
605
|
+
if (!this.disableAudio && !this.audioRenderer.isAudioPlaying()) {
|
|
606
|
+
this.audioRenderer.play();
|
|
595
607
|
}
|
|
596
608
|
}
|
|
609
|
+
else {
|
|
610
|
+
Logger.info(TAG, "Seek completed in paused state");
|
|
611
|
+
this.stateManager.setState("ready");
|
|
612
|
+
// Don't start clock or audio
|
|
613
|
+
}
|
|
614
|
+
// Emit seeked event now that we are actually ready
|
|
615
|
+
// Convert back from media time to UI time
|
|
616
|
+
this.emit("seeked", Math.max(0, time - this.startTime));
|
|
597
617
|
}
|
|
598
618
|
/**
|
|
599
619
|
* Main Playback Loop
|
|
600
620
|
*/
|
|
601
621
|
processLoop = async () => {
|
|
602
|
-
|
|
603
|
-
if (
|
|
622
|
+
const currentState = this.stateManager.getState();
|
|
623
|
+
// Run if playing OR buffering (for rebuffering) OR if we are resolving a seek (fetching target frame)
|
|
624
|
+
if (currentState !== "playing" && currentState !== "buffering" && !this.waitingForVideoSync)
|
|
604
625
|
return;
|
|
605
626
|
// Capture session ID at start of loop - if a new seek starts, this loop should abort
|
|
606
627
|
const currentSessionId = this.seekSessionId;
|
|
@@ -612,6 +633,29 @@ export class MoviPlayer extends EventEmitter {
|
|
|
612
633
|
Logger.debug(TAG, "ProcessLoop aborted: new seek started");
|
|
613
634
|
return;
|
|
614
635
|
}
|
|
636
|
+
// Check if audio is rebuffering due to playback rate change
|
|
637
|
+
if (!this.disableAudio && this.audioRenderer.isRebuffering()) {
|
|
638
|
+
// Enter buffering state and pause clock until rebuffering completes
|
|
639
|
+
const currentState = this.stateManager.getState();
|
|
640
|
+
if (currentState === "playing") {
|
|
641
|
+
this.wasPlayingBeforeRebuffer = true;
|
|
642
|
+
this.stateManager.setState("buffering");
|
|
643
|
+
this.clock.pause();
|
|
644
|
+
Logger.debug(TAG, "Entered buffering state for playback rate change");
|
|
645
|
+
}
|
|
646
|
+
// Continue processing to allow new audio to be decoded and scheduled
|
|
647
|
+
}
|
|
648
|
+
else if (this.stateManager.getState() === "buffering" && this.wasPlayingBeforeRebuffer) {
|
|
649
|
+
// Rebuffering complete, resume playback only if we were playing before
|
|
650
|
+
// Transition to paused first, then let play() method handle the transition to playing
|
|
651
|
+
this.stateManager.setState("paused");
|
|
652
|
+
this.wasPlayingBeforeRebuffer = false;
|
|
653
|
+
Logger.debug(TAG, "Exited buffering state, resuming playback");
|
|
654
|
+
// Resume playback by calling play() method (proper state machine transition)
|
|
655
|
+
this.play().catch((err) => {
|
|
656
|
+
Logger.error(TAG, "Failed to resume playback after rebuffering:", err);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
615
659
|
// Update FileSource preload position based on current time
|
|
616
660
|
if (this.source instanceof FileSource && this.mediaInfo) {
|
|
617
661
|
const currentTime = this.clock.getTime();
|
|
@@ -769,6 +813,11 @@ export class MoviPlayer extends EventEmitter {
|
|
|
769
813
|
this.seekingToKeyframe = false;
|
|
770
814
|
Logger.warn(TAG, "EOF reached before finding keyframe after seek");
|
|
771
815
|
}
|
|
816
|
+
// If we were waiting for sync, trigger it now so player doesn't hang in loading state
|
|
817
|
+
if (this.waitingForVideoSync) {
|
|
818
|
+
Logger.warn(TAG, "EOF reached while waiting for seek sync, forcing completion");
|
|
819
|
+
this.notifySeekCompletion(this.seekTargetTime);
|
|
820
|
+
}
|
|
772
821
|
Logger.debug(TAG, "EOF reached");
|
|
773
822
|
break;
|
|
774
823
|
}
|
|
@@ -829,10 +878,9 @@ export class MoviPlayer extends EventEmitter {
|
|
|
829
878
|
if (this.seekTargetTime >= 0 &&
|
|
830
879
|
packet.timestamp >= this.seekTargetTime) {
|
|
831
880
|
Logger.debug(TAG, `Audio reached seek target: ${packet.timestamp.toFixed(3)}s (target: ${this.seekTargetTime.toFixed(3)}s)`);
|
|
832
|
-
//
|
|
833
|
-
// If video exists, we wait for a valid frame in onFrame()
|
|
881
|
+
// If there is no video track, audio completion triggers state transition
|
|
834
882
|
if (!this.trackManager.getActiveVideoTrack()) {
|
|
835
|
-
this.
|
|
883
|
+
this.notifySeekCompletion(packet.timestamp);
|
|
836
884
|
}
|
|
837
885
|
}
|
|
838
886
|
this.audioDecoder.decode(packet.data, packet.timestamp, packet.keyframe);
|
|
@@ -931,6 +979,8 @@ export class MoviPlayer extends EventEmitter {
|
|
|
931
979
|
if (currentState !== "seeking") {
|
|
932
980
|
this.wasPlayingBeforeSeek = currentState === "playing";
|
|
933
981
|
}
|
|
982
|
+
// Pause clock so UI time doesn't advance during seek while in loading state
|
|
983
|
+
this.clock.pause();
|
|
934
984
|
const mySessionId = ++this.seekSessionId;
|
|
935
985
|
this.stateManager.setState("seeking");
|
|
936
986
|
this.emit("seeking", seconds);
|
|
@@ -991,57 +1041,14 @@ export class MoviPlayer extends EventEmitter {
|
|
|
991
1041
|
this.seekTime = performance.now();
|
|
992
1042
|
if (this.seekSessionId !== mySessionId)
|
|
993
1043
|
return; // Superceded
|
|
994
|
-
//
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
if (!this.stateManager.setState("playing")) {
|
|
1001
|
-
Logger.warn(TAG, "Failed to transition to playing after seek, transitioning to ready first");
|
|
1002
|
-
// Fallback: try through ready
|
|
1003
|
-
if (this.stateManager.setState("ready")) {
|
|
1004
|
-
this.stateManager.setState("playing");
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
else if (currentState === "ready") {
|
|
1009
|
-
// ready -> playing is valid
|
|
1010
|
-
this.stateManager.setState("playing");
|
|
1011
|
-
}
|
|
1012
|
-
else {
|
|
1013
|
-
Logger.warn(TAG, `Cannot transition to playing from state: ${currentState} after seek`);
|
|
1014
|
-
}
|
|
1015
|
-
// Ensure clock is running (restores isRunning=true)
|
|
1016
|
-
this.clock.start();
|
|
1017
|
-
// Ensure audio engine is ready (reset() might have stopped it or cleared clock)
|
|
1018
|
-
if (!this.disableAudio && !this.audioRenderer.isAudioPlaying()) {
|
|
1019
|
-
await this.audioRenderer.play();
|
|
1020
|
-
}
|
|
1021
|
-
// IMPORTANT: Restart video presentation loop explicitly
|
|
1022
|
-
if (this.videoRenderer) {
|
|
1023
|
-
this.videoRenderer.startPresentationLoop();
|
|
1024
|
-
}
|
|
1025
|
-
if (this.animationFrameId !== null) {
|
|
1026
|
-
cancelAnimationFrame(this.animationFrameId);
|
|
1027
|
-
this.animationFrameId = null;
|
|
1028
|
-
}
|
|
1029
|
-
this.processLoop();
|
|
1030
|
-
}
|
|
1031
|
-
else {
|
|
1032
|
-
// PAUSED SEEK LOGIC
|
|
1033
|
-
// We still need to fetch and decode the frame at the new position
|
|
1034
|
-
this.stateManager.setState("ready");
|
|
1035
|
-
// Temporarily start processLoop to fetch the target frame
|
|
1036
|
-
// It will stop automatically once waitingForVideoSync becomes false (see processLoop check)
|
|
1037
|
-
this.processLoop();
|
|
1038
|
-
// Ensure the video renderer loop is running to actually draw the frame
|
|
1039
|
-
if (this.videoRenderer) {
|
|
1040
|
-
this.videoRenderer.startPresentationLoop();
|
|
1041
|
-
}
|
|
1044
|
+
// Start processing loop to find and decode the target frame/packet.
|
|
1045
|
+
// notifySeekCompletion will be called once the first valid frame is received.
|
|
1046
|
+
this.processLoop();
|
|
1047
|
+
// Ensure the video renderer loop is running to actually draw frames as they arrive
|
|
1048
|
+
if (this.videoRenderer) {
|
|
1049
|
+
this.videoRenderer.startPresentationLoop();
|
|
1042
1050
|
}
|
|
1043
|
-
|
|
1044
|
-
Logger.info(TAG, `Seeked to ${seconds}s`);
|
|
1051
|
+
Logger.info(TAG, `Seek initiated to ${seconds}s, waiting for sync...`);
|
|
1045
1052
|
}
|
|
1046
1053
|
catch (error) {
|
|
1047
1054
|
// Reset seeking flag on error
|
|
@@ -1902,41 +1909,16 @@ export class MoviPlayer extends EventEmitter {
|
|
|
1902
1909
|
if (duration <= 0) {
|
|
1903
1910
|
return 0;
|
|
1904
1911
|
}
|
|
1905
|
-
// For HttpSource, convert buffered bytes to time
|
|
1912
|
+
// For HttpSource, convert buffered bytes to time using stable linear estimation.
|
|
1913
|
+
// Transient bitrate-based estimation is unstable during seeks.
|
|
1906
1914
|
if (this.source instanceof HttpSource && this.fileSize > 0) {
|
|
1907
1915
|
const bufferedBytes = this.source.getBufferedEnd();
|
|
1908
1916
|
if (bufferedBytes > 0) {
|
|
1909
|
-
// Use current read position as reference for more accurate conversion
|
|
1910
|
-
const currentReadPos = this.source.getPosition();
|
|
1911
|
-
const currentTime = this.clock.getTime();
|
|
1912
|
-
// Require minimum thresholds to ensure accurate bitrate calculation
|
|
1913
|
-
const MIN_READ_POS = 1024 * 1024; // At least 1MB read
|
|
1914
|
-
const MIN_TIME = 1.0; // At least 1 second of playback
|
|
1915
|
-
// If we have a valid read position and current time, use them as a reference point
|
|
1916
|
-
if (currentReadPos >= MIN_READ_POS &&
|
|
1917
|
-
currentTime >= MIN_TIME &&
|
|
1918
|
-
currentReadPos < this.fileSize) {
|
|
1919
|
-
// Calculate effective bitrate from current position and time
|
|
1920
|
-
const normalizedTime = Math.max(0.1, currentTime - this.startTime);
|
|
1921
|
-
const effectiveBitrate = currentReadPos / normalizedTime; // bytes per second
|
|
1922
|
-
if (effectiveBitrate > 0) {
|
|
1923
|
-
// Estimate time for buffered end based on effective bitrate
|
|
1924
|
-
const estimatedTime = bufferedBytes / effectiveBitrate;
|
|
1925
|
-
// Clamp to valid range
|
|
1926
|
-
return Math.max(0, Math.min(duration, estimatedTime));
|
|
1927
|
-
}
|
|
1928
|
-
}
|
|
1929
|
-
// Fallback: Account for metadata overhead (first 1-2% is often metadata)
|
|
1930
|
-
const metadataOverhead = Math.min(this.fileSize * 0.02, 2 * 1024 * 1024);
|
|
1931
|
-
const effectiveFileSize = this.fileSize - metadataOverhead;
|
|
1932
|
-
const effectiveBufferedBytes = Math.max(0, bufferedBytes - metadataOverhead);
|
|
1933
|
-
if (effectiveFileSize > 0 && effectiveBufferedBytes >= 0) {
|
|
1934
|
-
const ratio = Math.min(1, effectiveBufferedBytes / effectiveFileSize);
|
|
1935
|
-
return ratio * duration;
|
|
1936
|
-
}
|
|
1937
|
-
// Last resort: simple linear
|
|
1938
1917
|
const ratio = Math.min(1, bufferedBytes / this.fileSize);
|
|
1939
|
-
|
|
1918
|
+
const bufferedTime = ratio * duration;
|
|
1919
|
+
// Ensure buffer appears at least as far as current playback position
|
|
1920
|
+
// This prevents buffer bar from appearing behind progress bar
|
|
1921
|
+
return Math.max(bufferedTime, this.getCurrentTime());
|
|
1940
1922
|
}
|
|
1941
1923
|
}
|
|
1942
1924
|
// For FileSource, the entire file is buffered
|
|
@@ -1983,46 +1965,13 @@ export class MoviPlayer extends EventEmitter {
|
|
|
1983
1965
|
return 0;
|
|
1984
1966
|
}
|
|
1985
1967
|
const duration = this.mediaInfo.duration;
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
if (bufferStartBytes < 0) {
|
|
1991
|
-
return 0;
|
|
1992
|
-
}
|
|
1993
|
-
// Use current read position as reference for more accurate conversion
|
|
1994
|
-
// This accounts for metadata at the beginning (moov atom) which takes bytes but no playback time
|
|
1995
|
-
const currentReadPos = this.source.getPosition();
|
|
1996
|
-
const currentTime = this.clock.getTime();
|
|
1997
|
-
// If we have a valid read position and current time, use them as a reference point
|
|
1998
|
-
// Require minimum thresholds to ensure accurate bitrate calculation
|
|
1999
|
-
const MIN_READ_POS = 1024 * 1024; // At least 1MB read
|
|
2000
|
-
const MIN_TIME = 1.0; // At least 1 second of playback
|
|
2001
|
-
if (currentReadPos >= MIN_READ_POS &&
|
|
2002
|
-
currentTime >= MIN_TIME &&
|
|
2003
|
-
currentReadPos < this.fileSize) {
|
|
2004
|
-
// Calculate effective bitrate from current position and time
|
|
2005
|
-
const effectiveBitrate = currentReadPos / currentTime; // bytes per second
|
|
2006
|
-
if (effectiveBitrate > 0) {
|
|
2007
|
-
// Estimate time for buffer start based on effective bitrate
|
|
2008
|
-
// This is more accurate than linear file ratio
|
|
2009
|
-
const estimatedTime = bufferStartBytes / effectiveBitrate;
|
|
2010
|
-
// Clamp to valid range
|
|
2011
|
-
return Math.max(0, Math.min(duration, estimatedTime));
|
|
2012
|
-
}
|
|
2013
|
-
}
|
|
2014
|
-
// Fallback to linear estimation if we don't have a good reference point
|
|
2015
|
-
// Account for typical metadata overhead (first 1-2% of file is often metadata)
|
|
2016
|
-
const metadataOverhead = Math.min(this.fileSize * 0.02, 2 * 1024 * 1024); // Max 2MB or 2%
|
|
2017
|
-
const effectiveFileSize = this.fileSize - metadataOverhead;
|
|
2018
|
-
const effectiveStartBytes = Math.max(0, bufferStartBytes - metadataOverhead);
|
|
2019
|
-
if (effectiveFileSize > 0 && effectiveStartBytes >= 0) {
|
|
2020
|
-
const ratio = Math.min(1, effectiveStartBytes / effectiveFileSize);
|
|
1968
|
+
// For HttpSource, convert buffer start bytes to time using stable linear estimation
|
|
1969
|
+
if (this.source instanceof HttpSource && this.fileSize > 0) {
|
|
1970
|
+
const bufferStartBytes = this.source.getBufferStart();
|
|
1971
|
+
const ratio = Math.min(1, bufferStartBytes / this.fileSize);
|
|
2021
1972
|
return ratio * duration;
|
|
2022
1973
|
}
|
|
2023
|
-
|
|
2024
|
-
const ratio = Math.min(1, bufferStartBytes / this.fileSize);
|
|
2025
|
-
return ratio * duration;
|
|
1974
|
+
return 0;
|
|
2026
1975
|
}
|
|
2027
1976
|
/**
|
|
2028
1977
|
* Get buffer end time in seconds (for HttpSource)
|