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
25 changes: 25 additions & 0 deletions __mocks__/expo-audio.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,31 @@
// Mock for expo-audio to understand the PermissionStatus structure
export const getRecordingPermissionsAsync = jest.fn();
export const requestRecordingPermissionsAsync = jest.fn();
export const setAudioModeAsync = jest.fn().mockResolvedValue(undefined);

const createMockAudioPlayer = () => ({
id: 'mock-audio-player',
isLoaded: true,
isBuffering: false,
playing: false,
muted: false,
loop: false,
volume: 1,
currentStatus: {
isLoaded: true,
isBuffering: false,
playing: false,
didJustFinish: false,
error: null,
},
play: jest.fn(),
pause: jest.fn(),
seekTo: jest.fn().mockResolvedValue(undefined),
remove: jest.fn(),
addListener: jest.fn(() => ({ remove: jest.fn() })),
});

export const createAudioPlayer = jest.fn(createMockAudioPlayer);

// Default mock implementation
getRecordingPermissionsAsync.mockResolvedValue({
Expand Down
36 changes: 0 additions & 36 deletions __mocks__/expo-av.ts

This file was deleted.

1 change: 1 addition & 0 deletions app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
microphonePermission: 'Allow Resgrid Unit to access the microphone for audio input used in PTT and calls.',
},
],
'expo-video',
'react-native-ble-manager',
'@livekit/react-native-expo-plugin',
'@config-plugins/react-native-webrtc',
Expand Down
159 changes: 36 additions & 123 deletions docs/audio-stream-refactoring.md
Original file line number Diff line number Diff line change
@@ -1,148 +1,61 @@
# Audio Stream Store Refactoring
# Audio Stream Store

## Overview

The audio stream store has been refactored to use `expo-av` instead of `expo-audio` to resolve issues with playing remote MP3 streams over the internet in the new Expo architecture.
The audio stream store uses `expo-audio`, the audio package supported by the current Expo SDK. Remote streams are created with `createAudioPlayer()` and their loading, buffering, playback, completion, and error states are observed through `playbackStatusUpdate` events.

## Key Changes
## Player lifecycle

### 1. Replaced expo-audio with expo-av

**Before:**
```typescript
import { type AudioPlayer, createAudioPlayer } from 'expo-audio';
```

**After:**
```typescript
import { Audio, type AVPlaybackSource, type AVPlaybackStatus } from 'expo-av';
```

### 2. Updated Audio Player Management

**Before:**
- Used `createAudioPlayer()` function
- Audio player instance stored as `AudioPlayer`

**After:**
- Uses `Audio.Sound.createAsync()` method
- Audio player instance stored as `Audio.Sound`
import { createAudioPlayer, setAudioModeAsync, type AudioPlayer } from 'expo-audio';

await setAudioModeAsync({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Unhandled promise rejection risk identified where the awaited setAudioModeAsync call lacks a try/catch guard. Wrap the operation in a try/catch block to comply with Rule [1] and degrade gracefully.

Kody rule violation: Handle async operations with proper error handling

Prompt for LLM

File docs/audio-stream-refactoring.md:

Line 12:

Unhandled promise rejection risk identified where the awaited `setAudioModeAsync` call lacks a try/catch guard. Wrap the operation in a try/catch block to comply with Rule [1] and degrade gracefully.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

OS-level external call setAudioModeAsync lacks error handling and mapping to application-level errors. Wrap the call in a try/catch block to comply with Rule [27], include the operation name and context in structured logging, and surface a typed error.

Kody rule violation: Add try-catch blocks for external calls

Prompt for LLM

File docs/audio-stream-refactoring.md:

Line 12:

OS-level external call `setAudioModeAsync` lacks error handling and mapping to application-level errors. Wrap the call in a try/catch block to comply with Rule [27], include the operation name and context in structured logging, and surface a typed error.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

allowsRecording: false,
shouldPlayInBackground: true,
playsInSilentMode: true,
interruptionMode: 'duckOthers',
shouldRouteThroughEarpiece: false,
});

### 3. Enhanced Audio Configuration
const streamUrl = stream?.Url?.trim();
if (!streamUrl) {
throw new Error('Audio stream URL is required');
}

