Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions apps/client/src/composables/useVideoPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1405,6 +1405,49 @@ function subscribeAndPipeToWorker(
})
}

export function subscribeOnlyVideo(
moqClient: MOQtailClient,
videoTrackAlias: number,
videoFullTrackName: FullTrackName,
) {
const setup = async (): Promise<{ videoRequestId?: bigint; cleanup: () => Promise<void> }> => {
try {
const response = await moqClient.subscribe({
fullTrackName: videoFullTrackName,
groupOrder: GroupOrder.Original,
filterType: FilterType.LatestObject,
forward: true,
priority: 0,
trackAlias: videoTrackAlias,
})

if (response instanceof SubscribeError) {
console.error('subscribeOnlyVideo: subscribe returned error', response)
return { cleanup: async () => {} }
}

const { requestId } = response
console.log('subscribeOnlyVideo: subscribed, requestId=', requestId)

return {
videoRequestId: requestId,
cleanup: async () => {
try {
await moqClient.unsubscribe(requestId)
console.log('subscribeOnlyVideo: unsubscribed requestId=', requestId)
} catch (err) {
console.warn('subscribeOnlyVideo: failed to unsubscribe', err)
}
},
}
} catch (err) {
console.error('subscribeOnlyVideo: unexpected error', err)
return { cleanup: async () => {} }
}
}
return setup
}

function handleWorkerMessages(
worker: Worker,
audioNode: AudioWorkletNode,
Expand Down
123 changes: 90 additions & 33 deletions apps/client/src/pages/SessionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,7 @@ import {
RoomTimeoutMessage,
} from '@/types/types'
import { useSocket } from '@/sockets/SocketContext'
import {
FullTrackName,
ObjectForwardingPreference,
Tuple,
GroupOrder,
FetchType,
Location,
FetchError,
} from 'moqtail-ts/model'
import { FullTrackName, ObjectForwardingPreference, Tuple, GroupOrder, FetchType, FetchError } from 'moqtail-ts/model'
import {
announceNamespaces,
initializeChatMessageSender,
Expand All @@ -76,6 +68,7 @@ import {
resizeCanvasWorker,
resizeCanvasForMaximization,
clearScreenshareCanvas,
subscribeOnlyVideo,
} from '@/composables/useVideoPipeline'
import { MOQtailClient } from 'moqtail-ts/client'
import { NetworkTelemetry, ClockNormalizer } from 'moqtail-ts/util'
Expand Down Expand Up @@ -185,6 +178,10 @@ function SessionPage() {
}>({})
const [isFetching, setIsFetching] = useState(false)
const isRewindCleaningUp = useRef<boolean>(false)
// temporary SD subscriptions created for rewind (keyed by userId)
const tempSDSubscriptionsRef = useRef<{
[userId: string]: { requestId: bigint; cleanup: () => Promise<void> }
}>({})

type VideoQuality = 'SD' | 'HD'
const [userVideoQualities, setUserVideoQualities] = useState<{ [userId: string]: VideoQuality }>({})
Expand Down Expand Up @@ -364,9 +361,50 @@ function SessionPage() {
},
})
*/
// get request id from the video track subscription
// get request id from the video track subscription. If the viewer is HD-subscribed
// and no SD request id exists, create a temporary SD subscription (no canvas/worker)
console.log('userSubscriptions', userSubscriptions)
const videoRequestId = userSubscriptions[userId]?.videoRequestId
let videoRequestId = userSubscriptions[userId]?.videoRequestId

const isHDSubscribed = !!userSubscriptions[userId]?.videoHDSubscribed

if (videoRequestId === undefined && isHDSubscribed) {
console.log('No SD request id while HD subscribed - creating temporary SD subscription for rewind')
// create a silent SD subscription using subscribeOnlyVideo
const canvasRef = remoteCanvasRefs[userId]
if (!roomState) {
console.error('Room state missing, cannot create SD subscription')
return
}
const roomName = roomState.name
const videoFullTrackName = getTrackname(roomName, userId, 'video')
const videoTrackAlias = canvasRef?.current ? parseInt(canvasRef.current.dataset.videotrackalias || '-1') : -1

if (videoTrackAlias === -1) {
console.error('Cannot create temp SD subscription - track alias not available for user', userId)
return
}

const setup = subscribeOnlyVideo(moqClientRef.current!, videoTrackAlias, videoFullTrackName)
const result = await setup()
if (result.videoRequestId) {
tempSDSubscriptionsRef.current[userId] = { requestId: result.videoRequestId, cleanup: result.cleanup }
videoRequestId = result.videoRequestId
// Update local subscription state so other parts of app can see the requestId
setUserSubscriptions((prev) => ({
...prev,
[userId]: {
...prev[userId],
videoSubscribed: true,
videoRequestId: result.videoRequestId,
},
}))
} else {
console.error('Temporary SD subscription did not return a request id')
return
}
}

