@editframe/elements 0.20.4-beta.0 → 0.21.0-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.
Files changed (92) hide show
  1. package/dist/DelayedLoadingState.js +0 -27
  2. package/dist/EF_FRAMEGEN.d.ts +5 -3
  3. package/dist/EF_FRAMEGEN.js +50 -11
  4. package/dist/_virtual/_@oxc-project_runtime@0.93.0/helpers/decorate.js +7 -0
  5. package/dist/elements/ContextProxiesController.js +2 -22
  6. package/dist/elements/EFAudio.js +4 -8
  7. package/dist/elements/EFCaptions.js +59 -84
  8. package/dist/elements/EFImage.js +5 -6
  9. package/dist/elements/EFMedia/AssetIdMediaEngine.js +2 -4
  10. package/dist/elements/EFMedia/AssetMediaEngine.js +35 -30
  11. package/dist/elements/EFMedia/BaseMediaEngine.js +57 -73
  12. package/dist/elements/EFMedia/BufferedSeekingInput.js +134 -76
  13. package/dist/elements/EFMedia/JitMediaEngine.js +9 -19
  14. package/dist/elements/EFMedia/audioTasks/makeAudioBufferTask.js +3 -6
  15. package/dist/elements/EFMedia/audioTasks/makeAudioFrequencyAnalysisTask.js +1 -1
  16. package/dist/elements/EFMedia/audioTasks/makeAudioInitSegmentFetchTask.js +1 -1
  17. package/dist/elements/EFMedia/audioTasks/makeAudioInputTask.js +6 -5
  18. package/dist/elements/EFMedia/audioTasks/makeAudioSeekTask.js +1 -3
  19. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentFetchTask.js +1 -1
  20. package/dist/elements/EFMedia/audioTasks/makeAudioSegmentIdTask.js +1 -1
  21. package/dist/elements/EFMedia/audioTasks/makeAudioTimeDomainAnalysisTask.js +1 -1
  22. package/dist/elements/EFMedia/shared/AudioSpanUtils.js +4 -16
  23. package/dist/elements/EFMedia/shared/BufferUtils.js +2 -15
  24. package/dist/elements/EFMedia/shared/GlobalInputCache.js +0 -24
  25. package/dist/elements/EFMedia/shared/PrecisionUtils.js +0 -21
  26. package/dist/elements/EFMedia/shared/ThumbnailExtractor.js +0 -17
  27. package/dist/elements/EFMedia/tasks/makeMediaEngineTask.js +1 -10
  28. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.d.ts +29 -0
  29. package/dist/elements/EFMedia/videoTasks/MainVideoInputCache.js +32 -0
  30. package/dist/elements/EFMedia/videoTasks/ScrubInputCache.js +1 -15
  31. package/dist/elements/EFMedia/videoTasks/makeScrubVideoBufferTask.js +1 -7
  32. package/dist/elements/EFMedia/videoTasks/makeScrubVideoInputTask.js +8 -5
  33. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.js +12 -13
  34. package/dist/elements/EFMedia/videoTasks/makeScrubVideoSegmentIdTask.js +1 -1
  35. package/dist/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.js +134 -70
  36. package/dist/elements/EFMedia/videoTasks/makeVideoBufferTask.js +7 -11
  37. package/dist/elements/EFMedia.js +26 -24
  38. package/dist/elements/EFSourceMixin.js +5 -7
  39. package/dist/elements/EFSurface.js +6 -9
  40. package/dist/elements/EFTemporal.js +19 -37
  41. package/dist/elements/EFThumbnailStrip.js +16 -59
  42. package/dist/elements/EFTimegroup.js +95 -90
  43. package/dist/elements/EFVideo.d.ts +6 -2
  44. package/dist/elements/EFVideo.js +142 -107
  45. package/dist/elements/EFWaveform.js +18 -27
  46. package/dist/elements/SampleBuffer.js +2 -5
  47. package/dist/elements/TargetController.js +3 -3
  48. package/dist/elements/durationConverter.js +4 -4
  49. package/dist/elements/updateAnimations.js +14 -35
  50. package/dist/gui/ContextMixin.js +23 -52
  51. package/dist/gui/EFConfiguration.js +7 -7
  52. package/dist/gui/EFControls.js +5 -5
  53. package/dist/gui/EFFilmstrip.js +77 -98
  54. package/dist/gui/EFFitScale.js +5 -6
  55. package/dist/gui/EFFocusOverlay.js +4 -4
  56. package/dist/gui/EFPreview.js +4 -4
  57. package/dist/gui/EFScrubber.js +9 -9
  58. package/dist/gui/EFTimeDisplay.js +5 -5
  59. package/dist/gui/EFToggleLoop.js +4 -4
  60. package/dist/gui/EFTogglePlay.js +5 -5
  61. package/dist/gui/EFWorkbench.js +5 -5
  62. package/dist/gui/TWMixin2.js +1 -1
  63. package/dist/index.d.ts +1 -0
  64. package/dist/otel/BridgeSpanExporter.d.ts +13 -0
  65. package/dist/otel/BridgeSpanExporter.js +87 -0
  66. package/dist/otel/setupBrowserTracing.d.ts +12 -0
  67. package/dist/otel/setupBrowserTracing.js +30 -0
  68. package/dist/otel/tracingHelpers.d.ts +34 -0
  69. package/dist/otel/tracingHelpers.js +113 -0
  70. package/dist/transcoding/cache/RequestDeduplicator.js +0 -21
  71. package/dist/transcoding/cache/URLTokenDeduplicator.js +1 -21
  72. package/dist/transcoding/utils/UrlGenerator.js +2 -19
  73. package/dist/utils/LRUCache.js +6 -53
  74. package/package.json +10 -2
  75. package/src/elements/EFCaptions.browsertest.ts +2 -0
  76. package/src/elements/EFMedia/AssetMediaEngine.ts +65 -37
  77. package/src/elements/EFMedia/BaseMediaEngine.ts +110 -52
  78. package/src/elements/EFMedia/BufferedSeekingInput.ts +218 -101
  79. package/src/elements/EFMedia/audioTasks/makeAudioInputTask.ts +7 -3
  80. package/src/elements/EFMedia/videoTasks/MainVideoInputCache.ts +76 -0
  81. package/src/elements/EFMedia/videoTasks/makeScrubVideoInputTask.ts +16 -10
  82. package/src/elements/EFMedia/videoTasks/makeScrubVideoSeekTask.ts +7 -1
  83. package/src/elements/EFMedia/videoTasks/makeUnifiedVideoSeekTask.ts +222 -116
  84. package/src/elements/EFMedia.ts +16 -1
  85. package/src/elements/EFTimegroup.browsertest.ts +10 -8
  86. package/src/elements/EFTimegroup.ts +164 -76
  87. package/src/elements/EFVideo.browsertest.ts +19 -27
  88. package/src/elements/EFVideo.ts +203 -101
  89. package/src/otel/BridgeSpanExporter.ts +150 -0
  90. package/src/otel/setupBrowserTracing.ts +68 -0
  91. package/src/otel/tracingHelpers.ts +251 -0
  92. package/types.json +1 -1
