@vivix-ai/ivi-frontend-sdk 0.3.9 → 0.3.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/dist/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var iviSdkTs = require('@vivix-ai/ivi-sdk-ts');
4
+ var api = require('@opentelemetry/api');
4
5
  var websocket = require('@vivix-ai/ivi-sdk-ts/transports/websocket');
5
6
  var react = require('react');
6
7
  var jsxRuntime = require('react/jsx-runtime');
@@ -1141,6 +1142,94 @@ var ConversationManager = class {
1141
1142
  ];
1142
1143
  }
1143
1144
  };
1145
+ var DEFAULT_TRACER_NAME = "@vivix-ai/ivi-frontend-sdk";
1146
+ function getIviTelemetryTracer(options) {
1147
+ return options?.tracer ?? api.trace.getTracer(DEFAULT_TRACER_NAME);
1148
+ }
1149
+ function getIviTelemetryContext(options) {
1150
+ return options?.context ?? api.context.active();
1151
+ }
1152
+ function startIviTelemetrySpan(name, options, attributes) {
1153
+ const tracer = getIviTelemetryTracer(options);
1154
+ const parentContext = getIviTelemetryContext(options);
1155
+ const span = tracer.startSpan(
1156
+ name,
1157
+ {
1158
+ kind: api.SpanKind.INTERNAL,
1159
+ attributes: mergeIviTelemetryAttributes(options?.attributes, attributes)
1160
+ },
1161
+ parentContext
1162
+ );
1163
+ return {
1164
+ span,
1165
+ context: api.trace.setSpan(parentContext, span)
1166
+ };
1167
+ }
1168
+ function endIviTelemetrySpan(span, attributes) {
1169
+ const cleaned = cleanIviTelemetryAttributes(attributes);
1170
+ if (cleaned) {
1171
+ span.setAttributes(cleaned);
1172
+ }
1173
+ span.setStatus({ code: api.SpanStatusCode.OK });
1174
+ span.end();
1175
+ }
1176
+ function failIviTelemetrySpan(span, error, attributes) {
1177
+ const cleaned = cleanIviTelemetryAttributes(attributes);
1178
+ if (cleaned) {
1179
+ span.setAttributes(cleaned);
1180
+ }
1181
+ recordIviTelemetryError(span, error);
1182
+ span.end();
1183
+ }
1184
+ function recordIviTelemetrySpan(name, options, attributes, error) {
1185
+ const { span } = startIviTelemetrySpan(name, options, attributes);
1186
+ if (error === void 0) {
1187
+ span.setStatus({ code: api.SpanStatusCode.OK });
1188
+ } else {
1189
+ recordIviTelemetryError(span, error);
1190
+ }
1191
+ span.end();
1192
+ }
1193
+ function withIviTelemetryContext(options, context, attributes) {
1194
+ return {
1195
+ ...options,
1196
+ context,
1197
+ attributes: mergeIviTelemetryAttributes(options?.attributes, attributes)
1198
+ };
1199
+ }
1200
+ function mergeIviTelemetryAttributes(base, next) {
1201
+ return cleanIviTelemetryAttributes({
1202
+ ...base ?? {},
1203
+ ...next ?? {}
1204
+ });
1205
+ }
1206
+ function cleanIviTelemetryAttributes(attributes) {
1207
+ const cleaned = {};
1208
+ Object.entries(attributes ?? {}).forEach(([key, value]) => {
1209
+ if (isIviTelemetryAttributeValue(value)) {
1210
+ cleaned[key] = value;
1211
+ }
1212
+ });
1213
+ return Object.keys(cleaned).length > 0 ? cleaned : void 0;
1214
+ }
1215
+ function recordIviTelemetryError(span, error) {
1216
+ const message = error instanceof Error ? error.message : String(error);
1217
+ span.recordException(error instanceof Error ? error : message);
1218
+ span.setStatus({
1219
+ code: api.SpanStatusCode.ERROR,
1220
+ message
1221
+ });
1222
+ }
1223
+ function isIviTelemetryAttributeValue(value) {
1224
+ if (typeof value === "string" || typeof value === "boolean") return true;
1225
+ if (typeof value === "number") return Number.isFinite(value);
1226
+ if (!Array.isArray(value)) return false;
1227
+ const nonNullValues = value.filter((item) => item !== null && item !== void 0);
1228
+ if (nonNullValues.length === 0) return true;
1229
+ const firstType = typeof nonNullValues[0];
1230
+ if (firstType !== "string" && firstType !== "number" && firstType !== "boolean") return false;
1231
+ return nonNullValues.every((item) => typeof item === firstType && (typeof item !== "number" || Number.isFinite(item)));
1232
+ }
1144
1233
 