if (videoRequestId === undefined) {
console.error('No video request id found for user:', userId)
return
Expand Down Expand Up @@ -458,6 +496,26 @@ function SessionPage() {
setIsRewindPlayerOpen(false)
setSelectedRewindUserId('')

// If we created a temporary SD subscription for this user, clean it up now
const tempSub = tempSDSubscriptionsRef.current[selectedRewindUserId]
if (tempSub) {
console.log('Cleaning up temporary SD subscription for', selectedRewindUserId)
tempSub.cleanup()
delete tempSDSubscriptionsRef.current[selectedRewindUserId]
// Clear subscription state if it was only temporary
setUserSubscriptions((prev) => {
const current = prev[selectedRewindUserId] || {}
return {
...prev,
[selectedRewindUserId]: {
...current,
videoSubscribed: false,
videoRequestId: undefined,
},
}
})
}

// Add a delay to ensure rewind player cleanup is complete
setTimeout(() => {
console.log('SessionPage: Rewind player cleanup complete')
Expand Down Expand Up @@ -2920,28 +2978,6 @@ function SessionPage() {
data-videoquality={userVideoQualities[user.id] || 'SD'}
className="w-full h-full object-cover"
/>

{/* Video Quality Toggle Button for Remote User */}
<div className="absolute top-3 left-3">
<button
onClick={() => handleToggleRemoteUserQuality(user.id)}
className={`px-2 py-1 rounded-md transition-all duration-200 text-xs font-semibold min-w-[2.5rem] shadow-lg ${
pendingHDRequests.has(user.id)
? 'bg-gray-500 text-gray-300 cursor-not-allowed'
: (userVideoQualities[user.id] || 'SD') === 'HD'
? 'bg-lime-600 hover:bg-lime-700 text-white'
: 'bg-orange-600 hover:bg-orange-700 text-white'
}`}
disabled={isRewindCleaningUp.current || pendingHDRequests.has(user.id)}
title={
pendingHDRequests.has(user.id)
? 'Waiting for HD permission response...'
: `Switch to ${(userVideoQualities[user.id] || 'SD') === 'SD' ? 'HD (720p)' : 'SD (360p)'} quality`
}
>
{pendingHDRequests.has(user.id) ? '...' : userVideoQualities[user.id] || 'SD'}
</button>
</div>
{/* Show initials when remote video is off */}
{!user.hasVideo && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-700">
Expand Down Expand Up @@ -3008,6 +3044,27 @@ function SessionPage() {
)}
{/* Info card toggle buttons */}
<div className="absolute top-3 right-3 flex space-x-1">
{/* Video Quality Toggle Button for Remote User - moved here as the left-most control */}
{!isSelf(user.id) && !(user as any).originalUserId && user.hasVideo && (
<button
onClick={() => handleToggleRemoteUserQuality(user.id)}
className={`px-2 py-1 rounded-md transition-all duration-200 text-xs font-semibold min-w-[2.5rem] shadow-lg ${
pendingHDRequests.has(user.id)
? 'bg-gray-500 text-gray-300 cursor-not-allowed'
: (userVideoQualities[user.id] || 'SD') === 'HD'
? 'bg-lime-600 hover:bg-lime-700 text-white'
: 'bg-orange-600 hover:bg-orange-700 text-white'
}`}
disabled={isRewindCleaningUp.current || pendingHDRequests.has(user.id)}
title={
pendingHDRequests.has(user.id)
? 'Waiting for HD permission response...'
: `Switch to ${(userVideoQualities[user.id] || 'SD') === 'SD' ? 'HD (720p)' : 'SD (360p)'} quality`
}
>
{pendingHDRequests.has(user.id) ? '...' : userVideoQualities[user.id] || 'SD'}
</button>
)}
{/* Subscription Controls - Only for remote users and not screenshare virtual users */}
{!isSelf(user.id) && !(user as any).originalUserId && (
<>
Expand Down
Loading