@@ -1,11 +1,12 @@
1
1
  import { Task } from "@lit/task";
2
+ import { context, trace } from "@opentelemetry/api";
2
3
  import debug from "debug";
3
4
  import { css, html, type PropertyValueMap } from "lit";
4
5
  import { customElement, property, state } from "lit/decorators.js";
5
6
  import { createRef, ref } from "lit/directives/ref.js";
6
-
7
7
  import { DelayedLoadingState } from "../DelayedLoadingState.js";
8
8
  import { TWMixin } from "../gui/TWMixin.js";
9
+ import { withSpan, withSpanSync } from "../otel/tracingHelpers.js";
9
10
  import { makeScrubVideoBufferTask } from "./EFMedia/videoTasks/makeScrubVideoBufferTask.ts";
10
11
  import { makeScrubVideoInitSegmentFetchTask } from "./EFMedia/videoTasks/makeScrubVideoInitSegmentFetchTask.ts";
11
12
  import { makeScrubVideoInputTask } from "./EFMedia/videoTasks/makeScrubVideoInputTask.ts";
@@ -203,11 +204,40 @@ export class EFVideo extends TWMixin(EFMedia) {
203
204
  console.error("frameTask error", error);
204
205
  },
205
206
  onComplete: () => {},
206
- task: async ([_desiredSeekTimeMs]) => {
207
- this.unifiedVideoSeekTask.run();
208
- await this.unifiedVideoSeekTask.taskComplete;
209
- this.paintTask.run();
210
- await this.paintTask.taskComplete;
207
+ task: async ([_desiredSeekTimeMs], { signal }) => {
208
+ const t0 = performance.now();
209
+
210
+ await withSpan(
211
+ "video.frameTask",
212
+ {
213
+ elementId: this.id || "unknown",
214
+ desiredSeekTimeMs: _desiredSeekTimeMs,
215
+ src: this.src || "none",
216
+ },
217
+ undefined,
218
+ async (span) => {
219
+ const t1 = performance.now();
220
+ span.setAttribute("preworkMs", t1 - t0);
221
+
222
+ this.unifiedVideoSeekTask.run();
223
+ const t2 = performance.now();
224
+ span.setAttribute("seekRunMs", t2 - t1);
225
+
226
+ await this.unifiedVideoSeekTask.taskComplete;
227
+ const t3 = performance.now();
228
+ span.setAttribute("seekAwaitMs", t3 - t2);
229
+ if (signal.aborted) {
230
+ span.setAttribute("aborted", true);
231
+ return;
232
+ }
233
+
234
+ const t4 = performance.now();
235
+ this.paint(_desiredSeekTimeMs, span);
236
+ const t5 = performance.now();
237
+ span.setAttribute("paintMs", t5 - t4);
238
+ span.setAttribute("totalFrameMs", t5 - t0);
239
+ },
240
+ );
211
241
  },
212
242
  });
