diff --git a/.changeset/real-crabs-beam.md b/.changeset/real-crabs-beam.md
new file mode 100644
index 000000000..15bf6c0f3
--- /dev/null
+++ b/.changeset/real-crabs-beam.md
@@ -0,0 +1,6 @@
+---
+"@livekit/components-core": patch
+"@livekit/components-react": patch
+---
+
+Add activeSegements to useTrackTranscription
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 0cf789972..0fb9dda5d 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- - uses: pnpm/action-setup@v2
+ - uses: pnpm/action-setup@v4
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
diff --git a/.github/workflows/size-limit.yaml b/.github/workflows/size-limit.yaml
index 50cad3bbc..79ad03364 100644
--- a/.github/workflows/size-limit.yaml
+++ b/.github/workflows/size-limit.yaml
@@ -11,7 +11,7 @@ jobs:
CI_JOB_NUMBER: 1
steps:
- uses: actions/checkout@v4
- - uses: pnpm/action-setup@v2
+ - uses: pnpm/action-setup@v4
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 1d675f442..dbe6daab1 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -16,7 +16,7 @@ jobs:
with:
fetch-depth: 2
- - uses: pnpm/action-setup@v2
+ - uses: pnpm/action-setup@v4
- name: Use Node.js 20
uses: actions/setup-node@v4
diff --git a/examples/nextjs/pages/transcriptions.tsx b/examples/nextjs/pages/transcriptions.tsx
new file mode 100644
index 000000000..d7be2700e
--- /dev/null
+++ b/examples/nextjs/pages/transcriptions.tsx
@@ -0,0 +1,77 @@
+import {
+ LiveKitRoom,
+ useToken,
+ setLogLevel,
+ useParticipantTracks,
+ useTrackTranscription,
+} from '@livekit/components-react';
+import { Track } from 'livekit-client';
+import type { NextPage } from 'next';
+import { generateRandomUserId } from '../lib/helper';
+import { useMemo } from 'react';
+
+const TranscriptionExample: NextPage = () => {
+ const params = typeof window !== 'undefined' ? new URLSearchParams(location.search) : null;
+ const roomName = useMemo(() => params?.get('room') ?? generateRandomUserId(), []);
+ setLogLevel('info', { liveKitClientLogLevel: 'debug' });
+ const userId = useMemo(() => params?.get('user') ?? 'test-user', []);
+
+ const tokenOptions = useMemo(() => {
+ return {
+ userInfo: {
+ identity: userId,
+ name: userId,
+ },
+ };
+ }, [userId]);
+
+ const token = useToken(process.env.NEXT_PUBLIC_LK_TOKEN_ENDPOINT, roomName, tokenOptions);
+
+ return (
+
+
{
+ console.error(e);
+ alert(
+ 'Error acquiring camera or microphone permissions. Please make sure you grant the necessary permissions in your browser and reload the tab',
+ );
+ }}
+ >
+
+
+
+ );
+};
+
+function ParticipantTranscriptions({
+ identity,
+ activeOnly = true,
+ ...props
+}: React.HtmlHTMLAttributes & { identity: string; activeOnly?: boolean }) {
+ const audioTracks = useParticipantTracks([Track.Source.Microphone], identity);
+ const transcriptions = useTrackTranscription(audioTracks[0]);
+ const segments = activeOnly ? transcriptions.activeSegments : transcriptions.segments;
+
+ return (
+
+ {segments.map((segment) => (
+
{segment.text}
+ ))}
+
+ );
+}
+
+export default TranscriptionExample;
diff --git a/packages/core/src/helper/transcriptions.ts b/packages/core/src/helper/transcriptions.ts
index c60f16784..035229a3b 100644
--- a/packages/core/src/helper/transcriptions.ts
+++ b/packages/core/src/helper/transcriptions.ts
@@ -8,11 +8,11 @@ export type ReceivedTranscriptionSegment = TranscriptionSegment & {
export function getActiveTranscriptionSegments(
segments: ReceivedTranscriptionSegment[],
syncTimes: { timestamp: number; rtpTimestamp?: number },
- maxAge = 0,
+ maxAge = 5_000,
) {
return segments.filter((segment) => {
- const hasTrackSync = !!syncTimes.rtpTimestamp;
- const currentTrackTime = syncTimes.rtpTimestamp ?? performance.timeOrigin + performance.now();
+ const hasTrackSync = !!syncTimes.rtpTimestamp && segment.startTime !== 0;
+ const currentTrackTime = syncTimes.rtpTimestamp || performance.timeOrigin + performance.now();
// if a segment arrives late, consider startTime to be the media timestamp from when the segment was received client side
const displayStartTime = hasTrackSync
? Math.max(segment.receivedAtMediaTimestamp, segment.startTime)
@@ -29,6 +29,7 @@ export function addMediaTimestampToTranscription(
segment: TranscriptionSegment,
timestamps: { timestamp: number; rtpTimestamp?: number },
): ReceivedTranscriptionSegment {
+ console.log('new media timestamp', { segment, receivedAt: timestamps.timestamp });
return {
...segment,
receivedAtMediaTimestamp: timestamps.rtpTimestamp ?? 0,
diff --git a/packages/react/etc/components-react.api.md b/packages/react/etc/components-react.api.md
index eb7c573dd..b6846bb0c 100644
--- a/packages/react/etc/components-react.api.md
+++ b/packages/react/etc/components-react.api.md
@@ -667,6 +667,7 @@ export interface TrackToggleProps extends Omit extends Omit
+ participantIdentity
+ ? participantIdentity === room.localParticipant.identity
+ ? room.localParticipant
+ : room.getParticipantByIdentity(participantIdentity)
+ : participantContext,
+ [connectionState, participantIdentity, room, participantContext],
+ );
const observable = React.useMemo(
() => (p ? participantTracksObservable(p, { sources }) : undefined),
[p?.sid, p?.identity, JSON.stringify(sources)],
diff --git a/packages/react/src/hooks/useTrackSyncTime.ts b/packages/react/src/hooks/useTrackSyncTime.ts
index a1f2ef737..ff9d72871 100644
--- a/packages/react/src/hooks/useTrackSyncTime.ts
+++ b/packages/react/src/hooks/useTrackSyncTime.ts
@@ -5,13 +5,14 @@ import { useObservableState } from './internal';
/**
* @internal
*/
-export function useTrackSyncTime({ publication }: TrackReferenceOrPlaceholder) {
+export function useTrackSyncTime(trackRef?: TrackReferenceOrPlaceholder) {
const observable = React.useMemo(
- () => (publication?.track ? trackSyncTimeObserver(publication.track) : undefined),
- [publication?.track],
+ () =>
+ trackRef?.publication?.track ? trackSyncTimeObserver(trackRef?.publication.track) : undefined,
+ [trackRef?.publication?.track],
);
return useObservableState(observable, {
- timestamp: Date.now(),
- rtpTimestamp: publication?.track?.rtpTimestamp,
+ timestamp: performance.timeOrigin + performance.now(),
+ rtpTimestamp: trackRef?.publication?.track?.rtpTimestamp,
});
}
diff --git a/packages/react/src/hooks/useTrackTranscription.ts b/packages/react/src/hooks/useTrackTranscription.ts
index d99589252..4b84157e3 100644
--- a/packages/react/src/hooks/useTrackTranscription.ts
+++ b/packages/react/src/hooks/useTrackTranscription.ts
@@ -1,12 +1,12 @@
import {
type ReceivedTranscriptionSegment,
- addMediaTimestampToTranscription as addTimestampsToTranscription,
+ addMediaTimestampToTranscription,
dedupeSegments,
- // getActiveTranscriptionSegments,
+ getActiveTranscriptionSegments,
getTrackReferenceId,
trackTranscriptionObserver,
type TrackReferenceOrPlaceholder,
- // didActiveSegmentsChange,
+ didActiveSegmentsChange,
} from '@livekit/components-core';
import type { TranscriptionSegment } from 'livekit-client';
import * as React from 'react';
@@ -22,12 +22,12 @@ export interface TrackTranscriptionOptions {
*/
bufferSize?: number;
/** amount of time (in ms) that the segment is considered `active` past its original segment duration, defaults to 2_000 */
- // maxAge?: number;
+ maxAge?: number;
}
const TRACK_TRANSCRIPTION_DEFAULTS = {
bufferSize: 100,
- // maxAge: 2_000,
+ maxAge: 5_000,
} as const satisfies TrackTranscriptionOptions;
/**
@@ -35,28 +35,31 @@ const TRACK_TRANSCRIPTION_DEFAULTS = {
* @alpha
*/
export function useTrackTranscription(
- trackRef: TrackReferenceOrPlaceholder,
+ trackRef?: TrackReferenceOrPlaceholder,
options?: TrackTranscriptionOptions,
) {
const opts = { ...TRACK_TRANSCRIPTION_DEFAULTS, ...options };
const [segments, setSegments] = React.useState>([]);
- // const [activeSegments, setActiveSegments] = React.useState>(
- // [],
- // );
- // const prevActiveSegments = React.useRef([]);
+ const [activeSegments, setActiveSegments] = React.useState>(
+ [],
+ );
+ const prevActiveSegments = React.useRef([]);
const syncTimestamps = useTrackSyncTime(trackRef);
- const handleSegmentMessage = (newSegments: TranscriptionSegment[]) => {
- setSegments((prevSegments) =>
- dedupeSegments(
- prevSegments,
- // when first receiving a segment, add the current media timestamp to it
- newSegments.map((s) => addTimestampsToTranscription(s, syncTimestamps)),
- opts.bufferSize,
- ),
- );
- };
+ const handleSegmentMessage = React.useCallback(
+ (newSegments: TranscriptionSegment[]) => {
+ setSegments((prevSegments) =>
+ dedupeSegments(
+ prevSegments,
+ // when first receiving a segment, add the current media timestamp to it
+ newSegments.map((s) => addMediaTimestampToTranscription(s, syncTimestamps)),
+ opts.bufferSize,
+ ),
+ );
+ },
+ [syncTimestamps, opts.bufferSize],
+ );
React.useEffect(() => {
- if (!trackRef.publication) {
+ if (!trackRef?.publication) {
return;
}
const subscription = trackTranscriptionObserver(trackRef.publication).subscribe((evt) => {
@@ -65,22 +68,21 @@ export function useTrackTranscription(
return () => {
subscription.unsubscribe();
};
- }, [getTrackReferenceId(trackRef), handleSegmentMessage]);
+ }, [trackRef && getTrackReferenceId(trackRef), handleSegmentMessage]);
- // React.useEffect(() => {
- // if (syncTimestamps) {
- // const newActiveSegments = getActiveTranscriptionSegments(
- // segments,
- // syncTimestamps,
- // opts.maxAge,
- // );
- // // only update active segment array if content actually changed
- // if (didActiveSegmentsChange(prevActiveSegments.current, newActiveSegments)) {
- // setActiveSegments(newActiveSegments);
- // prevActiveSegments.current = newActiveSegments;
- // }
- // }
- // }, [syncTimestamps, segments, opts.maxAge]);
-
- return { segments };
+ React.useEffect(() => {
+ if (syncTimestamps) {
+ const newActiveSegments = getActiveTranscriptionSegments(
+ segments,
+ syncTimestamps,
+ opts.maxAge,
+ );
+ // only update active segment array if content actually changed
+ if (didActiveSegmentsChange(prevActiveSegments.current, newActiveSegments)) {
+ setActiveSegments(newActiveSegments);
+ prevActiveSegments.current = newActiveSegments;
+ }
+ }
+ }, [syncTimestamps, segments, opts.maxAge]);
+ return { segments, activeSegments };
}