@cinecrew/cinecrew-player 0.1.9 → 0.1.10

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cinecrew/cinecrew-player",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "type": "module",
5
5
  "description": "Customizable cross-platform live and on-demand video player for React and React Native.",
6
6
  "license": "MIT",
@@ -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
- let sourceStream;
67
- try {
68
- sourceStream = capture.call(video);
69
- } catch (error) {
70
- if (error?.name === 'SecurityError' || /cross.?origin|security/i.test(error?.message || '')) {
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.getTracks().forEach((track) => track.stop());
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, video, getAspectRatio());
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.getTracks().forEach((track) => track.stop());
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.getTracks().forEach((track) => track.stop());
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, video, getAspectRatio());
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.getTracks().forEach((track) => track.stop());
133
- sourceStream.getTracks().forEach((track) => track.stop());
153
+ stopStreamTracks(canvasStream);
154
+ stopStreamTracks(sourceStream);
134
155
  },
135
156
  };
136
157
  }