avbridge 2.1.2 → 2.2.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/CHANGELOG.md +93 -0
- package/dist/{avi-GNTV5ZOH.cjs → avi-6SJLWIWW.cjs} +19 -4
- package/dist/avi-6SJLWIWW.cjs.map +1 -0
- package/dist/{avi-V6HYQVR2.js → avi-GCGM7OJI.js} +18 -3
- package/dist/avi-GCGM7OJI.js.map +1 -0
- package/dist/{chunk-EJH67FXG.js → chunk-5DMTJVIU.js} +99 -3
- package/dist/chunk-5DMTJVIU.js.map +1 -0
- package/dist/{chunk-3AUGRKPY.js → chunk-C5VA5U5O.js} +94 -16
- package/dist/chunk-C5VA5U5O.js.map +1 -0
- package/dist/{chunk-JQH6D4OE.cjs → chunk-G4APZMCP.cjs} +100 -3
- package/dist/chunk-G4APZMCP.cjs.map +1 -0
- package/dist/{chunk-Y5FYF5KG.cjs → chunk-HZLQNKFN.cjs} +5 -2
- package/dist/chunk-HZLQNKFN.cjs.map +1 -0
- package/dist/{chunk-PQTZS7OA.js → chunk-ILKDNBSE.js} +5 -2
- package/dist/chunk-ILKDNBSE.js.map +1 -0
- package/dist/{chunk-DPVIOYGC.cjs → chunk-OE66B34H.cjs} +98 -20
- package/dist/chunk-OE66B34H.cjs.map +1 -0
- package/dist/element-browser.js +210 -10
- package/dist/element-browser.js.map +1 -1
- package/dist/element.cjs +4 -4
- package/dist/element.d.cts +1 -1
- package/dist/element.d.ts +1 -1
- package/dist/element.js +3 -3
- package/dist/index.cjs +18 -18
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +5 -5
- package/dist/libav-loader-27RDIN2I.js +3 -0
- package/dist/{libav-loader-XKH2TKUW.js.map → libav-loader-27RDIN2I.js.map} +1 -1
- package/dist/libav-loader-IV4AJ2HW.cjs +12 -0
- package/dist/{libav-loader-6APXVNIV.cjs.map → libav-loader-IV4AJ2HW.cjs.map} +1 -1
- package/dist/{player-BdtUG4rh.d.cts → player-DUyvltvy.d.cts} +3 -3
- package/dist/{player-BdtUG4rh.d.ts → player-DUyvltvy.d.ts} +3 -3
- package/dist/source-CN43EI7Z.cjs +28 -0
- package/dist/{source-SC6ZEQYR.cjs.map → source-CN43EI7Z.cjs.map} +1 -1
- package/dist/source-FFZ7TW2B.js +3 -0
- package/dist/{source-ZFS4H7J3.js.map → source-FFZ7TW2B.js.map} +1 -1
- package/package.json +1 -1
- package/src/classify/rules.ts +9 -2
- package/src/player.ts +22 -1
- package/src/probe/avi.ts +8 -1
- package/src/strategies/fallback/audio-output.ts +25 -3
- package/src/strategies/fallback/decoder.ts +96 -8
- package/src/strategies/fallback/index.ts +90 -6
- package/src/strategies/fallback/libav-loader.ts +12 -0
- package/src/types.ts +10 -1
- package/src/util/debug.ts +131 -0
- package/src/util/source.ts +4 -0
- package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.mjs +1 -1
- package/vendor/libav/avbridge/libav-6.8.8.0-avbridge.wasm.wasm +0 -0
- package/dist/avi-GNTV5ZOH.cjs.map +0 -1
- package/dist/avi-V6HYQVR2.js.map +0 -1
- package/dist/chunk-3AUGRKPY.js.map +0 -1
- package/dist/chunk-DPVIOYGC.cjs.map +0 -1
- package/dist/chunk-EJH67FXG.js.map +0 -1
- package/dist/chunk-JQH6D4OE.cjs.map +0 -1
- package/dist/chunk-PQTZS7OA.js.map +0 -1
- package/dist/chunk-Y5FYF5KG.cjs.map +0 -1
- package/dist/libav-loader-6APXVNIV.cjs +0 -12
- package/dist/libav-loader-XKH2TKUW.js +0 -3
- package/dist/source-SC6ZEQYR.cjs +0 -28
- package/dist/source-ZFS4H7J3.js +0 -3
|
@@ -2,6 +2,7 @@ import type { MediaContext, PlaybackSession } from "../../types.js";
|
|
|
2
2
|
import { VideoRenderer } from "./video-renderer.js";
|
|
3
3
|
import { AudioOutput } from "./audio-output.js";
|
|
4
4
|
import { startDecoder, type DecoderHandles } from "./decoder.js";
|
|
5
|
+
import { dbg } from "../../util/debug.js";
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Fallback strategy session.
|
|
@@ -30,8 +31,27 @@ import { startDecoder, type DecoderHandles } from "./decoder.js";
|
|
|
30
31
|
* seek(t)` — none of the buffering choreography leaks out.
|
|
31
32
|
*/
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
// Gate for cold-start playback. We want to start playing as soon as
|
|
35
|
+
// there's any decoded output — the decoder will keep pumping during
|
|
36
|
+
// playback, so more-is-better buffering only helps for fast decoders.
|
|
37
|
+
//
|
|
38
|
+
// For software-decode-bound content (rv40 / wmv3 / mpeg4 @ 720p+ on
|
|
39
|
+
// single-threaded WASM), the decoder may run *slower* than realtime.
|
|
40
|
+
// Waiting for a large audio-buffer threshold is actively wrong in that
|
|
41
|
+
// case: it will never be reached, so the old gate would sit out its
|
|
42
|
+
// full 10-second timeout before playing anything. An aggressive gate
|
|
43
|
+
// ships the first frame to the screen fast, at the cost of the audio
|
|
44
|
+
// clock racing a little ahead of video in the first few seconds —
|
|
45
|
+
// which is the same situation we'd have been in after the timeout
|
|
46
|
+
// anyway.
|
|
47
|
+
//
|
|
48
|
+
// READY_AUDIO_BUFFER_SECONDS: minimum audio queued before start. Set
|
|
49
|
+
// low enough that a slow decoder still reaches it before the user
|
|
50
|
+
// loses patience; 40 ms ≈ 2 cook packets or ~2 AAC packets.
|
|
51
|
+
// READY_TIMEOUT_SECONDS: hard safety. If even 40 ms of audio can't be
|
|
52
|
+
// produced in 3 s, give up and play whatever we have.
|
|
53
|
+
const READY_AUDIO_BUFFER_SECONDS = 0.04;
|
|
54
|
+
const READY_TIMEOUT_SECONDS = 3;
|
|
35
55
|
|
|
36
56
|
export async function createFallbackSession(
|
|
37
57
|
ctx: MediaContext,
|
|
@@ -86,16 +106,80 @@ export async function createFallbackSession(
|
|
|
86
106
|
* playback smoothly. Returns early on timeout so we don't hang forever
|
|
87
107
|
* if the decoder is producing nothing (e.g. immediately past EOF after
|
|
88
108
|
* a seek to the end).
|
|
109
|
+
*
|
|
110
|
+
* The gate has three exit paths in order of preference:
|
|
111
|
+
*
|
|
112
|
+
* 1. **Fully ready** — audio buffer ≥ target AND ≥1 video frame.
|
|
113
|
+
* The happy path for fast decoders (native + remux never reach
|
|
114
|
+
* this function; this is fallback only).
|
|
115
|
+
*
|
|
116
|
+
* 2. **Video-ready, audio grace period elapsed** — we have video
|
|
117
|
+
* frames but the audio scheduler is still empty. RM/AVI
|
|
118
|
+
* containers commonly deliver a video GOP before their first
|
|
119
|
+
* audio packet, so "no audio yet" ≠ "no audio coming". We give
|
|
120
|
+
* the demuxer a 500 ms grace window from first-frame, then
|
|
121
|
+
* start regardless. Audio will be scheduled at its correct
|
|
122
|
+
* media time once its packets arrive.
|
|
123
|
+
*
|
|
124
|
+
* 3. **Hard timeout** — after {@link READY_TIMEOUT_SECONDS} seconds
|
|
125
|
+
* with neither condition met, start anyway and emit an
|
|
126
|
+
* unconditional diagnostic so the specific underflow is visible.
|
|
127
|
+
*
|
|
128
|
+
* Path #2 is what fixed the "RMVB sits on the play button for 10 s
|
|
129
|
+
* with audio=0ms, frames=N" case — the gate was waiting on audio
|
|
130
|
+
* packets that were several seconds behind in the file stream, and
|
|
131
|
+
* the timeout was the only way out.
|
|
89
132
|
*/
|
|
90
133
|
async function waitForBuffer(): Promise<void> {
|
|
91
134
|
const start = performance.now();
|
|
135
|
+
let firstFrameAtMs = 0;
|
|
136
|
+
dbg.info("cold-start",
|
|
137
|
+
`gate entry: want audio ≥ ${READY_AUDIO_BUFFER_SECONDS * 1000}ms + 1 frame`,
|
|
138
|
+
);
|
|
92
139
|
while (true) {
|
|
93
|
-
const
|
|
94
|
-
|
|
140
|
+
const audioAhead = audio.isNoAudio() ? Infinity : audio.bufferAhead();
|
|
141
|
+
const audioReady = audio.isNoAudio() || audioAhead >= READY_AUDIO_BUFFER_SECONDS;
|
|
142
|
+
const hasFrames = renderer.hasFrames();
|
|
143
|
+
const nowMs = performance.now();
|
|
144
|
+
|
|
145
|
+
if (hasFrames && firstFrameAtMs === 0) firstFrameAtMs = nowMs;
|
|
146
|
+
|
|
147
|
+
// Happy path: both ready.
|
|
148
|
+
if (audioReady && hasFrames) {
|
|
149
|
+
dbg.info("cold-start",
|
|
150
|
+
`gate satisfied in ${(nowMs - start).toFixed(0)}ms ` +
|
|
151
|
+
`(audio=${(audioAhead * 1000).toFixed(0)}ms, frames=${renderer.queueDepth()})`,
|
|
152
|
+
);
|
|
95
153
|
return;
|
|
96
154
|
}
|
|
97
|
-
|
|
98
|
-
|
|
155
|
+
|
|
156
|
+
// Grace path: have video, still waiting for audio that's
|
|
157
|
+
// on its way (first 500 ms after first-frame).
|
|
158
|
+
if (
|
|
159
|
+
hasFrames &&
|
|
160
|
+
firstFrameAtMs > 0 &&
|
|
161
|
+
nowMs - firstFrameAtMs >= 500
|
|
162
|
+
) {
|
|
163
|
+
dbg.info("cold-start",
|
|
164
|
+
`gate released on video-only grace at ${(nowMs - start).toFixed(0)}ms ` +
|
|
165
|
+
`(frames=${renderer.queueDepth()}, audio=${(audioAhead * 1000).toFixed(0)}ms — ` +
|
|
166
|
+
`demuxer hasn't delivered audio packets yet, starting anyway and letting ` +
|
|
167
|
+
`the audio scheduler catch up at its media-time anchor)`,
|
|
168
|
+
);
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Hard timeout.
|
|
173
|
+
if ((nowMs - start) / 1000 > READY_TIMEOUT_SECONDS) {
|
|
174
|
+
dbg.diag("cold-start",
|
|
175
|
+
`gate TIMEOUT after ${READY_TIMEOUT_SECONDS}s — ` +
|
|
176
|
+
`audio=${(audioAhead * 1000).toFixed(0)}ms ` +
|
|
177
|
+
`(needed ${READY_AUDIO_BUFFER_SECONDS * 1000}ms), ` +
|
|
178
|
+
`frames=${renderer.queueDepth()} (needed ≥1). ` +
|
|
179
|
+
`Decoder produced nothing in ${READY_TIMEOUT_SECONDS}s — either a corrupt source, ` +
|
|
180
|
+
`a missing codec, or WASM is catastrophically slow on this file. ` +
|
|
181
|
+
`Check getDiagnostics().runtime for decode counters.`,
|
|
182
|
+
);
|
|
99
183
|
return;
|
|
100
184
|
}
|
|
101
185
|
await new Promise((r) => setTimeout(r, 50));
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
* breaks libav's sibling-binary loading.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
import { dbg } from "../../util/debug.js";
|
|
26
|
+
|
|
25
27
|
export type LibavVariant = "webcodecs" | "default" | "avbridge";
|
|
26
28
|
|
|
27
29
|
export interface LoadLibavOptions {
|
|
@@ -85,12 +87,22 @@ export function loadLibav(
|
|
|
85
87
|
async function loadVariant(
|
|
86
88
|
variant: LibavVariant,
|
|
87
89
|
wantThreads: boolean,
|
|
90
|
+
): Promise<LibavInstance> {
|
|
91
|
+
return dbg.timed("libav-load", `load "${variant}" (threads=${wantThreads})`, 5000, () =>
|
|
92
|
+
loadVariantInner(variant, wantThreads),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function loadVariantInner(
|
|
97
|
+
variant: LibavVariant,
|
|
98
|
+
wantThreads: boolean,
|
|
88
99
|
): Promise<LibavInstance> {
|
|
89
100
|
const key = cacheKey(variant, wantThreads);
|
|
90
101
|
const base = `${libavBaseUrl()}/${variant}`;
|
|
91
102
|
// The custom variant is named `libav-avbridge.mjs`; the npm variants follow
|
|
92
103
|
// the same convention (`libav-webcodecs.mjs`, `libav-default.mjs`).
|
|
93
104
|
const variantUrl = `${base}/libav-${variant}.mjs`;
|
|
105
|
+
dbg.info("libav-load", `fetching ${variantUrl}`);
|
|
94
106
|
|
|
95
107
|
// Preflight HEAD-ish check: issue a bytes=0-0 range request so a missing
|
|
96
108
|
// file fails fast with a clear error instead of hanging deep inside the
|
package/src/types.ts
CHANGED
|
@@ -23,6 +23,7 @@ export type ContainerKind =
|
|
|
23
23
|
| "avi"
|
|
24
24
|
| "asf"
|
|
25
25
|
| "flv"
|
|
26
|
+
| "rm" // RealMedia (.rm / .rmvb)
|
|
26
27
|
| "ogg"
|
|
27
28
|
| "wav"
|
|
28
29
|
| "mp3"
|
|
@@ -41,7 +42,10 @@ export type VideoCodec =
|
|
|
41
42
|
| "mpeg4" // MPEG-4 Part 2 (DivX/Xvid)
|
|
42
43
|
| "wmv3"
|
|
43
44
|
| "vc1"
|
|
44
|
-
| "
|
|
45
|
+
| "rv10" // RealVideo 1.0 (H.263-like)
|
|
46
|
+
| "rv20" // RealVideo G2
|
|
47
|
+
| "rv30" // RealVideo 8
|
|
48
|
+
| "rv40" // RealVideo 9/10
|
|
45
49
|
| "mpeg2"
|
|
46
50
|
| "mpeg1"
|
|
47
51
|
| "theora"
|
|
@@ -60,6 +64,11 @@ export type AudioCodec =
|
|
|
60
64
|
| "wmav2"
|
|
61
65
|
| "wmapro"
|
|
62
66
|
| "alac"
|
|
67
|
+
| "cook" // RealAudio Cooker (G2/RealAudio 8)
|
|
68
|
+
| "ra_144" // RealAudio 1.0 (14.4 kbps)
|
|
69
|
+
| "ra_288" // RealAudio 2.0 (28.8 kbps)
|
|
70
|
+
| "sipr" // RealAudio Sipr (voice codec)
|
|
71
|
+
| "atrac3" // Sony ATRAC3 (sometimes seen in .rm)
|
|
63
72
|
| (string & {});
|
|
64
73
|
|
|
65
74
|
export interface VideoTrackInfo {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug + self-diagnosis helper.
|
|
3
|
+
*
|
|
4
|
+
* avbridge has a lot of async stages (probe → classify → libav load →
|
|
5
|
+
* strategy.execute → decoder pump → cold-start gate → first paint) and
|
|
6
|
+
* when something's slow or wrong the symptom — "it hangs", "it stutters",
|
|
7
|
+
* "it plays audio without video" — is usually nowhere near the actual
|
|
8
|
+
* cause. This module gives us two things:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Gated verbose logging.** `dbg.info(tag, ...)` etc. are no-ops
|
|
11
|
+
* unless the consumer sets `globalThis.AVBRIDGE_DEBUG = true` (or
|
|
12
|
+
* uses the matching `?avbridge_debug` URL search param at dev time).
|
|
13
|
+
* When enabled, every log is prefixed with `[avbridge:<tag>]` so the
|
|
14
|
+
* console is filterable.
|
|
15
|
+
*
|
|
16
|
+
* 2. **Unconditional self-diagnosis.** `dbg.warnIf(cond, tag, ...)`
|
|
17
|
+
* always fires when a suspicious condition is detected, even with
|
|
18
|
+
* debug off. These are the things we *know* mean something is
|
|
19
|
+
* broken or degraded and the user would want to know about — e.g.
|
|
20
|
+
* the cold-start gate timing out, the decoder running slower than
|
|
21
|
+
* realtime, a libav variant taking longer to load than any network
|
|
22
|
+
* should take, >20% of packets getting rejected.
|
|
23
|
+
*
|
|
24
|
+
* The guiding principle: **if a symptom caused more than 10 minutes of
|
|
25
|
+
* human debugging once, add a targeted warning so the next instance
|
|
26
|
+
* self-identifies in the console.** This module is where those
|
|
27
|
+
* warnings live.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/** Read the debug flag fresh on every call so it's runtime-toggleable. */
|
|
31
|
+
function isDebugEnabled(): boolean {
|
|
32
|
+
if (typeof globalThis === "undefined") return false;
|
|
33
|
+
const g = globalThis as { AVBRIDGE_DEBUG?: unknown };
|
|
34
|
+
if (g.AVBRIDGE_DEBUG === true) return true;
|
|
35
|
+
// Convenience: if running in a browser with a `?avbridge_debug` search
|
|
36
|
+
// param, flip the flag on automatically. Useful for demos and quick
|
|
37
|
+
// user reproduction without editing code.
|
|
38
|
+
if (typeof location !== "undefined" && typeof URLSearchParams !== "undefined") {
|
|
39
|
+
try {
|
|
40
|
+
const p = new URLSearchParams(location.search);
|
|
41
|
+
if (p.has("avbridge_debug")) {
|
|
42
|
+
g.AVBRIDGE_DEBUG = true;
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
} catch { /* ignore */ }
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function fmt(tag: string): string {
|
|
51
|
+
return `[avbridge:${tag}]`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* eslint-disable no-console */
|
|
55
|
+
|
|
56
|
+
export const dbg = {
|
|
57
|
+
/** Verbose — only when debug is enabled. The hot-path normal case. */
|
|
58
|
+
info(tag: string, ...args: unknown[]): void {
|
|
59
|
+
if (isDebugEnabled()) console.info(fmt(tag), ...args);
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
/** Warning — only when debug is enabled. Non-fatal oddities. */
|
|
63
|
+
warn(tag: string, ...args: unknown[]): void {
|
|
64
|
+
if (isDebugEnabled()) console.warn(fmt(tag), ...args);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Self-diagnosis warning. **Always** emits regardless of debug flag.
|
|
69
|
+
* Use this only for conditions that mean something is actually wrong
|
|
70
|
+
* or degraded — not for routine chatter.
|
|
71
|
+
*/
|
|
72
|
+
diag(tag: string, ...args: unknown[]): void {
|
|
73
|
+
console.warn(fmt(tag), ...args);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Timing helper: wraps an async call and logs its elapsed time when
|
|
78
|
+
* debug is on. The callback runs whether debug is on or off — this is
|
|
79
|
+
* just for the `dbg.info` at the end.
|
|
80
|
+
*
|
|
81
|
+
* Also unconditionally fires `dbg.diag` if the elapsed time exceeds
|
|
82
|
+
* `slowMs`, so "the bootstrap took 8 seconds" shows up even without
|
|
83
|
+
* debug mode enabled.
|
|
84
|
+
*/
|
|
85
|
+
async timed<T>(
|
|
86
|
+
tag: string,
|
|
87
|
+
label: string,
|
|
88
|
+
slowMs: number,
|
|
89
|
+
fn: () => Promise<T>,
|
|
90
|
+
): Promise<T> {
|
|
91
|
+
const start = performance.now();
|
|
92
|
+
try {
|
|
93
|
+
const result = await fn();
|
|
94
|
+
const elapsed = performance.now() - start;
|
|
95
|
+
if (isDebugEnabled()) {
|
|
96
|
+
console.info(fmt(tag), `${label} ${elapsed.toFixed(0)}ms`);
|
|
97
|
+
}
|
|
98
|
+
if (elapsed > slowMs) {
|
|
99
|
+
console.warn(
|
|
100
|
+
fmt(tag),
|
|
101
|
+
`${label} took ${elapsed.toFixed(0)}ms (>${slowMs}ms expected) — ` +
|
|
102
|
+
`this is unusually slow; possible causes: ${hintForTag(tag)}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return result;
|
|
106
|
+
} catch (err) {
|
|
107
|
+
const elapsed = performance.now() - start;
|
|
108
|
+
console.warn(
|
|
109
|
+
fmt(tag),
|
|
110
|
+
`${label} FAILED after ${elapsed.toFixed(0)}ms:`,
|
|
111
|
+
err,
|
|
112
|
+
);
|
|
113
|
+
throw err;
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
function hintForTag(tag: string): string {
|
|
119
|
+
switch (tag) {
|
|
120
|
+
case "probe":
|
|
121
|
+
return "slow network (range request), large sniff window, or libav cold-start";
|
|
122
|
+
case "libav-load":
|
|
123
|
+
return "large .wasm download, misconfigured AVBRIDGE_LIBAV_BASE, or server-side MIME type";
|
|
124
|
+
case "bootstrap":
|
|
125
|
+
return "probe+classify+strategy-init chain; enable AVBRIDGE_DEBUG for a phase breakdown";
|
|
126
|
+
case "cold-start":
|
|
127
|
+
return "decoder is producing output slower than realtime — check framesDecoded in getDiagnostics()";
|
|
128
|
+
default:
|
|
129
|
+
return "unknown stage — enable globalThis.AVBRIDGE_DEBUG for more detail";
|
|
130
|
+
}
|
|
131
|
+
}
|
package/src/util/source.ts
CHANGED
|
@@ -215,6 +215,10 @@ export function sniffContainerFromBytes(head: Uint8Array): ContainerKind {
|
|
|
215
215
|
) return "asf";
|
|
216
216
|
// FLV: 46 4C 56
|
|
217
217
|
if (head[0] === 0x46 && head[1] === 0x4c && head[2] === 0x56) return "flv";
|
|
218
|
+
// RealMedia (.rm / .rmvb): ".RMF" — 2E 52 4D 46
|
|
219
|
+
if (head[0] === 0x2e && head[1] === 0x52 && head[2] === 0x4d && head[3] === 0x46) {
|
|
220
|
+
return "rm";
|
|
221
|
+
}
|
|
218
222
|
// OggS: 4F 67 67 53
|
|
219
223
|
if (head[0] === 0x4f && head[1] === 0x67 && head[2] === 0x67 && head[3] === 0x53) return "ogg";
|
|
220
224
|
// FLAC: 66 4C 61 43
|