@cinecrew/cinecrew-player 0.1.9 → 0.1.11
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/package.json
CHANGED
|
@@ -150,6 +150,33 @@ function applyPlayerStyle(stage, frame, player, playerStyle) {
|
|
|
150
150
|
});
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
function createOgvRecordingAudioRoute() {
|
|
154
|
+
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
|
155
|
+
if (!AudioContextClass) return null;
|
|
156
|
+
|
|
157
|
+
let audioContext;
|
|
158
|
+
try {
|
|
159
|
+
audioContext = new AudioContextClass();
|
|
160
|
+
const output = audioContext.createGain();
|
|
161
|
+
const recordingDestination = audioContext.createMediaStreamDestination();
|
|
162
|
+
output.connect(audioContext.destination);
|
|
163
|
+
output.connect(recordingDestination);
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
audioContext,
|
|
167
|
+
audioDestination: output,
|
|
168
|
+
stream: recordingDestination.stream,
|
|
169
|
+
cleanup: () => {
|
|
170
|
+
recordingDestination.stream.getTracks().forEach((track) => track.stop());
|
|
171
|
+
if (audioContext.state !== 'closed') audioContext.close().catch(() => {});
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
} catch {
|
|
175
|
+
if (audioContext && audioContext.state !== 'closed') audioContext.close().catch(() => {});
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
153
180
|
/**
|
|
154
181
|
* Use ogv.js for Ogg/Theora media on web, while keeping the returned media-like
|
|
155
182
|
* player in videoRef so the shared controls, progress, and recording logic work.
|
|
@@ -191,6 +218,7 @@ export function useWebOgvPlayback({
|
|
|
191
218
|
let player = null;
|
|
192
219
|
let playerFrame = null;
|
|
193
220
|
let resizeObserver = null;
|
|
221
|
+
let recordingAudioRoute = null;
|
|
194
222
|
const reportError = (error) => {
|
|
195
223
|
if (disposed) return;
|
|
196
224
|
onBufferingRef?.current?.(false);
|
|
@@ -227,10 +255,20 @@ export function useWebOgvPlayback({
|
|
|
227
255
|
}
|
|
228
256
|
|
|
229
257
|
OGVLoader.base = resolvedBase;
|
|
258
|
+
recordingAudioRoute = createOgvRecordingAudioRoute();
|
|
230
259
|
player = new OGVPlayer({
|
|
231
260
|
base: resolvedBase,
|
|
232
261
|
worker: true,
|
|
262
|
+
...(recordingAudioRoute ? {
|
|
263
|
+
audioContext: recordingAudioRoute.audioContext,
|
|
264
|
+
audioDestination: recordingAudioRoute.audioDestination,
|
|
265
|
+
} : {}),
|
|
233
266
|
});
|
|
267
|
+
if (recordingAudioRoute) {
|
|
268
|
+
player.__cinecrewRecordingAudioStream = recordingAudioRoute.stream;
|
|
269
|
+
player.__cinecrewRecordingAudioContext = recordingAudioRoute.audioContext;
|
|
270
|
+
player.__cinecrewRecordingCanvas = player.querySelector('canvas') || player._view;
|
|
271
|
+
}
|
|
234
272
|
player.className = 'cinecrew-player__video';
|
|
235
273
|
playerFrame = document.createElement('div');
|
|
236
274
|
playerFrame.className = 'cinecrew-player__ogv-frame';
|
|
@@ -283,6 +321,7 @@ export function useWebOgvPlayback({
|
|
|
283
321
|
try { player.remove(); } catch {}
|
|
284
322
|
if (videoRef.current === player) videoRef.current = null;
|
|
285
323
|
}
|
|
324
|
+
recordingAudioRoute?.cleanup();
|
|
286
325
|
if (playerFrameRef.current === playerFrame) playerFrameRef.current = null;
|
|
287
326
|
};
|
|
288
327
|
}, [activeUrl, useOgv, playerContainerRef, videoRef, pausedRef, onErrorRef, onBufferingRef, resourceBase, onProgressRef, onPlayingRef, onEndedRef, onPlaybackRouteRef]);
|
|
@@ -19,8 +19,8 @@ function drawVideoFrame(context, canvas, video, aspectRatio) {
|
|
|
19
19
|
|
|
20
20
|
const ratio = parseRatio(aspectRatio);
|
|
21
21
|
const frame = fitRect(width, height, ratio);
|
|
22
|
-
const sourceWidth = video.videoWidth;
|
|
23
|
-
const sourceHeight = video.videoHeight;
|
|
22
|
+
const sourceWidth = video.videoWidth || video.width;
|
|
23
|
+
const sourceHeight = video.videoHeight || video.height;
|
|
24
24
|
if (!sourceWidth || !sourceHeight) return;
|
|
25
25
|
|
|
26
26
|
const mode = String(aspectRatio || 'FIT').toUpperCase();
|
|
@@ -48,6 +48,33 @@ function drawVideoFrame(context, canvas, video, aspectRatio) {
|
|
|
48
48
|
context.drawImage(video, frame.x + (frame.width - drawWidth) / 2, frame.y + (frame.height - drawHeight) / 2, drawWidth, drawHeight);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function isCrossOriginCaptureError(error) {
|
|
52
|
+
return error?.name === 'SecurityError' || /cross.?origin|security/i.test(error?.message || '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function captureSourceStream(video, capture, ogvCanvas) {
|
|
56
|
+
try {
|
|
57
|
+
if (typeof capture === 'function') return capture.call(video);
|
|
58
|
+
return ogvCanvas.captureStream(30);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (isCrossOriginCaptureError(error)) return null;
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getRecordingAudioTracks(video, sourceStream, additionalAudioStream) {
|
|
66
|
+
const suppliedTracks = additionalAudioStream?.getAudioTracks?.() || [];
|
|
67
|
+
if (suppliedTracks.length) return suppliedTracks;
|
|
68
|
+
|
|
69
|
+
const ogvTracks = video.__cinecrewRecordingAudioStream?.getAudioTracks?.() || [];
|
|
70
|
+
if (ogvTracks.length) return ogvTracks;
|
|
71
|
+
return sourceStream.getAudioTracks();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function stopStreamTracks(stream) {
|
|
75
|
+
stream.getTracks().forEach((track) => track.stop());
|
|
76
|
+
}
|
|
77
|
+
|
|
51
78
|
/** Capture the visible media frame (not player chrome) and preserve its audio tracks.
|
|
52
79
|
* Returns `null` when cross-origin restrictions prevent direct capture so the
|
|
53
80
|
* caller can fall back to screen/tab capture via `createScreenRecordingStream`.
|
|
@@ -57,26 +84,20 @@ export function createVideoRecordingStream(video, playerElement, getAspectRatio,
|
|
|
57
84
|
if (!video || typeof MediaStream === 'undefined') {
|
|
58
85
|
throw new Error('This browser does not support recording this media element.');
|
|
59
86
|
}
|
|
87
|
+
const ogvCanvas = typeof capture !== 'function' ? video.__cinecrewRecordingCanvas : null;
|
|
60
88
|
|
|
61
89
|
// captureStream may not exist on every browser / element combination.
|
|
62
|
-
if (typeof capture !== 'function') {
|
|
90
|
+
if (typeof capture !== 'function' && typeof ogvCanvas?.captureStream !== 'function') {
|
|
63
91
|
return null;
|
|
64
92
|
}
|
|
65
93
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// Cross-origin media without CORS – let the caller fall back to screen capture.
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
throw error;
|
|
75
|
-
}
|
|
76
|
-
const extraAudioTracks = additionalAudioStream?.getAudioTracks?.() || [];
|
|
77
|
-
const audioTracks = extraAudioTracks.length ? extraAudioTracks : sourceStream.getAudioTracks();
|
|
94
|
+
// Cross-origin media without CORS lets the caller fall back to screen capture.
|
|
95
|
+
const sourceStream = captureSourceStream(video, capture, ogvCanvas);
|
|
96
|
+
if (!sourceStream) return null;
|
|
97
|
+
const audioTracks = getRecordingAudioTracks(video, sourceStream, additionalAudioStream);
|
|
98
|
+
const drawable = ogvCanvas || video;
|
|
78
99
|
const bounds = playerElement?.getBoundingClientRect?.();
|
|
79
|
-
const outputWidth = Math.max(2, video.videoWidth || Math.round(bounds?.width || 640));
|
|
100
|
+
const outputWidth = Math.max(2, video.videoWidth || drawable.width || Math.round(bounds?.width || 640));
|
|
80
101
|
const playerRatio = bounds?.width && bounds?.height ? bounds.width / bounds.height : 16 / 9;
|
|
81
102
|
const outputHeight = Math.max(2, Math.round(outputWidth / playerRatio));
|
|
82
103
|
const canvas = document.createElement('canvas');
|
|
@@ -84,13 +105,13 @@ export function createVideoRecordingStream(video, playerElement, getAspectRatio,
|
|
|
84
105
|
canvas.height = outputHeight;
|
|
85
106
|
const context = canvas.getContext('2d');
|
|
86
107
|
if (!context || typeof canvas.captureStream !== 'function') {
|
|
87
|
-
sourceStream
|
|
108
|
+
stopStreamTracks(sourceStream);
|
|
88
109
|
throw new Error('Canvas recording is not supported in this browser.');
|
|
89
110
|
}
|
|
90
111
|
|
|
91
112
|
let originClean = false;
|
|
92
113
|
try {
|
|
93
|
-
drawVideoFrame(context, canvas,
|
|
114
|
+
drawVideoFrame(context, canvas, drawable, getAspectRatio());
|
|
94
115
|
context.getImageData(0, 0, 1, 1);
|
|
95
116
|
originClean = true;
|
|
96
117
|
} catch {
|
|
@@ -99,7 +120,7 @@ export function createVideoRecordingStream(video, playerElement, getAspectRatio,
|
|
|
99
120
|
}
|
|
100
121
|
|
|
101
122
|
if (!originClean) {
|
|
102
|
-
sourceStream
|
|
123
|
+
stopStreamTracks(sourceStream);
|
|
103
124
|
return null;
|
|
104
125
|
}
|
|
105
126
|
|
|
@@ -111,7 +132,7 @@ export function createVideoRecordingStream(video, playerElement, getAspectRatio,
|
|
|
111
132
|
stream,
|
|
112
133
|
aspectRatioApplied: false,
|
|
113
134
|
cleanup: () => {
|
|
114
|
-
sourceStream
|
|
135
|
+
stopStreamTracks(sourceStream);
|
|
115
136
|
},
|
|
116
137
|
};
|
|
117
138
|
}
|
|
@@ -120,7 +141,7 @@ export function createVideoRecordingStream(video, playerElement, getAspectRatio,
|
|
|
120
141
|
const stream = new MediaStream([...canvasStream.getVideoTracks(), ...audioTracks]);
|
|
121
142
|
let frameId;
|
|
122
143
|
const draw = () => {
|
|
123
|
-
drawVideoFrame(context, canvas,
|
|
144
|
+
drawVideoFrame(context, canvas, drawable, getAspectRatio());
|
|
124
145
|
frameId = requestAnimationFrame(draw);
|
|
125
146
|
};
|
|
126
147
|
frameId = requestAnimationFrame(draw);
|
|
@@ -129,8 +150,8 @@ export function createVideoRecordingStream(video, playerElement, getAspectRatio,
|
|
|
129
150
|
aspectRatioApplied: true,
|
|
130
151
|
cleanup: () => {
|
|
131
152
|
cancelAnimationFrame(frameId);
|
|
132
|
-
canvasStream
|
|
133
|
-
sourceStream
|
|
153
|
+
stopStreamTracks(canvasStream);
|
|
154
|
+
stopStreamTracks(sourceStream);
|
|
134
155
|
},
|
|
135
156
|
};
|
|
136
157
|
}
|