Added proper audio mode configuration for streaming:
const player: AudioPlayer = createAudioPlayer(streamUrl, {
updateInterval: 1000,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules low

Magic number identified where the numeric literal 1000 is used inline for the updateInterval configuration. Extract this domain-specific value into a named constant like PLAYBACK_UPDATE_INTERVAL_MS to comply with Rule [9].

Kody rule violation: Replace magic numbers with named constants

Prompt for LLM

File docs/audio-stream-refactoring.md:

Line 21:

Magic number identified where the numeric literal `1000` is used inline for the `updateInterval` configuration. Extract this domain-specific value into a named constant like `PLAYBACK_UPDATE_INTERVAL_MS` to comply with Rule [9].

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

keepAudioSessionActive: true,
preferredForwardBufferDuration: 5,
});

```typescript
await Audio.setAudioModeAsync({
allowsRecordingIOS: false,
staysActiveInBackground: true,
playsInSilentModeIOS: true,
shouldDuckAndroid: true,
playThroughEarpieceAndroid: false,
player.addListener('playbackStatusUpdate', (status) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Missing error handler and indeterminate cleanup path identified for the player.addListener subscription. Implement explicit status.error handling inside the callback and store the subscription object for removal during teardown to comply with Rule [4].

Kody rule violation: Provide error handlers to subscription/listener APIs

Prompt for LLM

File docs/audio-stream-refactoring.md:

Line 26:

Missing error handler and indeterminate cleanup path identified for the `player.addListener` subscription. Implement explicit `status.error` handling inside the callback and store the subscription object for removal during teardown to comply with Rule [4].

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules low

Memory leak risk identified where the event listener registered via player.addListener lacks removal during cleanup. Capture the returned subscription and call remove() on it to comply with Rule [53].

Kody rule violation: Proper memory management in event listeners

Prompt for LLM

File docs/audio-stream-refactoring.md:

Line 26:

Memory leak risk identified where the event listener registered via `player.addListener` lacks removal during cleanup. Capture the returned subscription and call `remove()` on it to comply with Rule [53].

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

// Synchronize playback and buffering state and handle status.error.
});
player.play();
```

### 4. Improved State Management
Call `player.pause()` followed by `player.remove()` when replacing or stopping a stream. `remove()` releases the native player and its listeners.

Added new state properties for better stream status tracking:
## State

```typescript
interface AudioStreamState {
// ... existing properties
isLoading: boolean; // Track loading state
isBuffering: boolean; // Track buffering state
soundObject: Audio.Sound | null; // Sound instance
}
```

### 5. Better Error Handling
`useAudioStreamStore` exposes the available streams, the current stream and player, loading and buffering flags, and the `playStream`, `stopStream`, and `cleanup` operations. The public store API remains stable for UI consumers.

Enhanced error handling with proper cleanup and status updates.
## Dependencies

## Installation

Make sure you have `expo-av` installed:
Use Expo's SDK-aware installer when changing audio dependencies:

```bash
yarn add expo-av
```

## Usage Example

```typescript
import { useAudioStreamStore } from '@/stores/app/audio-stream-store';

const MyComponent = () => {
const {
availableStreams,
isLoadingStreams,
currentStream,
isPlaying,
isLoading,
isBuffering,
fetchAvailableStreams,
playStream,
stopStream,
} = useAudioStreamStore();

useEffect(() => {
fetchAvailableStreams();
}, []);

const handlePlay = async (stream) => {
try {
await playStream(stream);
} catch (error) {
console.error('Failed to play stream:', error);
}
};

// ... render logic
};
yarn expo install expo-audio
```

## Benefits

1. **Better Remote Streaming Support**: `expo-av` provides more robust support for remote MP3 streams
2. **Improved Audio Configuration**: Proper audio mode settings for background playback and silent mode
3. **Enhanced Error Handling**: Better error recovery and cleanup
4. **Loading States**: More granular loading and buffering states for better UX
5. **Memory Management**: Proper cleanup of audio resources
Run `yarn expo install --check` after dependency changes to ensure every native package matches the installed Expo SDK.

## Migration Notes
## Configuration

If you were using the previous audio stream store:

1. Replace any direct `audioPlayer` references with `soundObject`
2. Update any custom audio handling code to use `expo-av` APIs
3. The store API remains largely the same, so most usage code should work without changes
The app config enables background audio and declares microphone permissions for PTT and LiveKit calls. Runtime microphone permissions are checked without activating a competing audio session so permission handling does not race LiveKit or CallKeep.