213
243
 
@@ -244,64 +274,92 @@ export class EFVideo extends TWMixin(EFMedia) {
244
274
  };
245
275
  }
246
276
 
247
- paintTask = new Task(this, {
248
- autoRun: false,
249
- args: () => [this.desiredSeekTimeMs] as const,
250
- onError: (error) => {
251
- console.error("paintTask error", error);
252
- },
253
- onComplete: () => {},
254
- task: async ([_seekToMs], { signal }) => {
255
- // Check if we're in production rendering mode vs preview mode
256
- const isProductionRendering = this.isInProductionRenderingMode();
257
-
258
- // Unified video system: smart routing to scrub or main, with background upgrades
259
- try {
260
- await this.unifiedVideoSeekTask.taskComplete;
261
- const videoSample = this.unifiedVideoSeekTask.value;
262
-
263
- if (videoSample) {
264
- const videoFrame = videoSample.toVideoFrame();
265
- try {
266
- this.displayFrame(videoFrame, _seekToMs);
267
- } finally {
268
- videoFrame.close();
277
+ /**
278
+ * Paint the current video frame to canvas
279
+ * Called by frameTask after seek is complete
280
+ */
281
+ paint(seekToMs: number, parentSpan?: any): void {
282
+ const parentContext = parentSpan
283
+ ? trace.setSpan(context.active(), parentSpan)
284
+ : undefined;
285
+
286
+ withSpanSync(
287
+ "video.paint",
288
+ {
289
+ elementId: this.id || "unknown",
290
+ seekToMs,
291
+ src: this.src || "none",
292
+ },
293
+ parentContext,
294
+ (span) => {
295
+ const t0 = performance.now();
296
+
297
+ // Check if we're in production rendering mode vs preview mode
298
+ const isProductionRendering = this.isInProductionRenderingMode();
299
+ const t1 = performance.now();
300
+ span.setAttribute("isProductionRendering", isProductionRendering);
301
+ span.setAttribute("modeCheckMs", t1 - t0);
302
+
303
+ // Unified video system: smart routing to scrub or main, with background upgrades
304
+ // Note: frameTask guarantees unifiedVideoSeekTask is complete before calling paint
305
+ try {
306
+ const t2 = performance.now();
307
+ const videoSample = this.unifiedVideoSeekTask.value;
308
+ span.setAttribute("hasVideoSample", !!videoSample);
309
+ span.setAttribute("valueAccessMs", t2 - t1);
310
+
311
+ if (videoSample) {
312
+ const t3 = performance.now();
313
+ const videoFrame = videoSample.toVideoFrame();
314
+ const t4 = performance.now();
315
+ span.setAttribute("toVideoFrameMs", t4 - t3);
316
+
317
+ try {
318
+ const t5 = performance.now();
319
+ this.displayFrame(videoFrame, seekToMs, span);
320
+ const t6 = performance.now();
321
+ span.setAttribute("displayFrameMs", t6 - t5);
322
+ } finally {
323
+ videoFrame.close();
324
+ }
269
325
  }
326
+ } catch (error) {
327
+ console.warn("Unified video pipeline error:", error);
270
328
  }
271
- } catch (error) {
272
- console.warn("Unified video pipeline error:", error);
273
- }
274
329
 
275
- // EF_FRAMEGEN-aware rendering mode detection
276
- if (!isProductionRendering) {
277
- // Preview mode: skip rendering during initialization to prevent artifacts
278
- if (
279
- !this.rootTimegroup ||
280
- (this.rootTimegroup.currentTimeMs === 0 &&
281
- this.desiredSeekTimeMs === 0)
282
- ) {
283
- return; // Skip initialization frame in preview mode
284
- }
285
- // Preview mode: proceed with rendering
286
- } else {
287
- // Production rendering mode: only render when EF_FRAMEGEN has explicitly started frame rendering
288
- // This prevents initialization frames before the actual render sequence begins
289
- if (!this.rootTimegroup) {
290
- return;
291
- }
330
+ // EF_FRAMEGEN-aware rendering mode detection
331
+ if (!isProductionRendering) {
332
+ // Preview mode: skip rendering during initialization to prevent artifacts
333
+ if (
334
+ !this.rootTimegroup ||
335
+ (this.rootTimegroup.currentTimeMs === 0 &&
336
+ this.desiredSeekTimeMs === 0)
337
+ ) {
338
+ span.setAttribute("skipped", "preview-initialization");
339
+ return; // Skip initialization frame in preview mode
340
+ }
341
+ // Preview mode: proceed with rendering
342
+ } else {
343
+ // Production rendering mode: only render when EF_FRAMEGEN has explicitly started frame rendering
344
+ // This prevents initialization frames before the actual render sequence begins
345
+ if (!this.rootTimegroup) {
346
+ span.setAttribute("skipped", "no-root-timegroup");
347
+ return;
348
+ }
292
349
 
293
- if (!this.isFrameRenderingActive()) {
294
- return; // Wait for EF_FRAMEGEN to start frame sequence
295
- }
350
+ if (!this.isFrameRenderingActive()) {
351
+ span.setAttribute("skipped", "frame-rendering-not-active");
352
+ return; // Wait for EF_FRAMEGEN to start frame sequence
353
+ }
296
354
 
297
- // Production mode: EF_FRAMEGEN has started frame sequence, proceed with rendering
298
- }
355
+ // Production mode: EF_FRAMEGEN has started frame sequence, proceed with rendering
356
+ }
299
357
 
300
- if (signal.aborted) {
301
- return;
302
- }
303
- },
304
- });
358
+ const tEnd = performance.now();
359
+ span.setAttribute("totalPaintMs", tEnd - t0);
360
+ },
361
+ );
362
+ }
305
363
 
306
364
  /**
307
365
  * Clear the canvas when element becomes inactive
@@ -318,54 +376,98 @@ export class EFVideo extends TWMixin(EFMedia) {
318
376
  /**
319
377
  * Display a video frame on the canvas
320
378
  */
321
- displayFrame(frame: VideoFrame, seekToMs: number): number {
322
- log("trace: displayFrame start", { seekToMs, frameFormat: frame.format });
323
- if (!this.canvasElement) {
324
- log("trace: displayFrame aborted - no canvas element");
325
- throw new Error(
326
- `Frame display failed: Canvas element is not available at time ${seekToMs}ms. The video component may not be properly initialized.`,
327
- );
328
- }
329
-
330
- const ctx = this.canvasElement.getContext("2d");
331
- if (!ctx) {
332
- log("trace: displayFrame aborted - no canvas context");
333
- throw new Error(
334
- `Frame display failed: Unable to get 2D canvas context at time ${seekToMs}ms. This may indicate a browser compatibility issue or canvas corruption.`,
335
- );
336
- }
379
+ displayFrame(frame: VideoFrame, seekToMs: number, parentSpan?: any): void {
380
+ const parentContext = parentSpan
381
+ ? trace.setSpan(context.active(), parentSpan)
382
+ : undefined;
383
+
384
+ withSpanSync(
385
+ "video.displayFrame",
386
+ {
387
+ elementId: this.id || "unknown",
388
+ seekToMs,
389
+ format: frame.format || "unknown",
390
+ width: frame.codedWidth,
391
+ height: frame.codedHeight,
392
+ },
393
+ parentContext,
394
+ (span) => {
395
+ const t0 = performance.now();
337
396
 
338
- if (frame?.codedWidth && frame?.codedHeight) {
339
- if (
340
- this.canvasElement.width !== frame.codedWidth ||
341
- this.canvasElement.height !== frame.codedHeight
342
- ) {
343
- log("trace: updating canvas dimensions", {
344
- width: frame.codedWidth,
345
- height: frame.codedHeight,
397
+ log("trace: displayFrame start", {
398
+ seekToMs,
399
+ frameFormat: frame.format,
346
400
  });
347
- this.canvasElement.width = frame.codedWidth;
348
- this.canvasElement.height = frame.codedHeight;
349
- }
350
- }
351
401
 
352
- if (frame.format === null) {
353
- log("trace: displayFrame aborted - null frame format");
354
- throw new Error(
355
- `Frame display failed: Video frame has null format at time ${seekToMs}ms. This indicates corrupted or incompatible video data.`,
356
- );
357
- }
402
+ if (!this.canvasElement) {
403
+ log("trace: displayFrame aborted - no canvas element");
404
+ throw new Error(
405
+ `Frame display failed: Canvas element is not available at time ${seekToMs}ms. The video component may not be properly initialized.`,
406
+ );
407
+ }
408
+ const t1 = performance.now();
409
+ span.setAttribute("getCanvasMs", Math.round((t1 - t0) * 100) / 100);
410
+
411
+ const ctx = this.canvasElement.getContext("2d");
412
+ const t2 = performance.now();
413
+ span.setAttribute("getCtxMs", Math.round((t2 - t1) * 100) / 100);
414
+
415
+ if (!ctx) {
416
+ log("trace: displayFrame aborted - no canvas context");
417
+ throw new Error(
418
+ `Frame display failed: Unable to get 2D canvas context at time ${seekToMs}ms. This may indicate a browser compatibility issue or canvas corruption.`,
419
+ );
420
+ }
358
421
 
359
- ctx.drawImage(
360
- frame,
361
- 0,
362
- 0,
363
- this.canvasElement.width,
364
- this.canvasElement.height,
365
- );
366
- log("trace: frame drawn to canvas", { seekToMs });
422
+ let resized = false;
423
+ if (frame?.codedWidth && frame?.codedHeight) {
424
+ if (
425
+ this.canvasElement.width !== frame.codedWidth ||
426
+ this.canvasElement.height !== frame.codedHeight
427
+ ) {
428
+ log("trace: updating canvas dimensions", {
429
+ width: frame.codedWidth,
430
+ height: frame.codedHeight,
431
+ });
432
+ this.canvasElement.width = frame.codedWidth;
433
+ this.canvasElement.height = frame.codedHeight;
434
+ resized = true;
435
+ const t3 = performance.now();
436
+ span.setAttribute("resizeMs", Math.round((t3 - t2) * 100) / 100);
437
+ }
438
+ }
439
+ span.setAttribute("canvasResized", resized);
440
+
441
+ if (frame.format === null) {
442
+ log("trace: displayFrame aborted - null frame format");
443
+ throw new Error(
444
+ `Frame display failed: Video frame has null format at time ${seekToMs}ms. This indicates corrupted or incompatible video data.`,
445
+ );
446
+ }
367
447
 
368
- return seekToMs;
448
+ const tDrawStart = performance.now();
449
+ ctx.drawImage(
450
+ frame,
451
+ 0,
452
+ 0,
453
+ this.canvasElement.width,
454
+ this.canvasElement.height,
455
+ );
456
+ const tDrawEnd = performance.now();
457
+ span.setAttribute(
458
+ "drawImageMs",
459
+ Math.round((tDrawEnd - tDrawStart) * 100) / 100,
460
+ );
461
+ span.setAttribute(
462
+ "totalDisplayMs",
463
+ Math.round((tDrawEnd - t0) * 100) / 100,
464
+ );
465
+ span.setAttribute("canvasWidth", this.canvasElement.width);
466
+ span.setAttribute("canvasHeight", this.canvasElement.height);
467
+
468
+ log("trace: frame drawn to canvas", { seekToMs });
469
+ },
470
+ );
369
471
  }
370
472
 
371
473
  /**
@@ -0,0 +1,150 @@
1
+ import { type ExportResult, ExportResultCode } from "@opentelemetry/core";
2
+ import type { ReadableSpan, SpanExporter } from "@opentelemetry/sdk-trace-base";
3
+
4
+ function toHex(value: unknown): string {
5
+ if (typeof value === "string") return value;
6
+ if (Array.isArray(value)) {
7
+ return value
8
+ .map((b) => {
9
+ const byte = typeof b === "number" ? b : 0;
10
+ return byte.toString(16).padStart(2, "0");
11
+ })
12
+ .join("");
13
+ }
14
+ if (ArrayBuffer.isView(value)) {
15
+ return Array.from(value as Uint8Array)
16
+ .map((b) => b.toString(16).padStart(2, "0"))
17
+ .join("");
18
+ }
19
+ return String(value);
20
+ }
21
+
22
+ interface OtlpAttributeValue {
23
+ stringValue?: string;
24
+ intValue?: number;
25
+ doubleValue?: number;
26
+ boolValue?: boolean;
27
+ arrayValue?: { values: OtlpAttributeValue[] };
28
+ }
29
+
30
+ function convertAttribute(value: unknown): OtlpAttributeValue {
31
+ if (typeof value === "string") return { stringValue: value };
32
+ if (typeof value === "number")
33
+ return Number.isInteger(value)
34
+ ? { intValue: value }
35
+ : { doubleValue: value };
36
+ if (typeof value === "boolean") return { boolValue: value };
37
+ if (Array.isArray(value))
38
+ return { arrayValue: { values: value.map(convertAttribute) } };
39
+ return { stringValue: String(value) };
40
+ }
41
+
42
+ interface BridgeWithSpanExport {
43
+ exportSpans?: (endpoint: string, payload: string) => void;
44
+ }
45
+
46
+ export class BridgeSpanExporter implements SpanExporter {
47
+ private bridge: BridgeWithSpanExport;
48
+ private endpoint: string;
49
+
50
+ constructor(bridge: BridgeWithSpanExport, endpoint: string) {
51
+ this.bridge = bridge;
52
+ this.endpoint = endpoint;
53
+ }
54
+
55
+ export(
56
+ spans: ReadableSpan[],
57
+ resultCallback: (result: ExportResult) => void,
58
+ ): void {
59
+ if (!this.bridge?.exportSpans) {
60
+ resultCallback({ code: ExportResultCode.FAILED });
61
+ return;
62
+ }
63
+
64
+ try {
65
+ const otlpPayload = {
66
+ resourceSpans: [
67
+ {
68
+ resource: {
69
+ attributes: Object.entries(
70
+ spans[0]?.resource?.attributes || {},
71
+ ).map(([key, value]) => ({
72
+ key,
73
+ value: convertAttribute(value),
74
+ })),
75
+ },
76
+ scopeSpans: [
77
+ {
78
+ scope: {
79
+ name: "telecine-browser",
80
+ version: "1.0.0",
81
+ },
82
+ spans: spans.map((span) => {
83
+ const ctx = span.spanContext();
84
+ return {
85
+ traceId: toHex(ctx.traceId),
86
+ spanId: toHex(ctx.spanId),
87
+ parentSpanId: span.parentSpanId
88
+ ? toHex(span.parentSpanId)
89
+ : undefined,
90
+ name: span.name,
91
+ kind: span.kind,
92
+ startTimeUnixNano: String(
93
+ span.startTime[0] * 1_000_000_000 + span.startTime[1],
94
+ ),
95
+ endTimeUnixNano: String(
96
+ span.endTime[0] * 1_000_000_000 + span.endTime[1],
97
+ ),
98
+ attributes: Object.entries(span.attributes).map(
99
+ ([key, value]) => ({
100
+ key,
101
+ value: convertAttribute(value),
102
+ }),
103
+ ),
104
+ status: span.status,
105
+ events: span.events.map((event) => ({
106
+ timeUnixNano: String(
107
+ event.time[0] * 1_000_000_000 + event.time[1],
108
+ ),
109
+ name: event.name,
110
+ attributes: Object.entries(event.attributes || {}).map(
111
+ ([key, value]) => ({
112
+ key,
113
+ value: convertAttribute(value),
114
+ }),
115
+ ),
116
+ })),
117
+ links: span.links.map((link) => ({
118
+ traceId: toHex(link.context.traceId),
119
+ spanId: toHex(link.context.spanId),
120
+ attributes: Object.entries(link.attributes || {}).map(
121
+ ([key, value]) => ({
122
+ key,
123
+ value: convertAttribute(value),
124
+ }),
125
+ ),
126
+ })),
127
+ };
128
+ }),
129
+ },
130
+ ],
131
+ },
132
+ ],
133
+ };
134
+
135
+ const serializedPayload = JSON.stringify(otlpPayload);
136
+
137
+ this.bridge.exportSpans(this.endpoint, serializedPayload);
138
+ resultCallback({ code: ExportResultCode.SUCCESS });
139
+ } catch (error) {
140
+ resultCallback({
141
+ code: ExportResultCode.FAILED,
142
+ error: error instanceof Error ? error : new Error(String(error)),
143
+ });
144
+ }
145
+ }
146
+
147
+ shutdown(): Promise<void> {
148
+ return Promise.resolve();
149
+ }
150
+ }
@@ -0,0 +1,68 @@
1
+ import { ZoneContextManager } from "@opentelemetry/context-zone";
2
+ import { Resource } from "@opentelemetry/resources";
3
+ import {
4
+ BatchSpanProcessor,
5
+ SimpleSpanProcessor,
6
+ } from "@opentelemetry/sdk-trace-base";
7
+ import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
8
+ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
9
+ import { BridgeSpanExporter } from "./BridgeSpanExporter.js";
10
+
11
+ let isInitialized = false;
12
+ let provider: WebTracerProvider | null = null;
13
+
14
+ interface BridgeWithSpanExport {
15
+ exportSpans?: (endpoint: string, payload: string) => void;
16
+ }
17
+
18
+ export interface BrowserTracingConfig {
19
+ otelEndpoint: string;
20
+ serviceName?: string;
21
+ bridge?: BridgeWithSpanExport;
22
+ useBatching?: boolean;
23
+ }
24
+
25
+ export function setupBrowserTracing(config: BrowserTracingConfig): void {
26
+ if (isInitialized) {
27
+ return;
28
+ }
29
+
30
+ try {
31
+ if (!config.bridge) {
32
+ throw new Error("Bridge is required for browser tracing");
33
+ }
34
+
35
+ const exporter = new BridgeSpanExporter(config.bridge, config.otelEndpoint);
36
+
37
+ // Use batching to reduce overhead (spans exported in groups rather than one-by-one)
38
+ const spanProcessor = config.useBatching
39
+ ? new BatchSpanProcessor(exporter, {
40
+ maxQueueSize: 100,
41
+ maxExportBatchSize: 10,
42
+ scheduledDelayMillis: 500,
43
+ })
44
+ : new SimpleSpanProcessor(exporter);
45
+
46
+ provider = new WebTracerProvider({
47
+ resource: new Resource({
48
+ [ATTR_SERVICE_NAME]: config.serviceName || "telecine-browser",
49
+ }),
50
+ spanProcessors: [spanProcessor],
51
+ });
52
+
53
+ // Register with ZoneContextManager for browser context propagation
54
+ // ZoneContextManager includes zone.js for async context tracking
55
+ provider.register({
56
+ contextManager: new ZoneContextManager(),
57
+ });
58
+
59
+ isInitialized = true;
60
+ } catch (error) {
61
+ console.error("Failed to initialize browser tracing:", error);
62
+ throw error;
63
+ }
64
+ }
65
+
66
+ export function isBrowserTracingInitialized(): boolean {
67
+ return isInitialized;
68
+ }