1145
1234
  // src/runtime/managers/trtc-source-manager.ts
1146
1235
  var TAG = "[IVI-TRTC]";
@@ -1151,10 +1240,11 @@ var DEFAULT_DENOISER_OPTIONS = {
1151
1240
  mode: "normal"
1152
1241
  };
1153
1242
  var TrtcSourceManager = class {
1154
- constructor(onLog, onTrtcEvent, aiDenoiser) {
1243
+ constructor(onLog, onTrtcEvent, aiDenoiser, telemetry) {
1155
1244
  this.onLog = onLog;
1156
1245
  this.onTrtcEvent = onTrtcEvent;
1157
1246
  this.aiDenoiser = aiDenoiser;
1247
+ this.telemetry = telemetry;
1158
1248
  this.sessions = /* @__PURE__ */ new Map();
1159
1249
  this.sourceSessionKeys = /* @__PURE__ */ new Map();
1160
1250
  this.listeners = /* @__PURE__ */ new Map();
@@ -1450,6 +1540,11 @@ var TrtcSourceManager = class {
1450
1540
  const client = TRTC.create();
1451
1541
  const onRemoteVideoAvailable = (event) => {
1452
1542
  this.log("info", `\u8FDC\u7AEF\u89C6\u9891\u53EF\u7528 source=${session.sourceId} userId=${event.userId} streamType=${event.streamType}`);
1543
+ recordIviTelemetrySpan(
1544
+ "ivi.media.trtc.remote_video_available",
1545
+ this.telemetry,
1546
+ buildTrtcMediaTelemetryAttributes(session.sourceId, event.userId, event.streamType)
1547
+ );
1453
1548
  const remoteVideoKey = buildRemoteVideoKey(event.userId, String(event.streamType));
1454
1549
  session.remoteVideoStreams.add(remoteVideoKey);
1455
1550
  if (!session.hasEverReceivedRemoteVideo) {
@@ -1557,6 +1652,16 @@ var TrtcSourceManager = class {
1557
1652
  return;
1558
1653
  }
1559
1654
  binding.startingVideoKeys.add(remoteVideoKey);
1655
+ const telemetryAttributes = buildTrtcMediaTelemetryAttributes(
1656
+ binding.sourceId,
1657
+ userId,
1658
+ streamType
1659
+ );
1660
+ const telemetrySpan = startIviTelemetrySpan(
1661
+ "ivi.media.trtc.start_remote_video",
1662
+ this.telemetry,
1663
+ telemetryAttributes
1664
+ );
1560
1665
  try {
1561
1666
  this.log("info", `\u5F00\u59CB\u6E32\u67D3\u8FDC\u7AEF\u89C6\u9891 source=${binding.sourceId} view=${remoteVideoKey} userId=${userId} streamType=${streamType}`);
1562
1667
  await client.startRemoteVideo({
@@ -1565,14 +1670,24 @@ var TrtcSourceManager = class {
1565
1670
  view: binding.container,
1566
1671
  option: { fillMode: "contain" }
1567
1672
  });
1673
+ endIviTelemetrySpan(telemetrySpan.span);
1674
+ recordIviTelemetrySpan(
1675
+ "ivi.media.trtc.remote_video_rendered",
1676
+ this.telemetry,
1677
+ telemetryAttributes
1678
+ );
1568
1679
  this.log("info", `\u8FDC\u7AEF\u89C6\u9891\u6E32\u67D3\u5B8C\u6210 source=${binding.sourceId} userId=${userId} streamType=${streamType}`);
1569
1680
  binding.startedVideoKeys.add(remoteVideoKey);
1570
1681
  enforceContainMedia(binding.container);
1571
1682
  } catch (error) {
1572
1683
  if (error instanceof Error && /already started|OPERATION_ABORT/i.test(error.message)) {
1573
1684
  binding.startedVideoKeys.add(remoteVideoKey);
1685
+ endIviTelemetrySpan(telemetrySpan.span, {
1686
+ "ivi.media.trtc.start_remote_video.result": "already_started"
1687
+ });
1574
1688
  return;
1575
1689
  }
1690
+ failIviTelemetrySpan(telemetrySpan.span, error);
1576
1691
  this.log("warn", `\u8FDC\u7AEF\u89C6\u9891\u6E32\u67D3\u5931\u8D25 userId=${userId} streamType=${streamType}`, error);
1577
1692
  } finally {
1578
1693
  binding.startingVideoKeys.delete(remoteVideoKey);
@@ -1786,6 +1901,13 @@ function getAIDenoiserModeValue(mode) {
1786
1901
  function buildRemoteVideoKey(userId, streamType) {
1787
1902
  return `${userId}::${streamType}`;
1788
1903
  }
1904
+ function buildTrtcMediaTelemetryAttributes(sourceId, userId, streamType) {
1905
+ return {
1906
+ "ivi.source.id": sourceId,
1907
+ "ivi.trtc.user_id": userId,
1908
+ "ivi.trtc.stream_type": String(streamType)
1909
+ };
1910
+ }
1789
1911
  function parseRemoteVideoKey(key) {
1790
1912
  const separatorIndex = key.indexOf("::");
1791
1913
  if (separatorIndex < 0) {
@@ -1848,9 +1970,10 @@ function describeLivekitRoom(livekit) {
1848
1970
  // src/runtime/managers/livekit-source-manager.ts
1849
1971
  var TAG2 = "[IVI-LIVEKIT]";
1850
1972
  var LivekitSourceManager = class {
1851
- constructor(onLog, onRemoteVideoAvailable) {
1973
+ constructor(onLog, onRemoteVideoAvailable, telemetry) {
1852
1974
  this.onLog = onLog;
1853
1975
  this.onRemoteVideoAvailable = onRemoteVideoAvailable;
1976
+ this.telemetry = telemetry;
1854
1977
  this.sessions = /* @__PURE__ */ new Map();
1855
1978
  this.listeners = /* @__PURE__ */ new Map();
1856
1979
  }
@@ -2072,6 +2195,15 @@ var LivekitSourceManager = class {
2072
2195
  kind
2073
2196
  });
2074
2197
  if (kind === "video" && !session.hasEverReceivedRemoteVideo) {
2198
+ recordIviTelemetrySpan(
2199
+ "ivi.media.livekit.remote_video_available",
2200
+ this.telemetry,
2201
+ {
2202
+ "ivi.source.id": session.sourceId,
2203
+ "ivi.livekit.participant_identity": participant.identity,
2204
+ "ivi.livekit.track_sid": track.sid
2205
+ }
2206
+ );
2075
2207
  session.hasEverReceivedRemoteVideo = true;
2076
2208
  for (const waiter of session.remoteVideoWaiters) {
2077
2209
  waiter(true);
@@ -2293,11 +2425,16 @@ var IviRuntimeCoordinator = class {
2293
2425
  this.trtcSourceManager = new TrtcSourceManager(
2294
2426
  this.config.onLog,
2295
2427
  (event) => this.onTrtcManagerEvent(event),
2296
- this.config.trtcAIDenoiser
2428
+ this.config.trtcAIDenoiser,
2429
+ this.config.telemetry
2430
+ );
2431
+ this.livekitSourceManager = new LivekitSourceManager(
2432
+ this.config.onLog,
2433
+ () => {
2434
+ this.recomposeBootstrapState();
2435
+ },
2436
+ this.config.telemetry
2297
2437
  );
2298
- this.livekitSourceManager = new LivekitSourceManager(this.config.onLog, () => {
2299
- this.recomposeBootstrapState();
2300
- });
2301
2438
  this.sessionHandler = new SessionEventHandler(this.sessionManager, {
2302
2439
  onSessionCreated: (event) => this.onSessionCreated(event),
2303
2440
  onSessionEnded: (event) => this.onSessionEnded(event)
@@ -2334,15 +2471,27 @@ var IviRuntimeCoordinator = class {
2334
2471
  * 启动后状态会从 idle/stopped 进入 connecting,连接成功后由事件驱动进入后续状态。
2335
2472
  */
2336
2473
  async start() {
2337
- this.setState({
2338
- ...this.state,
2339
- status: "connecting"
2474
+ const telemetrySpan = startIviTelemetrySpan("ivi.runtime.start", this.config.telemetry, {
2475
+ "ivi.runtime.status": this.state.status,
2476
+ "ivi.session.id": this.state.session?.id
2340
2477
  });
2341
- this.dispatcher.start();
2342
2478
  try {
2479
+ this.setState({
2480
+ ...this.state,
2481
+ status: "connecting"
2482
+ });
2483
+ this.dispatcher.start();
2343
2484
  await this.client.connect();
2485
+ endIviTelemetrySpan(telemetrySpan.span, {
2486
+ "ivi.runtime.status": this.state.status,
2487
+ "ivi.session.id": this.state.session?.id
2488
+ });
2344
2489
  } catch (error) {
2345
2490
  this.stop();
2491
+ failIviTelemetrySpan(telemetrySpan.span, error, {
2492
+ "ivi.runtime.status": this.state.status,
2493
+ "ivi.session.id": this.state.session?.id
2494
+ });
2346
2495
  throw error;
2347
2496
  }
2348
2497
  }
@@ -2350,13 +2499,31 @@ var IviRuntimeCoordinator = class {
2350
2499
  * 停止协调器并断开实时连接,同时重置运行态数据为 stopped。
2351
2500
  */
2352
2501
  stop() {
2353
- this.dispatcher.stop();
2354
- this.client.disconnect();
2355
- this.resetStoppedState();
2502
+ const telemetrySpan = startIviTelemetrySpan("ivi.runtime.stop", this.config.telemetry, {
2503
+ "ivi.runtime.status": this.state.status,
2504
+ "ivi.session.id": this.state.session?.id
2505
+ });
2506
+ try {
2507
+ this.dispatcher.stop();
2508
+ this.client.disconnect();
2509
+ this.resetStoppedState();
2510
+ endIviTelemetrySpan(telemetrySpan.span, {
2511
+ "ivi.runtime.status": this.state.status
2512
+ });
2513
+ } catch (error) {
2514
+ failIviTelemetrySpan(telemetrySpan.span, error, {
2515
+ "ivi.runtime.status": this.state.status,
2516
+ "ivi.session.id": this.state.session?.id
2517
+ });
2518
+ throw error;
2519
+ }
2356
2520
  }
2357
2521
  getState() {
2358
2522
  return this.state;
2359
2523
  }
2524
+ getTelemetryOptions() {
2525
+ return this.config.telemetry;
2526
+ }
2360
2527
  onStateChange(listener) {
2361
2528
  this.stateListeners.add(listener);
2362
2529
  return () => {
@@ -2647,7 +2814,16 @@ var IviRuntimeCoordinator = class {
2647
2814
  if (this.state.status === composedNextState.status && this.state.session === composedNextState.session && this.state.stage === composedNextState.stage && this.state.tracks === composedNextState.tracks && this.state.sources === composedNextState.sources && this.state.streams === composedNextState.streams && this.state.conversationItems === composedNextState.conversationItems && this.state.conversations === composedNextState.conversations && isSameBootstrapState(this.state.bootstrap, composedNextState.bootstrap)) {
2648
2815
  return;
2649
2816
  }
2817
+ const previousState = this.state;
2650
2818
  this.state = composedNextState;
2819
+ if (previousState.status !== composedNextState.status) {
2820
+ recordIviTelemetrySpan("ivi.runtime.state_change", this.config.telemetry, {
2821
+ "ivi.runtime.status.previous": previousState.status,
2822
+ "ivi.runtime.status.next": composedNextState.status,
2823
+ "ivi.runtime.state_change.reason": "state_update",
2824
+ "ivi.session.id": composedNextState.session?.id ?? previousState.session?.id
2825
+ });
2826
+ }
2651
2827
  this.stateListeners.forEach((listener) => listener(this.state));
2652
2828
  }
2653
2829
  composeBootstrapState(nextState) {
@@ -2930,44 +3106,101 @@ async function createIVISession(request, options) {
2930
3106
  if (!fetchImpl) {
2931
3107
  throw new Error("fetch is not available. Pass options.fetch to createIVISession.");
2932
3108
  }
2933
- const requestBody = toCpCreateIVISessionRequestBody(request);
2934
- const response = await fetchImpl(resolveCreateIVISessionUrl(options), {
2935
- method: "POST",
2936
- headers: await buildHeaders(options.headers),
2937
- body: JSON.stringify(requestBody),
2938
- credentials: options.credentials,
2939
- signal: options.signal
2940
- });
2941
- const responseBody = await readJsonResponse(response);
2942
- if (!response.ok) {
2943
- throw new IviCreateIVISessionError(response, responseBody);
3109
+ const telemetrySpan = startIviTelemetrySpan(
3110
+ "ivi.session.create",
3111
+ options.telemetry,
3112
+ buildCreateSessionTelemetryAttributes(request)
3113
+ );
3114
+ try {
3115
+ const requestBody = toCpCreateIVISessionRequestBody(request);
3116
+ const headers = await buildHeaders(options.headers);
3117
+ injectIviTelemetryHeaders(headers, telemetrySpan.context);
3118
+ const response = await fetchImpl(resolveCreateIVISessionUrl(options), {
3119
+ method: "POST",
3120
+ headers,
3121
+ body: JSON.stringify(requestBody),
3122
+ credentials: options.credentials,
3123
+ signal: options.signal
3124
+ });
3125
+ const responseBody = await readJsonResponse(response);
3126
+ if (!response.ok) {
3127
+ const error = new IviCreateIVISessionError(response, responseBody);
3128
+ failIviTelemetrySpan(telemetrySpan.span, error, {
3129
+ "http.response.status_code": response.status,
3130
+ "http.response.status_text": response.statusText
3131
+ });
3132
+ throw error;
3133
+ }
3134
+ const result = normalizeCreateIVISessionResponse(responseBody, request, requestBody);
3135
+ endIviTelemetrySpan(telemetrySpan.span, {
3136
+ "http.response.status_code": response.status,
3137
+ "ivi.session.id": result.iviSessionId,
3138
+ "ivi.prebuild.playback.kind": result.prebuiltPlayback?.kind
3139
+ });
3140
+ return result;
3141
+ } catch (error) {
3142
+ if (!(error instanceof IviCreateIVISessionError)) {
3143
+ failIviTelemetrySpan(telemetrySpan.span, error);
3144
+ }
3145
+ throw error;
2944
3146
  }
2945
- return normalizeCreateIVISessionResponse(responseBody, request, requestBody);
2946
3147
  }
2947
3148
  function createRuntimeFromSession(session, options = {}) {
2948
- const websocketUrl = buildWebSocketUrl(session.endpoint, options.websocket);
2949
- const client = new iviSdkTs.IviClient({
2950
- ...options.clientConfig ?? {},
2951
- transport: new websocket.WebSocketTransport({
2952
- url: websocketUrl,
2953
- sessionId: session.iviSessionId,
2954
- protocols: options.websocket?.protocols,
2955
- socketFactory: options.websocket?.socketFactory
2956
- })
3149
+ const telemetrySpan = startIviTelemetrySpan("ivi.runtime.create", options.telemetry, {
3150
+ "ivi.session.id": session.iviSessionId,
3151
+ "ivi.prebuild.playback.kind": session.prebuiltPlayback?.kind
2957
3152
  });
2958
- const runtime = new IviRuntimeCoordinator(client, options.runtimeConfig);
2959
- if (options.fastStart !== false && session.prebuiltPlayback) {
2960
- runtime.setBootstrapSource(toBootstrapSource(session.prebuiltPlayback, options));
3153
+ try {
3154
+ const websocketUrl = buildWebSocketUrl(session.endpoint, options.websocket);
3155
+ const childTelemetry = withIviTelemetryContext(options.telemetry, telemetrySpan.context, {
3156
+ "ivi.session.id": session.iviSessionId
3157
+ });
3158
+ const clientTelemetry = mergeIviTelemetryOptions(
3159
+ options.clientConfig?.telemetry,
3160
+ childTelemetry
3161
+ );
3162
+ const runtimeTelemetry = mergeIviTelemetryOptions(
3163
+ options.runtimeConfig?.telemetry,
3164
+ childTelemetry
3165
+ );
3166
+ const client = new iviSdkTs.IviClient({
3167
+ ...options.clientConfig ?? {},
3168
+ telemetry: clientTelemetry,
3169
+ transport: new websocket.WebSocketTransport({
3170
+ url: websocketUrl,
3171
+ sessionId: session.iviSessionId,
3172
+ protocols: options.websocket?.protocols,
3173
+ socketFactory: options.websocket?.socketFactory
3174
+ })
3175
+ });
3176
+ const runtime = new IviRuntimeCoordinator(client, {
3177
+ ...options.runtimeConfig ?? {},
3178
+ telemetry: runtimeTelemetry
3179
+ });
3180
+ if (options.fastStart !== false && session.prebuiltPlayback) {
3181
+ runtime.setBootstrapSource(toBootstrapSource(session.prebuiltPlayback, options));
3182
+ }
3183
+ endIviTelemetrySpan(telemetrySpan.span);
3184
+ return {
3185
+ session,
3186
+ client,
3187
+ runtime
3188
+ };
3189
+ } catch (error) {
3190
+ failIviTelemetrySpan(telemetrySpan.span, error);
3191
+ throw error;
2961
3192
  }
2962
- return {
2963
- session,
2964
- client,
2965
- runtime
2966
- };
2967
3193
  }
2968
3194
  async function createIVISessionRuntime(request, options) {
2969
- const session = await createIVISession(request, options.http);
2970
- return createRuntimeFromSession(session, options);
3195
+ const telemetry = mergeIviTelemetryOptions(options.telemetry, options.http.telemetry);
3196
+ const session = await createIVISession(request, {
3197
+ ...options.http,
3198
+ telemetry
3199
+ });
3200
+ return createRuntimeFromSession(session, {
3201
+ ...options,
3202
+ telemetry
3203
+ });
2971
3204
  }
2972
3205
  function toCpCreateIVISessionRequestBody(request) {
2973
3206
  const body = { ...request };
@@ -3053,6 +3286,25 @@ function resolveCreateIVISessionUrl(options) {
3053
3286
  const path = options.path ?? "/v1/ivi/sessions";
3054
3287
  return new URL(path, ensureTrailingSlash(options.baseUrl)).toString();
3055
3288
  }
3289
+ function buildCreateSessionTelemetryAttributes(request) {
3290
+ return {
3291
+ "ivi.version": request.iviVersion,
3292
+ "ivi.stream.type": request.streamType,
3293
+ "ivi.prebuild.enabled": Boolean(
3294
+ request.prebuiltStream || request.prebuiltCharacters && request.prebuiltCharacters.length > 0
3295
+ )
3296
+ };
3297
+ }
3298
+ function mergeIviTelemetryOptions(explicit, inherited) {
3299
+ if (!explicit && !inherited) {
3300
+ return void 0;
3301
+ }
3302
+ return {
3303
+ tracer: explicit?.tracer ?? inherited?.tracer,
3304
+ context: explicit?.context ?? inherited?.context,
3305
+ attributes: mergeIviTelemetryAttributes(inherited?.attributes, explicit?.attributes)
3306
+ };
3307
+ }
3056
3308
  async function buildHeaders(headers) {
3057
3309
  const resolvedHeaders = typeof headers === "function" ? await headers() : headers;
3058
3310
  const next = new Headers(resolvedHeaders);
@@ -3061,6 +3313,14 @@ async function buildHeaders(headers) {
3061
3313
  }
3062
3314
  return next;
3063
3315
  }
3316
+ var headersTextMapSetter = {
3317
+ set(carrier, key, value) {
3318
+ carrier.set(key, value);
3319
+ }
3320
+ };
3321
+ function injectIviTelemetryHeaders(headers, context) {
3322
+ api.propagation.inject(context, headers, headersTextMapSetter);
3323
+ }
3064
3324
  async function readJsonResponse(response) {
3065
3325
  const text = await response.text();
3066
3326
  if (!text) {
@@ -3234,32 +3494,71 @@ var IviFrontendSdk = class {
3234
3494
  this.config = config;
3235
3495
  }
3236
3496
  createClient(config) {
3237
- return new iviSdkTs.IviClient(config);
3497
+ return new iviSdkTs.IviClient({
3498
+ ...config,
3499
+ telemetry: mergeSdkTelemetryOptions(config.telemetry, this.config.telemetry)
3500
+ });
3238
3501
  }
3239
3502
  createRuntimeCoordinator(clientConfig, runtimeConfig) {
3240
3503
  const client = this.createClient(clientConfig);
3241
- return new IviRuntimeCoordinator(client, runtimeConfig);
3504
+ return new IviRuntimeCoordinator(client, {
3505
+ ...runtimeConfig ?? {},
3506
+ telemetry: mergeSdkTelemetryOptions(runtimeConfig?.telemetry, this.config.telemetry)
3507
+ });
3242
3508
  }
3243
3509
  createIVISession(request, options) {
3510
+ const telemetry = mergeSdkTelemetryChain(
3511
+ this.config.telemetry,
3512
+ this.config.http?.telemetry,
3513
+ options?.telemetry
3514
+ );
3244
3515
  return createIVISession(request, {
3245
3516
  ...this.config.http ?? {},
3246
- ...options ?? {}
3517
+ ...options ?? {},
3518
+ telemetry
3247
3519
  });
3248
3520
  }
3249
3521
  createRuntimeFromSession(session, options) {
3250
- return createRuntimeFromSession(session, options);
3522
+ return createRuntimeFromSession(session, {
3523
+ ...options ?? {},
3524
+ telemetry: mergeSdkTelemetryChain(this.config.telemetry, options?.telemetry)
3525
+ });
3251
3526
  }
3252
3527
  createIVISessionRuntime(request, options) {
3528
+ const telemetry = mergeSdkTelemetryChain(
3529
+ this.config.telemetry,
3530
+ this.config.http?.telemetry,
3531
+ options?.http?.telemetry,
3532
+ options?.telemetry
3533
+ );
3253
3534
  const http = {
3254
3535
  ...this.config.http ?? {},
3255
- ...options?.http ?? {}
3536
+ ...options?.http ?? {},
3537
+ telemetry
3256
3538
  };
3257
3539
  return createIVISessionRuntime(request, {
3258
3540
  ...options ?? {},
3541
+ telemetry,
3259
3542
  http
3260
3543
  });
3261
3544
  }
3262
3545
  };
3546
+ function mergeSdkTelemetryChain(...items) {
3547
+ return items.reduce(
3548
+ (merged, item) => mergeSdkTelemetryOptions(item, merged),
3549
+ void 0
3550
+ );
3551
+ }
3552
+ function mergeSdkTelemetryOptions(explicit, inherited) {
3553
+ if (!explicit && !inherited) {
3554
+ return void 0;
3555
+ }
3556
+ return {
3557
+ tracer: explicit?.tracer ?? inherited?.tracer,
3558
+ context: explicit?.context ?? inherited?.context,
3559
+ attributes: mergeIviTelemetryAttributes(inherited?.attributes, explicit?.attributes)
3560
+ };
3561
+ }
3263
3562
  var IviStageViewContext = react.createContext(null);
3264
3563
  var EMPTY_RUNTIME_STATE = {
3265
3564
  status: "idle",
@@ -4460,7 +4759,16 @@ function makeLoadPolicy(label, onLog) {
4460
4759
  };
4461
4760
  }
4462
4761
  function IVIHlsVideo(props) {
4463
- const { url, videoProps, style, aggressivePreload = false, paused = false, onLog } = props;
4762
+ const {
4763
+ url,
4764
+ sourceId,
4765
+ videoProps,
4766
+ style,
4767
+ aggressivePreload = false,
4768
+ paused = false,
4769
+ onLog,
4770
+ telemetry
4771
+ } = props;
4464
4772
  const videoRef = react.useRef(null);
4465
4773
  const pausedRef = react.useRef(paused);
4466
4774
  react.useEffect(() => {
@@ -4497,6 +4805,7 @@ function IVIHlsVideo(props) {
4497
4805
  const onVideoError = () => {
4498
4806
  const el = videoRef.current;
4499
4807
  const mediaErr = el?.error;
4808
+ recordVideoTelemetry("ivi.media.video.error", telemetry, sourceId, "hls", mediaErr);
4500
4809
  emitHlsLog(onLog, "warn", "<video> \u5143\u7D20\u62A5\u9519", {
4501
4810
  code: mediaErr?.code,
4502
4811
  message: mediaErr?.message,
@@ -4506,6 +4815,13 @@ function IVIHlsVideo(props) {
4506
4815
  });
4507
4816
  };
4508
4817
  const onVideoStalled = () => {
4818
+ recordVideoTelemetry(
4819
+ "ivi.media.video.stalled",
4820
+ telemetry,
4821
+ sourceId,
4822
+ "hls",
4823
+ new Error("<video> stalled")
4824
+ );
4509
4825
  emitHlsLog(onLog, "warn", "<video> stalled\uFF08\u7F13\u51B2\u505C\u6EDE\uFF09", {
4510
4826
  currentTime: videoRef.current?.currentTime,
4511
4827
  readyState: videoRef.current?.readyState
@@ -4648,7 +4964,7 @@ function IVIHlsVideo(props) {
4648
4964
  hlsInstance?.destroy();
4649
4965
  hlsInstance = null;
4650
4966
  };
4651
- }, [url, aggressivePreload, onLog]);
4967
+ }, [url, sourceId, aggressivePreload, onLog, telemetry]);
4652
4968
  return /* @__PURE__ */ jsxRuntime.jsx(
4653
4969
  "video",
4654
4970
  {
@@ -4661,6 +4977,17 @@ function IVIHlsVideo(props) {
4661
4977
  }
4662
4978
  );
4663
4979
  }
4980
+ function recordVideoTelemetry(name, telemetry, sourceId, kind, error) {
4981
+ recordIviTelemetrySpan(
4982
+ name,
4983
+ telemetry,
4984
+ {
4985
+ "ivi.source.id": sourceId,
4986
+ "ivi.media.kind": kind
4987
+ },
4988
+ error
4989
+ );
4990
+ }
4664
4991
  function isM3u8Url(url) {
4665
4992
  return /\.m3u8(?:$|[?#])/i.test(url);
4666
4993
  }
@@ -4726,6 +5053,7 @@ function TrackSlotMediaContent(props) {
4726
5053
  source,
4727
5054
  isPreloading: !isActive
4728
5055
  };
5056
+ const telemetry = runtime?.getTelemetryOptions();
4729
5057
  const mediaStyle = buildAdaptiveMediaStyle(adaptToSourceSize, fitStrategy);
4730
5058
  const shouldMute = !isActive;
4731
5059
  if (renderMedia) return renderMedia(renderContext);
@@ -4797,9 +5125,11 @@ function TrackSlotMediaContent(props) {
4797
5125
  IVIHlsVideo,
4798
5126
  {
4799
5127
  url: playbackUrl,
5128
+ sourceId: slotSourceId,
4800
5129
  videoProps: mergedVideoProps,
4801
5130
  style: videoStyle,
4802
- paused: shouldPause
5131
+ paused: shouldPause,
5132
+ telemetry
4803
5133
  }
4804
5134
  );
4805
5135
  }
@@ -4821,6 +5151,7 @@ function TrackSlotMediaContent(props) {
4821
5151
  SlotVideo,
4822
5152
  {
4823
5153
  src: playbackUrl,
5154
+ sourceId: slotSourceId,
4824
5155
  paused: shouldPause,
4825
5156
  videoProps: {
4826
5157
  ...mergedVideoProps,
@@ -4828,7 +5159,8 @@ function TrackSlotMediaContent(props) {
4828
5159
  playsInline: videoProps?.playsInline ?? true,
4829
5160
  controls: isActive ? videoProps?.controls ?? true : false
4830
5161
  },
4831
- style: videoStyle
5162
+ style: videoStyle,
5163
+ telemetry
4832
5164
  }
4833
5165
  );
4834
5166
  }
@@ -4878,17 +5210,48 @@ function SlotVideo(props) {
4878
5210
  video.play().catch(() => void 0);
4879
5211
  }
4880
5212
  }, [props.paused]);
5213
+ const onError = (event) => {
5214
+ recordPlainVideoTelemetry(
5215
+ "ivi.media.video.error",
5216
+ props.telemetry,
5217
+ props.sourceId,
5218
+ event.currentTarget.error
5219
+ );
5220
+ props.videoProps.onError?.(event);
5221
+ };
5222
+ const onStalled = (event) => {
5223
+ recordPlainVideoTelemetry(
5224
+ "ivi.media.video.stalled",
5225
+ props.telemetry,
5226
+ props.sourceId,
5227
+ new Error("<video> stalled")
5228
+ );
5229
+ props.videoProps.onStalled?.(event);
5230
+ };
4881
5231
  return /* @__PURE__ */ jsxRuntime.jsx(
4882
5232
  "video",
4883
5233
  {
4884
5234
  ref: videoRef,
4885
5235
  src: props.src,
4886
5236
  ...props.videoProps,
5237
+ onError,
5238
+ onStalled,
4887
5239
  autoPlay: props.paused ? false : props.videoProps.autoPlay ?? true,
4888
5240
  style: props.style
4889
5241
  }
4890
5242
  );
4891
5243
  }
5244
+ function recordPlainVideoTelemetry(name, telemetry, sourceId, error) {
5245
+ recordIviTelemetrySpan(
5246
+ name,
5247
+ telemetry,
5248
+ {
5249
+ "ivi.source.id": sourceId,
5250
+ "ivi.media.kind": "video"
5251
+ },
5252
+ error
5253
+ );
5254
+ }
4892
5255
 
4893
5256
  // src/react/internal/use-multi-preload-sources.ts
4894
5257
  function useMultiPreloadSources(sources, activeSourceId) {