## Troubleshooting

### Common Issues

1. **Audio not playing on iOS in silent mode**: Make sure `playsInSilentModeIOS: true` is set
2. **Buffering issues**: The store now properly tracks buffering state - use `isBuffering` to show loading indicators
3. **Background playback**: Ensure your app has proper background audio permissions configured

### Audio Permissions

Make sure your app's configuration includes proper audio permissions:

**app.json/app.config.js:**
```json
{
"expo": {
"ios": {
"infoPlist": {
"UIBackgroundModes": ["audio"]
}
},
"android": {
"permissions": [
"android.permission.RECORD_AUDIO"
]
}
}
}
```
1. For silent-mode playback, verify `playsInSilentMode: true`.
2. For remote stream stalls, inspect the `isBuffering` and `error` fields delivered by `playbackStatusUpdate`.
3. For background playback, verify the platform background-audio configuration and keep the player audio session active.
Comment on lines +53 to +61

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== file context =="
if [ -f docs/audio-stream-refactoring.md ]; then
  nl -ba docs/audio-stream-refactoring.md | sed -n '1,120p'
else
  echo "docs/audio-stream-refactoring.md not found"
fi

echo
echo "== mentions of audio APIs and background config =="
rg -n "shouldPlayInBackground|keepAudioSessionActive|setActiveForLockScreen|interruptionMode|doNotMix|playsInSilentMode|background audio|playbackStatusUpdate|isBuffering" -S .

Repository: Resgrid/Unit

Length of output: 209


🌐 Web query:

Expo SDK 56 AudioPlayer setActiveForLockScreen shouldPlayInBackground keepAudioSessionActive interruptionMode doNotMix Android playback stops after three minutes

💡 Result:

