wenay-common2 1.0.70 → 1.0.74
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 +5 -3
- package/demo/client.ts +230 -3
- package/demo/index.html +37 -0
- package/demo/server.ts +91 -4
- package/doc/INTENT.md +31 -0
- package/doc/ROADMAP.md +233 -212
- package/doc/changes/1.0.71.md +9 -0
- package/doc/changes/1.0.72.md +6 -0
- package/doc/changes/1.0.73.md +6 -0
- package/doc/changes/1.0.74.md +12 -0
- package/doc/wenay-common2-rare.md +684 -627
- package/doc/wenay-common2.md +50 -5
- package/lib/Common/events/route-signal-webrtc.d.ts +1 -1
- package/lib/Common/events/route-signal-webrtc.js +15 -5
- package/lib/Common/media/media-index.d.ts +1 -0
- package/lib/Common/media/media-index.js +1 -0
- package/lib/Common/media/media-source.d.ts +3 -0
- package/lib/Common/media/media-source.js +136 -27
- package/lib/Common/media/media-view.d.ts +42 -0
- package/lib/Common/media/media-view.js +210 -0
- package/lib/Common/peer/peer-call.d.ts +65 -0
- package/lib/Common/peer/peer-call.js +173 -0
- package/lib/Common/peer/peer-client.d.ts +21 -5
- package/lib/Common/peer/peer-client.js +53 -3
- package/lib/Common/peer/peer-host.d.ts +22 -5
- package/lib/Common/peer/peer-host.js +49 -5
- package/lib/Common/peer/peer-index.d.ts +2 -0
- package/lib/Common/peer/peer-index.js +2 -0
- package/lib/Common/peer/peer-media-relay.d.ts +22 -0
- package/lib/Common/peer/peer-media-relay.js +155 -0
- package/lib/Common/peer/peer-relay.d.ts +10 -2
- package/lib/Common/peer/peer-relay.js +40 -18
- package/observe/listen-core.test.ts +1 -2
- package/package.json +1 -4
- package/replay/media-view.test.ts +164 -0
- package/replay/peer-call.test.ts +226 -0
- package/replay/peer-repair.test.ts +184 -0
- package/doc/changes/1.0.63.md +0 -8
- package/doc/changes/1.0.64.md +0 -10
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createMediaRelay = createMediaRelay;
|
|
4
|
+
const replay_listen_1 = require("../events/replay-listen");
|
|
5
|
+
const rpc_dynamic_1 = require("../rcp/rpc-dynamic");
|
|
6
|
+
function createMediaRelay(deps) {
|
|
7
|
+
const { lines, videoHistory = 8, audioHistory = 64, canWatch } = deps;
|
|
8
|
+
const accounts = new Map();
|
|
9
|
+
const watch = (0, rpc_dynamic_1.noStrict)({});
|
|
10
|
+
function makeLine(kind) {
|
|
11
|
+
return kind == 'video'
|
|
12
|
+
? (0, replay_listen_1.replayListen)({
|
|
13
|
+
history: videoHistory, current: 'last',
|
|
14
|
+
frame: function lastFrameOnly(tail) { return tail.length ? [tail[tail.length - 1]] : []; },
|
|
15
|
+
})
|
|
16
|
+
: (0, replay_listen_1.replayListen)({ history: audioHistory });
|
|
17
|
+
}
|
|
18
|
+
function linesOf(account) {
|
|
19
|
+
let entry = accounts.get(account);
|
|
20
|
+
if (!entry) {
|
|
21
|
+
entry = { emits: {}, view: (0, rpc_dynamic_1.noStrict)({}) };
|
|
22
|
+
for (const name of Object.keys(lines)) {
|
|
23
|
+
const [emit, api] = makeLine(lines[name]);
|
|
24
|
+
entry.emits[name] = emit;
|
|
25
|
+
entry.view[name] = api;
|
|
26
|
+
}
|
|
27
|
+
accounts.set(account, entry);
|
|
28
|
+
watch[account] = entry.view;
|
|
29
|
+
}
|
|
30
|
+
return entry;
|
|
31
|
+
}
|
|
32
|
+
function publishOf(account) {
|
|
33
|
+
linesOf(account);
|
|
34
|
+
return function publish(line, frame, sentAt) {
|
|
35
|
+
linesOf(account).emits[line]?.(frame, sentAt);
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
const watcherViews = new Map();
|
|
39
|
+
function allowed(watcher, owner, line) {
|
|
40
|
+
return canWatch ? canWatch(watcher, owner, line) : true;
|
|
41
|
+
}
|
|
42
|
+
function filteredLine(watcher, owner, name, source, cache) {
|
|
43
|
+
const [emit, filtered] = (0, replay_listen_1.replayListen)({
|
|
44
|
+
history: lines[name] == 'video' ? videoHistory : audioHistory,
|
|
45
|
+
current: function currentIfAllowed() {
|
|
46
|
+
return allowed(watcher, owner, name) ? source.keyframe()?.event : undefined;
|
|
47
|
+
},
|
|
48
|
+
...(lines[name] == 'video' ? {
|
|
49
|
+
frame: function lastAllowedFrame(tail) {
|
|
50
|
+
return tail.length ? [tail[tail.length - 1]] : [];
|
|
51
|
+
},
|
|
52
|
+
} : {}),
|
|
53
|
+
});
|
|
54
|
+
const offSource = source.on(function forwardAllowed(frame, sentAt) {
|
|
55
|
+
if (allowed(watcher, owner, name))
|
|
56
|
+
emit(frame, sentAt);
|
|
57
|
+
});
|
|
58
|
+
const closeFiltered = filtered.close;
|
|
59
|
+
filtered.close = function closePolicyLine() {
|
|
60
|
+
offSource();
|
|
61
|
+
closeFiltered();
|
|
62
|
+
};
|
|
63
|
+
cache.filtered.add(filtered);
|
|
64
|
+
return filtered;
|
|
65
|
+
}
|
|
66
|
+
function ownerViewFor(watcher, owner, cache) {
|
|
67
|
+
let view = cache.owners.get(owner);
|
|
68
|
+
if (view)
|
|
69
|
+
return view;
|
|
70
|
+
const entry = accounts.get(owner);
|
|
71
|
+
if (!entry)
|
|
72
|
+
return undefined;
|
|
73
|
+
const policyLines = {};
|
|
74
|
+
for (const name of Object.keys(lines))
|
|
75
|
+
policyLines[name] = filteredLine(watcher, owner, name, entry.view[name], cache);
|
|
76
|
+
view = (0, rpc_dynamic_1.noStrict)(new Proxy(policyLines, {
|
|
77
|
+
has(t, k) { return typeof k == 'string' && k in t && allowed(watcher, owner, k); },
|
|
78
|
+
get(t, k) {
|
|
79
|
+
if (typeof k != 'string')
|
|
80
|
+
return t[k];
|
|
81
|
+
return k in t && allowed(watcher, owner, k) ? t[k] : undefined;
|
|
82
|
+
},
|
|
83
|
+
}));
|
|
84
|
+
cache.owners.set(owner, view);
|
|
85
|
+
return view;
|
|
86
|
+
}
|
|
87
|
+
function watchOf(watcher) {
|
|
88
|
+
let cached = watcherViews.get(watcher);
|
|
89
|
+
if (cached)
|
|
90
|
+
return cached.root;
|
|
91
|
+
const owners = new Map();
|
|
92
|
+
const filtered = new Set();
|
|
93
|
+
function visible(owner) {
|
|
94
|
+
return accounts.has(owner) && Object.keys(lines).some(name => allowed(watcher, owner, name));
|
|
95
|
+
}
|
|
96
|
+
const root = (0, rpc_dynamic_1.noStrict)(new Proxy({}, {
|
|
97
|
+
has(_t, k) { return typeof k == 'string' && visible(k); },
|
|
98
|
+
get(_t, k) {
|
|
99
|
+
if (typeof k != 'string')
|
|
100
|
+
return undefined;
|
|
101
|
+
return visible(k) ? ownerViewFor(watcher, k, cached) : undefined;
|
|
102
|
+
},
|
|
103
|
+
}));
|
|
104
|
+
cached = { root, owners, filtered };
|
|
105
|
+
watcherViews.set(watcher, cached);
|
|
106
|
+
return root;
|
|
107
|
+
}
|
|
108
|
+
function dropLines(entry) {
|
|
109
|
+
for (const name of Object.keys(entry.view))
|
|
110
|
+
entry.view[name].close();
|
|
111
|
+
}
|
|
112
|
+
function closeWatcherCache(cache) {
|
|
113
|
+
for (const line of cache.filtered)
|
|
114
|
+
line.close();
|
|
115
|
+
cache.filtered.clear();
|
|
116
|
+
cache.owners.clear();
|
|
117
|
+
}
|
|
118
|
+
function dropAccount(account) {
|
|
119
|
+
const entry = accounts.get(account);
|
|
120
|
+
const ownCache = watcherViews.get(account);
|
|
121
|
+
if (ownCache)
|
|
122
|
+
closeWatcherCache(ownCache);
|
|
123
|
+
watcherViews.delete(account);
|
|
124
|
+
for (const cache of watcherViews.values()) {
|
|
125
|
+
const ownerView = cache.owners.get(account);
|
|
126
|
+
if (ownerView)
|
|
127
|
+
for (const line of Object.values(ownerView)) {
|
|
128
|
+
line.close();
|
|
129
|
+
cache.filtered.delete(line);
|
|
130
|
+
}
|
|
131
|
+
cache.owners.delete(account);
|
|
132
|
+
}
|
|
133
|
+
if (!entry)
|
|
134
|
+
return;
|
|
135
|
+
accounts.delete(account);
|
|
136
|
+
delete watch[account];
|
|
137
|
+
dropLines(entry);
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
publishOf,
|
|
141
|
+
watch,
|
|
142
|
+
watchOf,
|
|
143
|
+
lines: (account) => linesOf(account).view,
|
|
144
|
+
accounts: () => Array.from(accounts.keys()),
|
|
145
|
+
dropAccount,
|
|
146
|
+
close() {
|
|
147
|
+
for (const cache of watcherViews.values())
|
|
148
|
+
closeWatcherCache(cache);
|
|
149
|
+
for (const entry of accounts.values())
|
|
150
|
+
dropLines(entry);
|
|
151
|
+
accounts.clear();
|
|
152
|
+
watcherViews.clear();
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
@@ -2,11 +2,19 @@ import { ReplayEvent } from '../events/replay-listen';
|
|
|
2
2
|
import { ReplayRemote } from '../events/replay-wire';
|
|
3
3
|
import { StorePatch } from '../Observe/store';
|
|
4
4
|
export type PatchEnvelope = ReplayEvent<[StorePatch]>;
|
|
5
|
+
export type tRelayGap = 'resume' | 'sacred';
|
|
6
|
+
export type RelayPushResult = boolean | {
|
|
7
|
+
seq: number;
|
|
8
|
+
};
|
|
5
9
|
export declare function createPatchRelayJournal(opts?: {
|
|
6
10
|
history?: number;
|
|
11
|
+
gap?: tRelayGap;
|
|
7
12
|
}): {
|
|
8
|
-
push: (env: PatchEnvelope) =>
|
|
9
|
-
remote: ReplayRemote<[StorePatch]
|
|
13
|
+
push: (env: PatchEnvelope) => RelayPushResult;
|
|
14
|
+
remote: ReplayRemote<[StorePatch]> & {
|
|
15
|
+
seq: () => number;
|
|
16
|
+
};
|
|
17
|
+
gap: tRelayGap;
|
|
10
18
|
seq: () => number;
|
|
11
19
|
snapshot: () => any;
|
|
12
20
|
close: () => void;
|
|
@@ -16,37 +16,51 @@ function condensePatchTail(tail) {
|
|
|
16
16
|
return Array.from(held.values());
|
|
17
17
|
}
|
|
18
18
|
function createPatchRelayJournal(opts = {}) {
|
|
19
|
-
const { history = 1024 } = opts;
|
|
19
|
+
const { history = 1024, gap = 'resume' } = opts;
|
|
20
|
+
const folding = gap != 'sacred';
|
|
20
21
|
const [emitLine, line] = (0, Listen_1.listen)();
|
|
21
|
-
const fold = (0, store_1.createStore)({}, { drain: 'micro' });
|
|
22
|
+
const fold = folding ? (0, store_1.createStore)({}, { drain: 'micro' }) : null;
|
|
22
23
|
let ring = [];
|
|
23
24
|
let last = -1;
|
|
24
25
|
let lastTs = 0;
|
|
25
26
|
let hasState = false;
|
|
26
|
-
function
|
|
27
|
-
if (
|
|
28
|
-
return false;
|
|
29
|
-
const patch = env.event?.[0];
|
|
30
|
-
if (patch == null || !Array.isArray(patch.path))
|
|
31
|
-
return false;
|
|
32
|
-
const isRoot = patch.path.length == 0;
|
|
33
|
-
if (env.seq <= last) {
|
|
34
|
-
if (!isRoot || env.seq == last)
|
|
35
|
-
return false;
|
|
27
|
+
function accept(env, reset) {
|
|
28
|
+
if (reset)
|
|
36
29
|
ring = [];
|
|
37
|
-
}
|
|
38
30
|
last = env.seq;
|
|
39
31
|
lastTs = env.ts ?? lastTs;
|
|
40
32
|
hasState = true;
|
|
41
|
-
|
|
33
|
+
if (fold)
|
|
34
|
+
(0, store_1.applyStorePatch)(fold, env.event[0]);
|
|
42
35
|
ring.push(env);
|
|
43
36
|
if (ring.length > history)
|
|
44
37
|
ring.splice(0, ring.length - history);
|
|
45
38
|
emitLine(env);
|
|
46
39
|
return true;
|
|
47
40
|
}
|
|
41
|
+
function push(env) {
|
|
42
|
+
if (env == null || typeof env.seq != 'number')
|
|
43
|
+
return false;
|
|
44
|
+
const patch = env.event?.[0];
|
|
45
|
+
if (patch == null || !Array.isArray(patch.path))
|
|
46
|
+
return false;
|
|
47
|
+
const isRoot = patch.path.length == 0;
|
|
48
|
+
if (env.seq <= last) {
|
|
49
|
+
if (folding && isRoot && env.seq < last)
|
|
50
|
+
return accept(env, true);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (hasState && env.seq > last + 1) {
|
|
54
|
+
if (folding && isRoot)
|
|
55
|
+
return accept(env, true);
|
|
56
|
+
return { seq: last };
|
|
57
|
+
}
|
|
58
|
+
if (folding && !hasState && !isRoot)
|
|
59
|
+
return { seq: -1 };
|
|
60
|
+
return accept(env, false);
|
|
61
|
+
}
|
|
48
62
|
function keyframe() {
|
|
49
|
-
if (!hasState)
|
|
63
|
+
if (!fold || !hasState)
|
|
50
64
|
return null;
|
|
51
65
|
return { seq: last, ts: lastTs, event: [{ path: [], exists: true, value: fold.snapshot() }] };
|
|
52
66
|
}
|
|
@@ -64,14 +78,22 @@ function createPatchRelayJournal(opts = {}) {
|
|
|
64
78
|
if (tail)
|
|
65
79
|
return condensePatchTail(tail);
|
|
66
80
|
const kf = keyframe();
|
|
67
|
-
|
|
81
|
+
if (kf)
|
|
82
|
+
return [kf];
|
|
83
|
+
if (!folding && hasState)
|
|
84
|
+
throw new Error('sacred relay journal: tail evicted, no keyframe to invent');
|
|
85
|
+
return null;
|
|
68
86
|
}
|
|
69
|
-
const remote = {
|
|
87
|
+
const remote = {
|
|
88
|
+
line, since, keyframe, frame,
|
|
89
|
+
seq: () => last,
|
|
90
|
+
};
|
|
70
91
|
return {
|
|
71
92
|
push,
|
|
72
93
|
remote,
|
|
94
|
+
gap,
|
|
73
95
|
seq: () => last,
|
|
74
|
-
snapshot: () => fold
|
|
96
|
+
snapshot: () => fold?.snapshot(),
|
|
75
97
|
close: () => { line.close(); },
|
|
76
98
|
};
|
|
77
99
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wenay-common2",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.74",
|
|
4
4
|
"description": "Common library",
|
|
5
5
|
"strict": true,
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -42,8 +42,5 @@
|
|
|
42
42
|
"./package.json": "./package.json",
|
|
43
43
|
"./media": "./lib/Common/media/media-index.js",
|
|
44
44
|
"./peer": "./lib/Common/peer/peer-index.js"
|
|
45
|
-
},
|
|
46
|
-
"dependencies": {
|
|
47
|
-
"express": "^5.2.1"
|
|
48
45
|
}
|
|
49
46
|
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// replay/media-view.test.ts
|
|
3
|
+
//
|
|
4
|
+
// Media viewer helpers oracle (Node, no browser):
|
|
5
|
+
// - attachAudioPlayer: PCM decode, sequential playhead, live
|
|
6
|
+
// backlog drop, enable/disable, non-audio frames ignored
|
|
7
|
+
// - attachVideoCanvas: injected bitmap decoder, sizing follows
|
|
8
|
+
// frames, busy-skip keeps latest, stats
|
|
9
|
+
// - pipeMediaPublish: sentAt stamp, onError on rejection
|
|
10
|
+
// ============================================================
|
|
11
|
+
|
|
12
|
+
import {listen} from '../src/Common/events/Listen'
|
|
13
|
+
import {
|
|
14
|
+
attachAudioPlayer,
|
|
15
|
+
attachVideoCanvas,
|
|
16
|
+
encodeMediaFrame,
|
|
17
|
+
pipeMediaPublish,
|
|
18
|
+
} from '../src/Common/media/media-index'
|
|
19
|
+
|
|
20
|
+
let fails = 0
|
|
21
|
+
const ok = (condition: any, message: string) => {
|
|
22
|
+
if (!condition) { fails++; console.log(' FAIL', message) }
|
|
23
|
+
else console.log(' OK ', message)
|
|
24
|
+
}
|
|
25
|
+
const delay = (ms: number) => new Promise<void>(r => setTimeout(r, ms))
|
|
26
|
+
|
|
27
|
+
function pcmFrame(seq: number, nSamples = 480, sampleRate = 48000) {
|
|
28
|
+
const pcm = new Int16Array(nSamples)
|
|
29
|
+
for (let i = 0; i < nSamples; i++) pcm[i] = (i % 2 ? 1 : -1) * 1000
|
|
30
|
+
return encodeMediaFrame({kind: 'audio-pcm', codec: 'pcm16', seq, tMono: seq, sampleRate, channels: 1, nSamples}, new Uint8Array(pcm.buffer))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function videoFrame(seq: number, width: number, height: number) {
|
|
34
|
+
return encodeMediaFrame({kind: 'video-frame', codec: 'jpeg', seq, tMono: seq, width, height}, new Uint8Array([1, 2, 3]))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createFakeAudioContext() {
|
|
38
|
+
const scheduled: {at: number, frames: number}[] = []
|
|
39
|
+
const ctx = {
|
|
40
|
+
currentTime: 0,
|
|
41
|
+
destination: {},
|
|
42
|
+
resumed: 0,
|
|
43
|
+
closed: 0,
|
|
44
|
+
resume() { ctx.resumed++ },
|
|
45
|
+
close() { ctx.closed++ },
|
|
46
|
+
createBuffer: (_ch: number, frames: number, _rate: number) => ({
|
|
47
|
+
frames,
|
|
48
|
+
getChannelData: () => new Float32Array(frames),
|
|
49
|
+
}),
|
|
50
|
+
createBufferSource: () => {
|
|
51
|
+
const node: any = {connect: () => {}, start: (at: number) => scheduled.push({at, frames: node.buffer.frames})}
|
|
52
|
+
return node
|
|
53
|
+
},
|
|
54
|
+
scheduled,
|
|
55
|
+
}
|
|
56
|
+
return ctx
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function main() {
|
|
60
|
+
console.log('\n[media-view] audio player: sequential playhead + live backlog drop')
|
|
61
|
+
{
|
|
62
|
+
const [emit, line] = listen<[Uint8Array, number]>()
|
|
63
|
+
const ctx = createFakeAudioContext()
|
|
64
|
+
const player = attachAudioPlayer(line as any, {audioContext: () => ctx as any})
|
|
65
|
+
|
|
66
|
+
emit(pcmFrame(1), Date.now())
|
|
67
|
+
ok(player.stats().frames == 1 && player.stats().played == 0, 'disabled player counts frames but schedules nothing')
|
|
68
|
+
|
|
69
|
+
player.enable()
|
|
70
|
+
ok(player.enabled && ctx.resumed == 1, 'enable() builds the injected context and resumes it')
|
|
71
|
+
|
|
72
|
+
emit(pcmFrame(2), Date.now() - 100)
|
|
73
|
+
ok(player.stats().played == 1 && ctx.scheduled.length == 1, 'enabled player schedules a decoded pcm16 buffer')
|
|
74
|
+
ok(ctx.scheduled[0].frames == 480, 'sample count survives decode (480 frames)')
|
|
75
|
+
ok(player.stats().ageMs >= 80, 'sentAt stamp turns into a real age measurement')
|
|
76
|
+
|
|
77
|
+
const before = ctx.scheduled.length
|
|
78
|
+
emit(videoFrame(3, 320, 240), Date.now())
|
|
79
|
+
ok(ctx.scheduled.length == before && player.stats().frames == 3, 'video frames on an audio player are counted, not scheduled')
|
|
80
|
+
|
|
81
|
+
// 40 x 10ms chunks pushed instantly = 0.4s of queued audio while currentTime stays 0
|
|
82
|
+
for (let i = 0; i < 40; i++) emit(pcmFrame(10 + i), Date.now())
|
|
83
|
+
ok(player.stats().dropped > 0, 'backlog past maxBacklogSec is dropped (live policy), not queued forever')
|
|
84
|
+
const last = ctx.scheduled[ctx.scheduled.length - 1]
|
|
85
|
+
ok(last.at <= 0.45, `playhead rebased near "now" after the drop (last at=${last.at.toFixed(2)}s)`)
|
|
86
|
+
|
|
87
|
+
player.disable()
|
|
88
|
+
ok(!player.enabled && ctx.closed == 1, 'disable() closes the context')
|
|
89
|
+
player.off()
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
console.log('\n[media-view] video canvas: sizing follows frames, busy-skip keeps latest')
|
|
93
|
+
{
|
|
94
|
+
const [emit, line] = listen<[Uint8Array, number]>()
|
|
95
|
+
const drawnImages: any[] = []
|
|
96
|
+
const canvas: any = {
|
|
97
|
+
width: 0,
|
|
98
|
+
height: 0,
|
|
99
|
+
getContext: () => ({drawImage: (img: any) => drawnImages.push(img)}),
|
|
100
|
+
}
|
|
101
|
+
let decodeDelay = 0
|
|
102
|
+
const view = attachVideoCanvas(line as any, canvas, {
|
|
103
|
+
createBitmap: async blob => {
|
|
104
|
+
if (decodeDelay) await delay(decodeDelay)
|
|
105
|
+
return {size: blob.size, close: () => {}}
|
|
106
|
+
},
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
emit(videoFrame(1, 640, 480), Date.now() - 100)
|
|
110
|
+
await delay(10)
|
|
111
|
+
ok(view.stats().drawn == 1 && drawnImages.length == 1, 'video frame decoded via injected bitmap factory and drawn')
|
|
112
|
+
ok(canvas.width == 640 && canvas.height == 480, 'canvas takes the frame size')
|
|
113
|
+
ok(view.stats().width == 640 && view.stats().ageMs >= 80, 'stats expose frame size and age')
|
|
114
|
+
|
|
115
|
+
emit(pcmFrame(2), Date.now())
|
|
116
|
+
await delay(10)
|
|
117
|
+
ok(view.stats().drawn == 1 && view.stats().frames == 2, 'audio frame on a video canvas is counted, not drawn')
|
|
118
|
+
|
|
119
|
+
decodeDelay = 30
|
|
120
|
+
emit(videoFrame(3, 1280, 720), Date.now())
|
|
121
|
+
emit(videoFrame(4, 1280, 720), Date.now()) // arrives while frame 3 decodes -> skipped
|
|
122
|
+
await delay(60)
|
|
123
|
+
ok(view.stats().frames == 4 && view.stats().drawn == 2, 'busy-skip: overlapping frame dropped, not queued')
|
|
124
|
+
ok(canvas.width == 1280 && canvas.height == 720, 'canvas resized to the new resolution')
|
|
125
|
+
|
|
126
|
+
view.off()
|
|
127
|
+
emit(videoFrame(5, 320, 240), Date.now())
|
|
128
|
+
await delay(10)
|
|
129
|
+
ok(view.stats().frames == 4, 'off() unsubscribes the line')
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
console.log('\n[media-view] publish pipe: stamp + onError')
|
|
133
|
+
{
|
|
134
|
+
const [emit, line] = listen<[Uint8Array]>()
|
|
135
|
+
const sent: {frame: Uint8Array, sentAt?: number}[] = []
|
|
136
|
+
const errors: unknown[] = []
|
|
137
|
+
const off = pipeMediaPublish(line as any, (frame, sentAt) => {
|
|
138
|
+
sent.push({frame, sentAt})
|
|
139
|
+
return frame.byteLength == 0 ? Promise.reject(new Error('boom')) : Promise.resolve(true)
|
|
140
|
+
}, {onError: e => errors.push(e)})
|
|
141
|
+
|
|
142
|
+
emit(pcmFrame(1))
|
|
143
|
+
ok(sent.length == 1 && typeof sent[0].sentAt == 'number', 'frames are published with a wall-clock stamp by default')
|
|
144
|
+
|
|
145
|
+
emit(new Uint8Array(0))
|
|
146
|
+
await delay(10)
|
|
147
|
+
ok(errors.length == 1, 'publish rejection lands in onError, capture loop survives')
|
|
148
|
+
|
|
149
|
+
off()
|
|
150
|
+
emit(pcmFrame(2))
|
|
151
|
+
ok(sent.length == 2, 'off() stops publishing')
|
|
152
|
+
|
|
153
|
+
const noStamp: (number | undefined)[] = []
|
|
154
|
+
const [emit2, line2] = listen<[Uint8Array]>()
|
|
155
|
+
pipeMediaPublish(line2 as any, (_f, sentAt) => { noStamp.push(sentAt) }, {stamp: false})
|
|
156
|
+
emit2(pcmFrame(3))
|
|
157
|
+
ok(noStamp.length == 1 && noStamp[0] == undefined, 'stamp:false publishes without a timestamp')
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
console.log(fails ? `\n${fails} FAILED` : '\nall passed')
|
|
161
|
+
if (fails) process.exit(1)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
main().catch(e => { console.error(e); process.exit(1) })
|