The issue where audio playback stops after approximately three minutes on Android when the app is in the background is a known Android OS limitation that is addressed by enabling lock screen controls in Expo [1][2]. To achieve sustained background audio playback in Expo SDK 56 and later, you must explicitly call the setActiveForLockScreen method [1][3]. According to the official Expo documentation, if this is not configured, the Android operating system will terminate the audio playback after roughly three minutes to optimize battery usage [1][4]. Required configuration steps: 1. Enable Lock Screen Controls: You must use the setActiveForLockScreen method provided by the expo-audio library to enable media controls on the lock screen [1][2]. This signals to the Android OS that the app is actively performing media playback, preventing the system from killing the background task [1]. 2. Configure the Config Plugin: Ensure that the Expo audio config plugin is correctly set up in your app configuration (e.g., app.json or app.config.js) [1][4]. This plugin is responsible for adding the necessary permissions, such as FOREGROUND_SERVICE and FOREGROUND_SERVICE_MEDIA_PLAYBACK, and declaring the media playback foreground service (AudioControlsService) in your AndroidManifest.xml [2]. 3. Background Playback Settings: Ensure that shouldPlayInBackground is set to true in your audio mode configuration [1][2]. While setting shouldPlayInBackground and using the correct interruptionMode (such as mixWithOthers) are important for overall audio behavior, they are insufficient on their own to prevent the three-minute timeout on Android [1][5]. The foreground service enabled via the config plugin, combined with the activation of lock screen controls, is mandatory for indefinite background playback [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== docs file lines =="
if [ -f docs/audio-stream-refactoring.md ]; then
  awk '{printf "%6d  %s\n", NR, $0}' docs/audio-stream-refactoring.md | sed -n '1,120p'
else
  echo "docs/audio-stream-refactoring.md not found"
fi

echo
echo "== audio API mentions =="
rg -n "shouldPlayInBackground|keepAudioSessionActive|setActiveForLockScreen|interruptionMode|doNotMix|playsInSilentMode|background audio|playbackStatusUpdate|isBuffering" -S . || true

Repository: Resgrid/Unit

Length of output: 10133


🌐 Web query:

Expo SDK 56 setActiveForLockScreen metadata interruptionMode doNotMix

💡 Result:

When using setActiveForLockScreen in Expo SDK 56 (and other versions using expo-audio), you must configure your audio mode to use interruptionMode: 'doNotMix' via setAudioModeAsync [1][2][3]. The 'doNotMix' mode is a strict requirement for lock screen controls to function correctly because the operating system requires an exclusive audio focus to associate lock screen metadata and controls with your player [1][4][5]. If this mode is not set, the OS may fail to display your metadata or controls [1][2][4]. Key implementation details: 1. Configuration: Always set the audio mode before or when initializing your player [1][6]. setAudioModeAsync({ playsInSilentMode: true, shouldPlayInBackground: true, interruptionMode: 'doNotMix', }); 2. Activation: Use setActiveForLockScreen(true, metadata, options) to enable the controls [1][7]. 3. Android Background Playback: Enabling lock screen controls is essential for sustained background playback on Android; otherwise, the OS may terminate playback after approximately 3 minutes due to background limitations [1][8][7]. For more information, refer to the official Expo Audio documentation [1][2].

Citations:


Document the Android lock-screen requirement.

shouldPlayInBackground and keepAudioSessionActive are not sufficient for sustained Android playback. Add guidance for activating lock-screen controls with setActiveForLockScreen(true, metadata) and using interruptionMode: 'doNotMix' in the audio mode. Without lock-screen activation, background playback can stop after about three minutes.

🧰 Tools
🪛 LanguageTool

[style] ~56-~56: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...delivered by playbackStatusUpdate. 3. For background playback, verify the platfor...

(ENGLISH_WORD_REPEAT_BEGINNING_RULE)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/audio-stream-refactoring.md` around lines 48 - 56, Update the Android
background-playback guidance in the Configuration or Troubleshooting section to
document activating lock-screen controls with setActiveForLockScreen(true,
metadata) and configuring the audio mode with interruptionMode: 'doNotMix'.
Clarify that shouldPlayInBackground and keepAudioSessionActive alone do not
ensure sustained playback and that lock-screen activation prevents playback from
stopping after extended background operation.

10 changes: 10 additions & 0 deletions jest-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,16 @@ jest.mock('nativewind', () => ({
__esModule: true,
}));

// Avoid loading expo-video's native module in component tests.
jest.mock('expo-video', () => ({
VideoView: 'VideoView',
useVideoPlayer: jest.fn(() => ({
play: jest.fn(),
pause: jest.fn(),
addListener: jest.fn(() => ({ remove: jest.fn() })),
})),
Comment on lines +243 to +249

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

curl -fsSL 'https://docs.expo.dev/versions/v56.0.0/sdk/video/' \
  | rg -n 'useVideoPlayer\(source, setup\)|setup.*created'

rg -n -U -C 4 'useVideoPlayer\(' src --glob '*.{ts,tsx}'

Repository: Resgrid/Unit

Length of output: 50369


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== jest-setup useVideoPlayer mock =="
sed -n '230,255p' jest-setup.ts

echo
echo "== useVideoPlayer calls =="
rg -n -U -C 3 'useVideoPlayer\(' src --glob '*.{ts,tsx}' || true

echo
echo "== NativeVideoPlayer references =="
rg -n -U -C 3 'NativeVideoPlayer|play\(\)' src --glob '*.{ts,tsx}' || true

echo
echo "== package expo-video version =="
if [ -f package.json ]; then
  node - <<'JS'
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
console.log(JSON.stringify({ dependencies: pkg.dependencies?.['expo-video'], devDependencies: pkg.devDependencies?.['expo-video'] }, null, 2));
JS
fi

echo
echo "== focused setup callback text in curated docs source if available =="
curl -fsSL 'https://docs.expo.dev/versions/v56.0.0/sdk/video/' \
  | rg -n 'useVideoPlayer\(source, setup\)|callback after|created player|automatically play|play()' \
  | head -n 80

Repository: Resgrid/Unit

Length of output: 50369


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "== repo files around setup =="
wc -l jest-setup.ts
sed -n '238,252p' jest-setup.ts

echo
echo "== useVideoPlayer calls in tracked src files only =="
git ls-files 'src/*' 'src/**/*' \
  | xargs rg -n -U -C 3 'useVideoPlayer\(' --glob '*.{ts,tsx}' || true

echo
echo "== focused setup callback/signature in docs source only =="
curl -fsSL 'https://docs.expo.dev/versions/v56.0.0/sdk/video/' \
  | python3 - <<'PY'
import sys, re
data=sys.stdin.read()
for pat in [
    r'useVideoPlayer\s*\([^)]*\)\s*:',
    r'\\bsetup\\b.*callback',
    r'called.*after.*create',
    r'play\\(\\)',
]:
    hits=re.findall(pat, data, re.I)
    if hits:
        print(f"== pattern {pat!r} ==")
        print("\n".join(set(hits))[:200])
PY

echo
echo "== package expo-video metadata =="
python3 - <<'JS' || true
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
console.log(JSON.stringify({ dependencies: pkg.dependencies?.['expo-video'], devDependencies: pkg.devDependencies?.['expo-video'] }, null, 2));
JS

Repository: Resgrid/Unit

Length of output: 1357


🌐 Web query:

Expo SDK 56 video useVideoPlayer setup callback documentation

💡 Result:

In Expo SDK 56, the useVideoPlayer hook is the recommended way to create and manage a VideoPlayer instance, ensuring the player is automatically cleaned up when the component unmounts [1][2]. The setup callback is the second argument of the useVideoPlayer hook [2]. It is an optional function that allows you to configure the player immediately after it is created [2]. The signature of the hook is: useVideoPlayer(source, setup, playerBuilderOptions) [2] Parameters: - source (VideoSource): The video source used to initialize the player [2]. - setup (optional, (player: VideoPlayer) => void): A function that executes after the player is created, allowing you to set properties like player.loop = true or trigger player.play() [1][2]. - playerBuilderOptions (optional, PlayerBuilderOptions): Configuration options applied to the Android player builder before the native constructor is invoked [2]. Example usage: const player = useVideoPlayer(videoSource, player => { player.loop = true; player.play; }); For advanced use cases where you need a player that persists beyond the component's lifecycle, the createVideoPlayer function is available, but it requires you to manually call release() to prevent memory leaks [1][2].

Citations:


Invoke the useVideoPlayer setup callback in the mock.

NativeVideoPlayer calls useVideoPlayer(source, (player) => player.play()), but the current mock returns a stale player and never runs the optional setup function. This can mask a regression where automatic playback is missing from rendered tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@jest-setup.ts` around lines 243 - 249, Update the useVideoPlayer mock in
jest.mock('expo-video') to accept the setup callback and invoke it with the
mocked player before returning that player. Preserve the existing play, pause,
and addListener mock behavior so tests verify automatic playback through the
same setup path as NativeVideoPlayer.

Source: Coding guidelines

}));

// Mock zod globally to avoid validation schema issues in tests
jest.mock('zod', () => ({
z: {
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@
"expo-asset": "~56.0.21",
"expo-audio": "~56.0.13",
"expo-auth-session": "~56.0.16",
"expo-av": "16.0.8",
"expo-build-properties": "~56.0.24",
"expo-clipboard": "~56.0.4",
"expo-constants": "~56.0.22",
Expand All @@ -115,6 +114,7 @@
"expo-status-bar": "~56.0.4",
"expo-system-ui": "~56.0.5",
"expo-task-manager": "~56.0.24",
"expo-video": "~56.1.4",
"expo-web-browser": "~56.0.6",
"geojson": "0.5.0",
"i18next": "23.14.0",
Expand Down Expand Up @@ -220,8 +220,7 @@
"exclude": [
"react-native-restart",
"lucide-react-native",
"react-native-callkeep",
"expo-av"
"react-native-callkeep"
]
}
},
Expand Down
17 changes: 15 additions & 2 deletions src/components/call-video-feeds/video-player-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ResizeMode, Video } from 'expo-av';
import { useVideoPlayer, VideoView } from 'expo-video';
import { CopyIcon, XIcon } from 'lucide-react-native';
import React, { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
Expand All @@ -21,6 +21,19 @@ interface VideoPlayerModalProps {
onCopyUrl: (feed: CallVideoFeedResultData) => void;
}

interface NativeVideoPlayerProps {
uri: string;
contentType: 'hls' | 'dash';
}

const NativeVideoPlayer: React.FC<NativeVideoPlayerProps> = ({ uri, contentType }) => {
const player = useVideoPlayer({ uri, contentType }, (videoPlayer) => {
videoPlayer.play();
});
Comment on lines +30 to +32

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Bug high

The useVideoPlayer source passes an unrecognized contentType field, preventing the native player from identifying adaptive streams. Construct the source using the discriminated shapes { hls: uri } or { dash: uri } based on the format.

const source = contentType === 'hls' ? { hls: uri } : { dash: uri };
  const player = useVideoPlayer(source, (videoPlayer) => {
    videoPlayer.play();
  });
Prompt for LLM

File src/components/call-video-feeds/video-player-modal.tsx:

Line 30 to 32:

The `useVideoPlayer` source passes an unrecognized `contentType` field, preventing the native player from identifying adaptive streams. Construct the source using the discriminated shapes `{ hls: uri }` or `{ dash: uri }` based on the format.

Suggested Code:

const source = contentType === 'hls' ? { hls: uri } : { dash: uri };
  const player = useVideoPlayer(source, (videoPlayer) => {
    videoPlayer.play();
  });

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.


return <VideoView player={player} style={styles.video} nativeControls contentFit="contain" />;
Comment on lines +29 to +34

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

curl -fsSL 'https://docs.expo.dev/versions/v56.0.0/sdk/video/' \
  | rg -n 'statusChange|error|useEventListener'

rg -n -C 4 \
  'useVideoPlayer|statusChange|useEvent|useEventListener|useToastStore|logger' \
  src/components/call-video-feeds --glob '*.{ts,tsx}'

Repository: Resgrid/Unit

Length of output: 50369


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '--- target file outline ---\n'
ast-grep outline src/components/call-video-feeds/video-player-modal.tsx --view expanded || true

printf '\n--- target file relevant content ---\n'
cat -n src/components/call-video-feeds/video-player-modal.tsx | sed -n '1,220p'

printf '\n--- focused repository search ---\n'
rg -n -C 3 'useVideoPlayer|statusChange|useEvent|useEventListener|useToastStore|logger|NativeVideoPlayer|VideoView' src/components/call-video-feeds --glob '*.{ts,tsx}' || true

Repository: Resgrid/Unit

Length of output: 241


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- target file outline ---'
ast-grep outline src/components/call-video-feeds/video-player-modal.tsx --view expanded || true

printf '%s\n' ''
printf '%s\n' '--- target file relevant content ---'
cat -n src/components/call-video-feeds/video-player-modal.tsx | sed -n '1,220p'

printf '%s\n' ''
printf '%s\n' '--- focused repository search ---'
rg -n -C 3 'useVideoPlayer|statusChange|useEvent|useEventListener|useToastStore|logger|NativeVideoPlayer|VideoView' src/components/call-video-feeds --glob '*.{ts,tsx}' || true

Repository: Resgrid/Unit

Length of output: 13177


🌐 Web query:

Expo SDK 56 expo-video useVideoPlayer statusChange error event useEventListener

💡 Result:

In Expo SDK 56, the expo-video library uses an event-based system for the VideoPlayer, as changes to player properties do not automatically trigger React state updates [1][2]. To handle status changes and potential errors, you can use the statusChange event emitted by the VideoPlayer. The payload for this event, StatusChangeEventPayload, includes the following properties [2]: - status: The new VideoPlayerStatus. - error: An optional PlayerError object, which contains information if an error occurred during the status change [2]. - oldStatus: The previous VideoPlayerStatus [2]. You can listen for this event using the useEventListener hook from the expo package, which is built around the player's addListener and removeListener methods and handles cleanup automatically [1][3]. Example usage: import { useEventListener } from 'expo'; // Inside your component useEventListener(player, 'statusChange', ({ status, error }) => { if (error) { console.error('Player error:', error); } console.log('Player status changed to:', status); }); Alternatively, you can use the useEvent hook if you need a stateful value that updates automatically, or use player.addListener directly with a useEffect hook for more manual control [1][3].

Citations:


Handle native playback errors in NativeVideoPlayer.

NativeVideoPlayer starts playback but ignores statusChange.error. If the HLS/DASH manifest or request fails, the modal stays open without translated feedback, retry, or copy assistance. Subscribe with useEventListener or useEvent, render an error state, show a useToastStore toast, and log the failure through logger when appropriate. This follows the Expo video event flow documented for SDK 56.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/call-video-feeds/video-player-modal.tsx` around lines 29 - 34,
Update NativeVideoPlayer to subscribe to the video player's status changes using
the Expo SDK 56 event API, detect statusChange.error, and render an appropriate
translated error state instead of leaving the failed player active. Show a
useToastStore toast with retry or copy assistance as supported by existing
patterns, and log the playback failure through logger when appropriate while
preserving normal playback behavior.

Source: Coding guidelines

};

export const VideoPlayerModal: React.FC<VideoPlayerModalProps> = ({ isOpen, onClose, feed, onCopyUrl }) => {
const { t } = useTranslation();

Expand All @@ -40,7 +53,7 @@ export const VideoPlayerModal: React.FC<VideoPlayerModalProps> = ({ isOpen, onCl
switch (feed.FeedFormat) {
case FeedFormat.HLS:
case FeedFormat.DASH:
return <Video source={{ uri: feed.Url }} style={styles.video} useNativeControls resizeMode={ResizeMode.CONTAIN} shouldPlay />;
return <NativeVideoPlayer key={`${feed.FeedFormat}:${feed.Url}`} uri={feed.Url} contentType={feed.FeedFormat === FeedFormat.HLS ? 'hls' : 'dash'} />;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

curl -fsSL 'https://docs.expo.dev/versions/v56.0.0/sdk/video/' \
  | rg -n '`dash`|Android-only'

rg -n -C 5 \
  'FeedFormat\.DASH|contentType.*dash|VideoAssetTransportProvider|contentTypeHint|isAndroid' \
  . --glob '*.{ts,tsx,js}'

Repository: Resgrid/Unit

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -a 'video-player-modal|platform|FeedFormat|feedResult|call' . | sed 's#^\./##' | head -200

echo
echo "== exact DASH/transport/platform references in tracked files =="
rg -n -C 3 \
  'FeedFormat\.DASH|contentType:\s*["'\'']dash["'\'']|VideoAssetTransportProvider|contentTypeHint|isAndroid|FeedFormat' \
  --glob '*.{ts,tsx,js}' . || true

echo
echo "== video player modal lines =="
file="$(fd 'video-player-modal.tsx' . | head -1)"
if [ -n "$file" ]; then
  echo -- "$file"
  wc -l "$file"
  sed -n '1,140p' "$file" | cat -n
fi

echo
echo "== platform module =="
file="$(fd 'platform.ts' src/lib | head -1)"
if [ -n "$file" ]; then
  echo -- "$file"
  sed -n '1,160p' "$file" | cat -n
fi

echo
echo "== FeedFormat definitions/usages =="
rg -n -C 3 'enum FeedFormat|FeedFormat;' --glob '*.{ts,tsx,js}' . || true

echo
echo "== deterministic content-type mapping probe from source =="
python3 - <<'PY'
from pathlib import Path
p = next(Path('.').glob('src/components/call-video-feeds/video-player-modal.tsx'), None)
if p:
    src = p.read_text()
    print("modal_path", str(p))
    print("has_hls_branch", "FeedFormat.HLS ? 'hls'" in src)
    print("has_dash_branch", "FeedFormat.DASH ? 'dash'" in src)
    print("has_isAndroid_in_modal", "isAndroid" in src)
else:
    print("modal_path", None)
PY

Repository: Resgrid/Unit

Length of output: 37570


Guard FeedFormat.DASH on non-Android platforms.

contentType: 'dash' is Android-only in Expo video, so an .mpd feed sent to VideoPlayerModal will also fail on iOS if no custom transport is registered. Branch with isAndroid from @/lib/platform.ts and render a supported fallback or translated unsupported state.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/call-video-feeds/video-player-modal.tsx` at line 56, Update
the NativeVideoPlayer rendering branch in VideoPlayerModal to check isAndroid
from `@/lib/platform.ts` before passing contentType 'dash'; on non-Android
platforms, render the supported fallback or translated unsupported state instead
of attempting DASH playback, while preserving HLS and Android DASH behavior.

Source: Coding guidelines

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules low

Code duplication identified where shared string literals 'hls' and 'dash' are inlined at the call site and within the NativeVideoPlayerProps type (line 26). Extract these strings into a single source of truth to comply with Rule [6] and prevent drift.

Kody rule violation: Centralize string constants

Prompt for LLM

File src/components/call-video-feeds/video-player-modal.tsx:

Line 56:

Code duplication identified where shared string literals `'hls'` and `'dash'` are inlined at the call site and within the `NativeVideoPlayerProps` type (line 26). Extract these strings into a single source of truth to comply with Rule [6] and prevent drift.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.


case FeedFormat.YouTubeLive:
case FeedFormat.Embed:
Expand Down
Loading
Loading