# Changelog archive: 2025 Source: https://docs.daily.co/changelog/2025 Archived Daily changelog entries from 2025: transcription webhooks, canReceive permissions, meeting-move-aware recording and streaming, SIP/PSTN join-time permissions, and React Native New Architecture support. Archived changelog entries from 2025. For the latest updates, see the [changelog](/changelog). SIP/PSTN join-time permissions, React Native New Architecture support, recording and telephony improvements. ## Overview This month, we've shipped join-time permissions for SIP/PSTN users across our client SDKs. React Native developers can now build with the New Architecture enabled. We've also fixed some recording and telephony bugs. Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## daily-js [View full release notes →](https://github.com/daily-co/daily-js/releases) ### 0.80.0 * Added support for setting permissions for SIP/PSTN users at join time. * Removed the use of STUN from Xirsys. * Updated dependencies to resolve security vulnerabilities. *** ## react-native-daily-js [View full release notes →](https://github.com/daily-co/react-native-daily-js/releases) ### 0.77.0 * Added support for setting permissions for SIP/PSTN users at join time. * Fixed a build issue when using React Native's New Architecture. * Removed the use of STUN from Xirsys. * Updated dependencies to resolve security vulnerabilities. *** ## daily-python [View full release notes →](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) ### 0.19.3 * Added an option to ignore audio level when creating a custom audio track. ### 0.19.2 * Fixed a segmentation fault that could occur on exit when a custom audio source was still writing data. *** ## Media Services ### Recording * Fixed a bug where a failed session asset image URL could cause a recording composition to crash. ### Telephony * Fixed a bug to correctly extract the `displayName` from custom SIP headers for dial-in participants. Meeting move aware recording and streaming, new call transfer webhooks, improved telephony features, and platform-wide bug fixes. ## Overview Mobile SDKs now support canReceive permissions and improved connection reliability. The Python SDK gets powerful custom audio track capabilities. Meeting move aware recording is enabled by default, we've added call transfer webhooks, and transcription saves are now more resilient. Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## daily-js [View full release notes →](https://github.com/daily-co/daily-js/releases) ### 0.79.0 * Improved Krisp error handling. * Removed a client-side limit on `max_live_streams`. * Disabled Redux DevTools in production builds. *** ## react-native-daily-js [View full release notes →](https://github.com/daily-co/react-native-daily-js/releases) ### 0.76.0 * Removed a client-side limit on `max_live_streams`. * Disabled Redux DevTools in production builds. *** ## daily-android [View full release notes →](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) ### 0.31.0 * Added support for the `canReceive` permission to manage which media tracks remote participants can receive. * Improved connection reliability by retrying signaling channel connections and using an alternate WebSocket URI format. * Fixed a bug where custom ICE server configurations were not being applied. *** ## daily-ios [View full release notes →](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) ### 0.31.0 * Added support for the `canReceive` permission to manage which media tracks remote participants can receive. * Improved connection reliability by retrying signaling channel connections and using an alternate WebSocket URI format. *** ## daily-python [View full release notes →](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) ### 0.19.1 * Fixed an audio renderer issue that caused garbled audio with remote stereo tracks. * Added handling for meeting session ID changes that occur mid-call. ### 0.19.0 * Introduced `CustomAudioTrack` for use as an additional custom track or as the main microphone input. * Added support for `displayName`, `userId`, `video`, `codecs`, and `permissions` in `start_dialout()`. * Added `sample_rate` and `callback_interval_ms` arguments to `set_audio_renderer()` for more control over audio data. * Updated `add_custom_audio_track()` and `update_custom_audio_track()` to accept a `CustomAudioTrack` instance. * Switched to using system-level root certificates on all platforms. * Fixed a bug where blocking virtual microphones could fail to send audio. ### 0.18.2 * Fixed an installation issue on macOS versions older than 15.0. * Corrected type hints for `remove_custom_track()`. ### 0.18.1 * Improved connection reliability by retrying signaling channel connections and using an alternate WebSocket URI format. * Fixed a bug with virtual microphone completion callbacks. *** ## Media Services ### Recording * Meeting move aware recording and streaming is now enabled by default. This feature [(previously in pre-beta)](/changelog/2024#recording-and-streaming-recovery-during-meeting-moves-pre-beta) automatically resumes recordings and live streams when meetings are transferred between servers during maintenance or failovers, eliminating gaps in recorded content. ### Telephony * Introduced new webhooks to track call transfer status: `calltransfer.triggered`, `calltransfer.initiated`, `calltransfer.answered`, and `calltransfer.complete`. * Added support for passing custom SIP headers during pinless dial-in, which are now available in the `sipHeaders` object in the webhook payload. * Added `canReceive` permission controls for dial-in and dial-out participants via the `startDialOut()` API and room properties. * On-demand SIP dial-out endpoints are now the default behavior, improving resource efficiency. ### Transcription * Transcripts are now saved to storage every two minutes by default, improving data resilience. * The `/transcript` and `/transcripts` API endpoints now include `roomName`, `participantMinutes`, and `isVttAvailable` for more detailed information. * Fixed a race condition that could cause duplicate transcription messages when participant settings were updated rapidly. Stability fixes for ejection logic and network states, Python custom audio tracks, enhanced recording webhooks, improved telephony features, and performance diagnostics for slow joins. ## Overview We've shipped stability improvements across the board this month. Key fixes include meeting ejection logic, network state calculations, and slow join diagnostics. The Python SDK now supports custom audio tracks, and raw tracks recordings now include instance IDs in webhooks. Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## daily-js [View full release notes →](https://github.com/daily-co/daily-js/releases) ### 0.78.0 * Fixed ejection logic to prevent incorrect ejections or joins when a user's local clock is out of sync. ⚠️ **Note:** Participants can no longer join if `eject_after_elapsed` is set to a negative value. * Fixed an `OverconstrainedError` on Chrome that occurred when providing an invalid audio `deviceId`. * Fixed an issue where unhandled errors during Krisp initialization could cause problems when joining a call. * Reverted a change that disabled background effects on Windows when software WebGL is in use. Instead, a `video-processor-warning` is now logged, as this scenario can significantly reduce video frame rates. * Improved `networkState` calculations by removing `recvPacketLoss`, which could be misleading. * Added `instanceId` to webhooks for raw tracks recordings. * Added performance logging for `enumerateDevices()` to help identify slow join times, especially on Firefox when the tab is not in focus. * Removed the beta feature warning from `testCallQuality()`. *** ## react-native-daily-js [View full release notes →](https://github.com/daily-co/react-native-daily-js/releases) ### 0.75.0 * Fixed ejection logic to prevent issues caused by mismatched local clock times. ⚠️ **Note:** Participants can no longer join if `eject_after_elapsed` is set to a negative value. * Improved `networkState` calculation by removing `recvPacketLoss` from the formula. * Added `instanceId` to webhooks for raw tracks recordings. * Added performance logging for `enumerateDevices()` to help diagnose slow join times. *** ## daily-python [View full release notes →](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) ### 0.18.0 * Introduced support for creating, updating, and removing custom audio tracks. You can now use `CallClient.add_custom_audio_track()`, `CallClient.update_custom_audio_track()`, and `CallClient.remove_custom_audio_track()` for advanced audio manipulation. * Improved `canReceive` permission support to ensure track states (`"off"`, `offReasons`) update correctly when permissions are granted or revoked. * Audio and video renderer callbacks (`set_audio_renderer()`, `set_video_renderer()`) now receive the audio or video source as the last argument. * Reduced "no subscription for consumer" and "Failed to close consumer" log messages from error to debug, as these often occur during normal participant disconnections. ## Media Services ### Recording * **Instance ID in Webhooks**: The `recording.ready-to-download` and `raw-tracks-recording-error` webhooks now include an `instance_id` to uniquely identify raw tracks recording sessions. * Fixed an issue to ensure the `instanceId` is correctly propagated for raw tracks recordings from the initial API call through to the final webhook event. ### Telephony * **`dialin.ready` Webhook**: Introduced the `dialin.ready` webhook event, which notifies you when a room's SIP endpoint is ready to receive an inbound call. * **`origin` Field in Dial-Out Webhooks**: Dial-out webhook events now include an `origin` field, specifying the PSTN number or SIP URI that initiated the call. * **Default SIP Codec**: Added a domain-level property to set default codecs for SIP/PSTN calls. Opus is now the default codec for all dial-in calls. * **On-Demand SIP Endpoints**: SIP endpoints for dial-out are now created on-demand when `startDialOut()` is called, improving resource efficiency and room creation reliability. * **Error Messaging**: Enhanced error messages for dial-in/out configurations during room creation. New canReceive permission, dynamic track constraints, revamped network stats, enhanced telephony features (caller ID, SIP fixes), improved platform stability and compatibility (reconnection, ICE, and more), and security updates. ## Overview As we close out the first quarter, recent updates across the Daily ecosystem deliver major new features and bug fixes. We've introduced the `canReceive` permission for fine-grained media control, revamped network statistics (networkState, networkStateReasons) while deprecating older metrics, enhanced telephony capabilities (caller ID, SIP stability, API improvements), refined recording composition, and fixed bugs across the platform. Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## daily-js [View full release notes →](https://github.com/daily-co/daily-js/releases) ### 0.77.0 #### Introduced `canReceive` Permission We extended the available permissions to include `canReceive`, which allows meeting owners to control the media tracks of others in the session. This feature enables use cases like "warm transfers" (e.g., ensuring a customer doesn't hear an agent before the handoff is complete) and other scenarios requiring selective visibility or audibility within a call. See the [participant permissions documentation](/reference/daily-js/instance-methods/participants#param-permissions) for details. #### Improved Network Quality Statistics We introduced two new fields in the object returned by [`getNetworkStats()`](/reference/daily-js/instance-methods/get-network-stats) to provide more accurate network quality data: `networkState` and `networkStateReasons`. These replace the deprecated `threshold` and `quality` fields, offering a more reliable assessment based on send/receive packet loss, round-trip time, and available outgoing bitrate. #### Bug fixes and improvements * Fixed `proxyUrl` support in `dailyConfig`. * Optimized background effects on Windows by disabling them when hardware acceleration is unavailable. * Updated internal dependencies to resolve security vulnerabilities. * Simplified `recordings_bucket` response in `room()` method for enhanced security. ### 0.76.0 #### Bug fixes and performance improvements * Updated Krisp noise cancellation library to v2.2.1. * Fixed switching back from custom video track to device track via `setInputDevicesAsync()`. * Fixed `iceTransportPolicy` configuration to correctly override domain defaults. * Added `enable_dialout` to TypeScript definitions for `DailyRoomInfo`. ### 0.75.2 #### Bug fixes * Fixed unnecessary video track re-acquisition during input settings updates. ### 0.75.1 #### Bug fixes * Fixed video constraint preservation when changing devices. * Removed console error for null `deviceId` in `setInputDevicesAsync`. ### 0.75.0 #### Enhanced Participant Type for Dial-in/out The `participant` object now includes specific `participantType` values to identify telephony participants: `sip-dial-in`, `sip-dial-out`, `pstn-dial-in`, and `pstn-dial-out`. This allows for clearer identification of how participants joined via SIP or PSTN dial-in/out. #### Dynamic Input Track Constraints Introduced support for dynamically customizing media track constraints during a call using [`updateInputSettings()`](/reference/daily-js/instance-methods/update-input-settings). This allows changing camera resolution, frame rate, or switching between front and rear cameras without rejoining. The previous constraint-setting methods (`userMediaAudioConstraints`, `userMediaVideoConstraints`, and `setBandwidth` trackConstraints) are now deprecated. #### Bug fixes and improvements * Fixed track event reliability for rapid muting/unmuting and custom tracks. * Fixed track event participant info on leave. * Improved Krisp initialization error handling. * Fixed `ondevicechange` undefined error on certain systems. * Updated Banuba SDK to eliminate need for `'unsafe-eval'` CSP directive with `avoidEval: true`. * Updated minimum supported version to 0.64.0. *** ## react-native-daily-js [View full release notes →](https://github.com/daily-co/react-native-daily-js/releases) ### 0.74.0 #### Introduced `canReceive` Permission Added support for the new `canReceive` permission for granular control over which media tracks participants receive. See the [participant permissions documentation](/reference/daily-js/instance-methods/participants#param-permissions) for details. #### Improved Network Quality Statistics Introduced `networkState` and `networkStateReasons` fields in [`getNetworkStats()`](/reference/react-native/instance-methods/get-network-stats), replacing deprecated `threshold` and `quality` metrics. #### Bug fixes and improvements * Added caller ID support for SIP PSTN transfers in `sipCallTransfer()`. * Fixed `proxyUrl` support. * Updated dependencies for security fixes. * Simplified `recordings_bucket` response in `room()` method. ### 0.73.0 #### Bug fixes * Fixed switching from custom video track back to device track. * Fixed `iceTransportPolicy` configuration override. * Added `enable_dialout` to TypeScript definitions. ### 0.72.1 #### Reverted Erroneous API Changes Reverted unintended deprecation notices for `updateInputSettings()` and dynamic track constraints to maintain React Native's existing device handling APIs. ### 0.72.0 #### Enhanced Participant Type for Dial-in/out Added specific `participantType` values for telephony participants: `sip-dial-in`, `sip-dial-out`, `pstn-dial-in`, and `pstn-dial-out`. #### Added Audio-Only Raw Tracks Recording Added `raw-tracks-audio-only` streaming config for audio-only recording support. #### Bug fixes * Fixed track event reliability for muting and custom tracks. * Updated internal REST APIs to use native utilities. * Added participant info to `track-stopped` events on leave. ### 0.71.0 #### Bug fixes and improvements * Added `sessionId` to dial-in events. *** ## daily-android [View full release notes →](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) ### 0.30.0 #### Ignore Network Devices API Introduced `CallClient.GlobalConfig.setIgnoredNetworkDevices()` to exclude specific network interfaces (VPNs, virtual adapters) during ICE candidate gathering for improved connection reliability. #### Bug fixes * Fixed ICE server configuration application via `CallClient.setIceConfig()`. ### 0.29.0 #### Automatic Cloud Recording Start Added support for automatically starting cloud recording on join when `start_cloud_recording: true` is set in the meeting token. #### Bug fixes and improvements * Fixed reconnection with expired meeting tokens. * Updated dependencies (ffmpeg, libav, libvpx) for security patches. *** ## daily-ios [View full release notes →](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) ### 0.30.0 #### Bug fixes * Fixed ICE server configuration application via `setIceConfig()`. ### 0.29.0 #### Automatic Cloud Recording Start Added support for automatically starting cloud recording on join when `start_cloud_recording: true` is set in the meeting token. #### Bug fixes * Fixed reconnection with expired meeting tokens. *** ## daily-python [View full release notes →](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) ### 0.17.0 #### Improved Linux Compatibility Recompiled against glibc 2.28 for broader Linux distribution support. ### 0.16.1 #### Bug fixes * Fixed virtual speaker device selection. ### 0.16.0 #### Update `canReceive` Permissions Support Added support for modifying remote participant's `canReceive` permission via `update_remote_participants()`. #### Bug fixes * Fixed ICE server configuration application. ### 0.15.0 #### Automatic Cloud Recording Start Added support for automatically starting cloud recording on join when `start_cloud_recording: true` is set in the meeting token. #### Bug fixes * Fixed reconnection with expired meeting tokens. *** ## Daily Prebuilt #### Transcription Language Defaults Transcription now defaults to the room/domain `lang` configuration when started via Closed Captions, falling back to English if not configured. *** ## Media Services ### Recording and Live Streaming #### New Features * **Scale Mode Overrides**: Added `videoSettings.split.scaleModeOverrides` for individual participant scaling in split layout compositions. * **Background Color Support**: VCS render pipeline now supports custom background colors via `backgroundColor` parameter. #### Bug fixes * Fixed audio-only participant filtering in baseline compositions. * Fixed instance ID handling for raw tracks recording. ### Telephony #### Enhanced Caller ID Control Added `callerId` support in `sipCallTransfer()` for PSTN transfers with improved default caller ID selection logic. #### API Improvements * Domain dial-in configs endpoint now supports filtering by multiple phone numbers. * Purchased phone numbers endpoint now includes pagination support. * Room creation API provides more specific telephony provisioning error messages. #### Bug fixes and improvements * Improved SIP INVITE handling to prevent signaling state errors. * Enhanced error messages for invalid call transfers. * Fixed layout updates for disconnected SIP participants. * Added comprehensive [transfer guide documentation](/docs/guides/features/dial-in-dial-out/transfers). ### Transcription #### Bug fixes * Fixed race condition when stopping or updating transcription immediately after starting. New transcription webhooks, better multi-instance recording support, improved telephony features, platform-wide bug fixes, and strengthened SDK reliability. ## Overview Rounding out 2024 and kicking off 2025 with major improvements across Daily's platform components: bug fixes (Safari audio processing detection, call quality test error handling), expanded feature set (improved PSTN/SIP support, expanded multi-instance recording, new transcription webhooks), and strengthened infrastructure (SDK and media service reliability). Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## daily-js ### 0.74.0 #### Updated Krisp CSP Requirements Added missing `blob:` source to the [CSP guide](/docs/guides/privacy-and-security/content-security-policy): ```js theme={null} script-src 'self' https://unpkg.com/@daily-co/daily-js blob:; ``` #### Follow-up fix for Safari Audio Processing Detection Fixed a logical error in Safari version comparison for audio processing support. We resolved an edge case where Safari versions above 18 were incorrectly flagged as unsupported. This completes the [Safari 17.4+ audio processing detection fix](/changelog/2024#fixed-audio-processing-detection-in-safari-17-0-17-3) from our previous release. This change ensures accurate audio processing support detection across all Safari versions. #### Fixed Call Quality Test Error Handling Fixed error handling in `testCallQuality()` when rate limits are hit and improved promise rejection handling to avoid uncaught errors. Test failures now properly resolve with a FAILED result instead of throwing. #### Added Session ID to Dial-in Events Added sessionId field to all dial-in events: `dialin-connected`, `dialin-error`, `dialin-stopped`, and `dialin-warning`. For example: ```js theme={null} dialinConnected: { sessionId: 'abc-123', sipFrom: '+1234567890', ... } ``` #### Version Support Update * The oldest supported version of `daily-js` is now 0.63.0. *** ## react-native-daily-js ### 0.71.0 This release also includes several core improvements shared with daily-js 0.74.0: * Added sessionId to dial-in events See the daily-js 0.74.0 section for full details on these changes. *** ## daily-android ### 0.28.0 #### Added support for Cloudflare TURN/STUN servers Added support for using Cloudflare TURN/STUN servers for ICE connections. If you're interested in using this for your Daily domain, please contact [help@daily.co](mailto:help@daily.co) #### Fixed Android Proguard Build Configuration Fixed an issue where apps using R8/Proguard would fail to build due to missing Banuba video processor classes: * Added proper Proguard configuration for optional Banuba SDK dependencies * Ensured R8 optimization doesn't fail on missing video processor classes * Maintained backwards compatibility with existing video processor integrations This change prevents build failures in release builds when the video processor plugin is not included. ### 0.27.0 #### Fixed crash when creating and deleting `VideoTextureView`/`VideoTrackCapturerToSurface` without also creating a `CallClient`. Fixed an issue where `daily-android` would crash when cleaning up video views without an active CallClient: * Moved native library initialization to shared context to prevent premature loading * Added safety checks to prevent cleanup of uninitialized native contexts * Improved synchronization of client tracking in shared context This change prevents crashes when creating and destroying video views without an active call client, improving stability for applications that conditionally initialize video components. #### Enhanced Call Reliability during Meeting Moves Improved call reliability during meeting moves by implementing increased retry attempts for meeting moves to handle longer backend processing times. *** ## daily-ios ### 0.28.0 #### Added support for Cloudflare TURN/STUN servers Added support for using Cloudflare TURN/STUN servers for ICE connections. If you're interested in using this for your Daily domain, please contact [help@daily.co](mailto:help@daily.co) ### 0.27.0 #### Enhanced Call Reliability during Meeting Moves Improved call reliability during meeting moves by implementing increased retry attempts for meeting moves to handle longer backend processing times. *** ## daily-python ### 0.14.2 #### Fixed type hints for CallClient.send\_app\_message() Fixed type hints for `CallClient.send_app_message()` to match the expected payload structure. ### 0.14.1 #### Enhanced App Message Serialization Added `serialize_none` option to `CallClient.send_app_message()` to control whether `None` values are serialized to JSON `null` or ignored. This enables more flexible data handling in application messages. ### 0.14.0 #### Added DTMF Support for Dial-out Calls Added the ability to send DTMF tones in dial-out sessions using `CallClient.send_dtmf()`[docs](https://reference-python.daily.co/api_reference.html#daily.CallClient.send_dtmf): * Send numeric tones (0-9), symbols (#, \*) and letters (A-D) to control interactive voice systems * Control automated phone menus and voicemail systems during dial-out calls * Specify session ID and tone sequences via simple settings object This addition enables programmatic interaction with phone systems that require DTMF input during dial-out calls. #### Added SIP REFER Support Added support for transferring SIP calls to external endpoints via `CallClient.sip_refer()` ([docs](https://reference-python.daily.co/api_reference.html#daily.CallClient.send_dtmf)): * Transfer ongoing SIP dial-in calls to external SIP endpoints * Control transfers using session ID and destination endpoint * Support for standard SIP REFER mechanism This addition enables seamless call transfers between Daily and external SIP systems. #### Added Daily-to-Daily SIP Call Transfer Support Added support for transferring SIP/PSTN calls between Daily rooms via `CallClient.sip_call_transfer()` ([docs](https://reference-python.daily.co/api_reference.html#daily.CallClient.sip_call_transfer)): * Transfer SIP dial-in calls between Daily rooms * Transfer dial-out calls to other SIP/PSTN endpoints * Control transfers using session ID and destination endpoint This addition enables seamless call transfers within the Daily ecosystem and to external numbers. #### Changed field name in Dial Events Renamed `participantId` to `sessionId` in `DialoutEvent` and `DialinEvent` objects for consistency #### Enhanced Call Reliability during Meeting Moves Improved call reliability during meeting moves by implementing increased retry attempts for meeting moves to handle longer backend processing times. *** ## Daily Prebuilt * Enhanced accessibility across Prebuilt with ARIA attributes, improved keyboard navigation, semantic HTML, updated labels and roles, and better screen reader support to meet WCAG standards. * Fixed issue where GIFs with empty titles were not rendered in chat. * Fixed a case where flickering in chat input height occurred while typing. *** ## Media Services ### Recording and Live Streaming #### Support Multiple Recording Types with Multi-Instance Recording Added support for running different recording types simultaneously using instance IDs: ```js theme={null} // Start a cloud recording await call.startRecording({ instanceId: '123e4567-e89b-12d3-a456-426614174000', type: 'cloud', layout: { preset: 'default', }, }); // Start raw-tracks recording in parallel await call.startRecording({ instanceId: '987fdef0-c65b-11d1-b2e2-526614174000', type: 'raw-tracks', }); ``` * New `type` parameter in `startRecording()` ([docs](/reference/daily-js/instance-methods/start-recording)) API accepts `'cloud'`, `'raw-tracks'`, or `'local'` * Recording types must be enabled via `enable_recording` property on room/domain * Each recording instance requires a unique UUID via `instanceId` parameter You can now run cloud recordings and raw track captures simultaneously, enabling both processed and unprocessed recording workflows from a single call. #### Added Audio-Only Support for Raw Tracks Recording Added support for audio-only recording in raw-tracks recordings: ```typescript theme={null} // Start a raw-tracks recording with audio only await call.startRecording({ instanceId: '123e4567-e89b-12d3-a456-426614174000', type: 'raw-tracks', layout: { preset: 'raw-tracks-audio-only', }, }); ``` * New `raw-tracks-audio-only` preset captures only audio tracks, reducing storage requirements and simplifying post-processing workflows * Preset must be specified at recording start - cannot be changed via `updateRecording()` * Compatible with existing `raw-tracks` processing tools for audio extraction and analysis This addition makes raw-tracks recordings more flexible for audio-focused use cases where individual tracks are needed for post-processing like podcasting, voice analysis, and audio archival. #### Added Raw Tracks Processing Tools Released an open-source suite of tools for processing raw-tracks recordings: * New [`raw-tracks-tools` CLI](https://github.com/daily-co/raw-tracks-tools) provides tools for analyzing, converting and compositing WebM files from raw-tracks recordings * Supports track alignment, synchronization, and layout composition using Daily's VCS engine * Complements existing GUI tools like CloudConvert and Handbrake with more precise control over WebM processing This release makes raw-tracks recordings more accessible for post-processing workflows, especially for high-quality audio/video production. #### Added Audio Recording Loss Detection Added automatic detection of audio quality issues in recordings and streams. This fixes issues where audio quality issues where audio or video tracks were visible during the live call but not in the final recording/stream. #### Improved Recording Reliability with Network Optimizations Enhanced network reliability for all recording types (cloud, raw-tracks, live streaming) and phone integration: * Increased UDP buffer sizes to prevent packet loss between servers * Added server-to-server retransmission between SFU and media workers * Reduces recording freezes and dropped tracks in environments with network congestion #### Fixed Missing Tracks in Recordings Fixed an issue where tracks could be missing from recordings when added during WebSocket reconnections between SFU and recording services. New tracks are now properly subscribed even during connection re-establishment. ### Telephony #### Renamed Dialout Timeout Configuration Field Renamed dialout configuration parameter from `max_alone_duration_sec` to `max_idle_timeout_sec` to better describe its purpose of controlling how long dialout participants remain in an idle room. #### Added Participant Type Support in participants() for Phone Participants Added distinct participant types for phone participants in [participants()](/reference/daily-js/instance-methods/participants) to enable better handling of SIP and PSTN users: * New participant types: `sip-dial-in`, `sip-dial-out`, `pstn-dial-in`, `pstn-dial-out` * Automatically set for all phone-based participants, including umbrella participants This helps developers better identify and manage phone participants in their applications. #### Fixed Dialout ICE Connection Reliability Fixed an issue where SIP dialout calls could hang during ICE connection negotiation, improving the reliability of phone system integration. #### Added `dialout_config` to Room Configuration Added `dialout_config` ([docs](/reference/rest-api/rooms/get-room#response-config-dialout-config)) as a room-level configuration option to control dialout behavior: * `dialout_config.allow_room_start`: Allow dialouts to start an empty room * `dialout_config.max_idle_timeout_sec`: Control how long a dialout participant can remain in a room alone * `dialout_config.dialout_geo`: Specify region for dialout sessions ### Transcription #### Unified Transcription Configuration Across Live and Batch Processing Aligned transcription configuration options between live transcription and batch processing for consistent API usage: ```js theme={null} // Live transcription await call.startTranscription({ language: 'fr-CA', model: 'nova-2-general', }); ``` * All configuration options from [live transcription](/reference/daily-js/instance-methods/start-transcription) (language, model, punctuate, etc.) now supported in [batch processing](/reference/rest-api/batch-processor/reference/submit-job) * Previous undocumented `params` field maintained for backward compatibility * Added new `extra` field for additional Deepgram parameters in both APIs Simplifies transcription configuration by providing a consistent interface across real-time and batch processing workflows. #### Added Room Name in Transcription API Enhanced transcription list and info endpoints with room name support: ```js theme={null} // List transcripts with room name filter GET /api/transcripts?room_name=my-room* // Transcript info now includes room name { "transcriptId": "123", "roomId": "456", "roomName": "my-room", // ...other fields } ``` ### Webhooks #### New transcription webhook events Added three new webhook events to track the full lifecycle of transcriptions: * [`transcript.started`](/reference/rest-api/webhooks/events/transcript-started) indicates transcription has begun, including metadata like room ID, instance ID and initial status * [`transcript.ready-to-download`](/reference/rest-api/webhooks/events/transcript-ready-to-download) fires when transcription completes, providing S3 location details for downloading the transcript * [`transcript.error`](/reference/rest-api/webhooks/events/transcript-error) provides detailed error information and context if transcription fails during any stage Monitor and automate transcription workflows with comprehensive status updates and error handling through webhooks. ## Other ### Added support for Cloudflare TURN/STUN servers Added support for using Cloudflare TURN/STUN servers for TURN/STUN connections. If you're interested in using this for your Daily domain, please contact [help@daily.co](mailto:help@daily.co) # Changelog Source: https://docs.daily.co/changelog/index The latest updates and improvements from the Daily engineering team. Updates from the Daily engineering team across daily-js, Daily React, React Native, daily-python, telephony, and the REST API. Older entries live in the [yearly archives](#older-updates). EU region transcription, SIP video dial-out quality parameters, cross-SDK domain failover, native SDK stability fixes, and SIP 488 codec negotiation responses. ## Overview June brings automatic domain failover (`daily.co` → `dailywebrtc.com`/`.net`) across all client SDKs for improved connection resilience. daily-js 0.91.0 and react-native-daily-js 0.86.0 add support for specifying the EU region for transcription and new quality parameters (`fps`, `width`, `height`, `videoBitrate`) for SIP video dial-out. The native SDKs — daily-ios 0.38.0, daily-android 0.38.0, and daily-python 0.30.0 — ship important stability fixes, and daily-python adds custom screen-share audio tracks. On the server side, SIP interoperability improves with proper `488 Not Acceptable` responses for incompatible codecs and custom SIP header passthrough for the Daily provider. Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## daily-js [View full release notes →](https://github.com/daily-co/daily-js/releases/tag/daily-js-2026-06-10-0.91.0) ### 0.91.0 * Added support for specifying the EU region for [transcription](/reference/daily-js/instance-methods/start-transcription). * Added `fps`, `width`, `height`, and `videoBitrate` parameters to SIP video [dial-out](/reference/daily-js/instance-methods/start-dial-out). * External-facing track names are now included in the list of reserved values that are disallowed as custom track names. * Added automatic client domain failover (`daily.co` → `dailywebrtc.com`/`dailywebrtc.net`) for improved connection resilience. * Updated dependencies to resolve vulnerabilities. *** ## react-native-daily-js [View full release notes →](https://github.com/daily-co/react-native-daily-js/releases/tag/react-native-daily-js-2026-06-10-0.86.0) ### 0.86.0 * Added support for specifying the EU region for transcription. * Added `fps`, `width`, `height`, and `videoBitrate` parameters to SIP video dial-out. * Added support for AsyncStorage v2 and v3. * External-facing track names are now included in the list of reserved values that are disallowed as custom track names. * Added automatic client domain failover (`daily.co` → `dailywebrtc.com`/`dailywebrtc.net`) for improved connection resilience. * Updated dependencies to resolve vulnerabilities. *** ## daily-ios [View full release notes →](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) ### 0.38.0 * Added `CallClient.initAsync()`, a static async factory that runs the blocking WebRTC/MediaSoup initialization off the main thread, keeping the UI responsive during `CallClient` creation. * Added automatic fallback to `dailywebrtc.com` and `dailywebrtc.net` when `daily.co` authoritative nameservers are unreachable, improving connection resilience. * Adaptive Bitrate (ABR) is now enabled by default for camera tracks. It can still be disabled by setting `allowAdaptiveLayers` to `false` in the camera `sendSettings`. * Fixed an issue where SDK resources were not properly cleaned up when all `CallClient` instances were released. * Fixed an intermittent crash when force-quitting and relaunching the app while the camera was active. * Fixed support for the `cloud-audio-only` recording type. * Improved connection performance with a new HTTP client featuring connection timeouts and TLS configuration reuse. *** ## daily-android [View full release notes →](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) ### 0.38.0 * Added automatic fallback to `dailywebrtc.com` and `dailywebrtc.net` when `daily.co` authoritative nameservers are unreachable, improving connection resilience. * Adaptive Bitrate (ABR) is now enabled by default for camera tracks. It can still be disabled by setting `allowAdaptiveLayers` to `false` in the camera `sendSettings`. * Fixed a segmentation fault that could occur when a `CallClient` was released (or garbage collected) while still in a call. * Fixed support for the `cloud-audio-only` recording type. * Improved connection performance with a new HTTP client featuring connection timeouts and TLS configuration reuse. *** ## daily-python [View full release notes →](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) ### 0.30.0 * Added automatic fallback to `dailywebrtc.com` and `dailywebrtc.net` when `daily.co` authoritative nameservers are unreachable, improving connection resilience. * Fixed a use-after-free that could cause a segmentation fault in the audio playout thread if a `CallClient` was released while still in a call with participant audio renderers registered. * Fixed a segmentation fault that could occur when a `CallClient` was released (or garbage collected) while still in a call. ### 0.29.1 * Fixed an issue where a room configured with `enable_recording` set to `cloud-audio-only` was ignored when starting a recording, causing the recording to also capture video. * Fixed an issue where `StartRecordingProperties` none properties were serialized to `null`. ### 0.29.0 * Added support for `screenAudio` input, making it possible to send a custom screen-share audio track alongside or independently of `screenVideo`. *** ## Media Services ### Telephony * Added server-side support for a new `startDialIn()` method to dynamically start a SIP dial-in endpoint. The client-side API will be included in an upcoming daily-js release. * SIP calls with incompatible codecs now receive a proper `488 Not Acceptable` error response, and the `rtpmap` for the offered Opus codec has been fixed for better codec negotiation interoperability. * Custom SIP headers are now passed through for calls using the Daily SIP provider. * `dialout-error` events now include custom SIP headers. * Fixed a crash that could occur when a SIP session timed out after the session had already been terminated. ### Recording * The `recording-error` event now includes the `recordingId`, making it easier to correlate errors with specific recordings. ### Streaming * Fixed a VCS composition issue where pinned tracks that dropped from an explicit pin list were not evicted from the layout. ### Other * Oldest supported daily-js version bumped to 0.85.0, with 0.86.0 approaching end-of-support. *** ## REST API * Added `created_at` and `updated_at` to [transcripts API](/reference/rest-api/transcripts) responses. * Added an opt-in `include_sip_participants` query parameter to [`GET /meetings/:id`](/reference/rest-api/meetings/get-meeting). Critical Firefox 149 fix, `provider` and `sipCallId` in telephony events, `mtgSessionId` in presence endpoints, and VCS simulator video mode. ## Overview May brings daily-js 0.90.0 and react-native-daily-js 0.85.0 with a critical fix for a Firefox 149 regression that prevented remote participant media from playing, and new `provider` and `sipCallId` fields across all dial-in and dial-out events for improved telephony observability. On the server side, presence endpoints now include `mtgSessionId`, and the VCS simulator gains video mode support along with a layer pinning fix. Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## daily-js [View full release notes →](https://github.com/daily-co/daily-js/releases/tag/daily-js-2026-05-13-0.90.0) ### 0.90.0 * Added `provider` and `sipCallId` fields to all `dialin-*` and `dialout-*` events, making it easier to correlate telephony events with specific calls and providers. * ⚠️ Fixed a Firefox 149 regression that caused remote participant media not to play. * Fixed a corner case in iOS user-agent detection. * Updated dependencies to resolve vulnerabilities. *** ## react-native-daily-js [View full release notes →](https://github.com/daily-co/react-native-daily-js/releases/tag/react-native-daily-js-2026-05-13-0.85.0) ### 0.85.0 * Added `provider` and `sipCallId` fields to all `dialin-*` and `dialout-*` events. * Updated dependencies to resolve vulnerabilities. *** ## daily-python [View full release notes →](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) ### 0.28.1 * Fixed a segmentation fault that could occur after virtual microphone or speaker inputs or publishing updates. *** ## Media Services ### Streaming * Added video mode to the VCS simulator, allowing developers to test video layouts directly in the simulator. * Fixed a track add bug in VCS layer pinning. *** ## REST API * Added `mtgSessionId` to the responses of [`GET /presence`](/reference/rest-api/presence/get-presence) and `GET /rooms/:name/presence`. daily-react 0.25.0 with multi-instance recording and streaming, daily-python 0.28.0 with screen share custom tracks, and SIP telephony improvements. ## Overview April brings daily-react 0.25.0 with support for multiple concurrent recording and live streaming instances, and daily-python 0.28.0 with screen share custom tracks and Adaptive Bitrate enabled by default for camera tracks. On the server side, SIP dial-in and dial-out events now include `sipCallId` and `provider` fields, Signalwire is selectable as a telephony provider, and VCS streaming gains a configurable label placement parameter along with a mic status indicator. Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## daily-react [View full release notes →](https://github.com/daily-co/daily-react/releases) ### 0.25.0 * Added support for multiple concurrent recording and live streaming instances. `useRecording()` and `useLiveStreaming()` now accept an `instanceId` parameter for per-instance state. New `useRecordingInstances()` and `useLiveStreamingInstances()` hooks return all active instances, and new `RecordingInstanceState` and `LiveStreamingInstanceState` types are exported. Stopping or erroring one instance no longer clears state for others. * Added `networkStatus` to the return value of `useNetwork()`. * Added support for the `cloud-audio-only` recording type. * The daily-react version is now reported through daily-js via the new `aboutClient` API, so it's included in call logs regardless of which library creates the call instance. *** ## daily-python [View full release notes →](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) ### 0.28.0 * Added support for `screenVideo` custom tracks. You can now send a custom screen share track by setting `inputs.screenVideo.settings.customTrack.id`. * Adaptive Bitrate (ABR) is now enabled by default for camera tracks. It can still be disabled by setting `allow_adaptive_layers` to `False` in the camera `send_settings`. * Fixed support for the `cloud-audio-only` recording type. * Fixed panics in signalling reconnect paths when room lookup returns no worker. *** ## Daily Prebuilt * Tightened the iOS user-agent match to reject spoofed iOS substrings. * Fixed transcription language fallback so that an empty string falls back to the default language (switched from `??` to `||`). *** ## Media Services ### Telephony * SIP dial-in and dial-out events now include `sipCallId` and `provider` fields, making it easier to correlate events with specific calls and providers. * Signalwire and Daily can now be selected as a telephony provider for dial-in and dial-out. * Standardized the `provider` value to lowercase `signalwire` in dial-in and dial-out events. * Added `provider` as a valid parameter for the `startDialout` API. * Added support for video track updates on SIP video calls. * Fixed screen share track addition for SIP video. * Added support for configuring a Cloudflare anycast IP address instead of the STUN server hostname. ### Recording * Added a new `enable_raw_tracks_event_json` domain and room property to opt into JSON event output for raw-tracks recordings. * Added more WAV settings for raw-tracks transcoded mode. * Raw-tracks listings now verify S3 item `ContentType` to avoid including unexpected files. ### Streaming * Added a new VCS parameter `videoSettings.grid.labelPlacement` for customizing participant label position in streaming layouts. * Added a mic status indicator and a highlight border inset to VCS layouts. * Fixed VCS participant label misindexing in audio-only layouts. Smaller call-machine bundle (\~2MB less), VCS layout animations, configurable max API keys, and daily-js 0.81.0 minimum version bump. ## Overview March includes significant performance improvements for daily-js and react-native-daily-js, with the initial call-machine bundle now \~2MB smaller through dynamic loading. Audio processor errors no longer automatically mute the microphone, and `testCallQuality()` reliability has been fixed. On the server side, VCS streaming layouts gain animation support, and a new `max_api_keys` domain property lets you limit API key creation per domain. The oldest supported daily-js version has been bumped to 0.81.0, with versions through 0.83.1 approaching end-of-support. Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## daily-js [View full release notes →](https://github.com/daily-co/daily-js/releases) ### 0.89.1 * Fixed an issue causing all `testCallQuality()` tests to fail. * Fixed remaining vulnerabilities in dependencies. ### 0.89.0 * Reduced initial call-machine bundle download size by \~2MB. Larger portions are now dynamically loaded as-needed. * Audio processor errors no longer automatically mute the microphone. * Removed the client-side 3-hour limit for raw-tracks recordings. Limits are now handled server-side. * Fixed auto-syncing audio output device on Safari, where `setSinkId` requires a user gesture and was causing repeated errors. * Improved system idle detection to avoid stale reconnect retries after 5 minutes of inactivity. Calls will now leave cleanly in idle scenarios. * **Breaking:** If you self-host the call-machine-bundle using `callObjectBundleUrlOverride`, a new separate bundle is now required for Krisp noise cancellation. Contact [help@daily.co](mailto:help@daily.co) for upgrade instructions. *** ## react-native-daily-js [View full release notes →](https://github.com/daily-co/react-native-daily-js/releases) ### 0.84.1 * Fixed remaining vulnerabilities in dependencies. ### 0.84.0 * Reduced initial call-machine bundle download size by \~2MB with dynamic loading. * Added `digitDurationMs` option to `sendDTMF()` for configurable DTMF digit duration. * Removed the client-side 3-hour limit for raw-tracks recordings. * Improved system idle detection to avoid stale reconnect retries after 5 minutes of inactivity. * **Breaking:** If you self-host the call-machine-bundle using `callObjectBundleUrlOverride`, a new separate bundle is now required for Krisp noise cancellation. Contact [help@daily.co](mailto:help@daily.co) for upgrade instructions. *** ## Dashboard * Added a configurable `max_api_keys` domain property to set the maximum number of API keys allowed per domain. *** ## Media Services ### Telephony * Fixed video not working with Linphone SIP client. * Improved STUN server fallback during SDP negotiation retry. * Fixed hung media workers caused by unresolved SIP transport promises. * Improved handling of active SIP sessions during meeting moves. * Broadcast app messages are no longer sent to participants in the knocking (waiting room) state. * Oldest supported daily-js version bumped to 0.81.0. Versions through 0.83.1 are approaching end-of-support. ### Streaming * The new VCS pipeline is now the default for new customers. In April, we are transitioning existing customers to the new pipeline; the legacy pipeline will be EOLed later this summer. In the meantime, you can switch between the new and legacy pipelines with the `enable_legacy_compositor` flag. * Added a layout animation system for VCS (Video Compositing Service) streaming layouts. * Fixed RTMP URL handling for streaming destinations. * Improved jitter buffer latency and added video stall detection for RTMP streaming. ### Recording * Added `event-json` as a valid `dataOutputs` option for recordings via the REST API. We’re considering making `event-json` the default output format and invite feedback from the community. Note that this would add an additional `event-json` file to the existing S3 folder and may break downstream workflows that assume only media files are present. ### REST API * Added validation for the `cloud-audio-only` room and meeting token property. * New `max_api_keys` domain property to configure the maximum number of API keys per domain. DTMF tone support for SIP calls, raw-tracks recording improvements, Krisp SDK lazy loading, and webhook event ID timestamps. ## Overview February brings DTMF tone support for SIP calls, improved raw-tracks recording reliability during meeting moves, and a Krisp SDK performance optimization that splits noise cancellation into a separate bundle for faster page loads. Audio processor errors no longer mute the microphone, and the 3-hour limitation for raw-tracks recordings has been removed. Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## Daily Prebuilt * Audio processor error handling updated to no longer mute the microphone on Krisp errors. * Fixed a mute state sync race condition on the first call of the day. * Krisp SDK is now loaded as a separate bundle for faster initial page load. * Added a user-facing notification when the Krisp SDK fails to load. * Updated noise cancellation overload translations. *** ## Dashboard * Improved dashboard loading speed. * Removed references to the deprecated `owner_only_broadcast` property. *** ## Media Services ### Telephony * Added DTMF (telephone-event) support for sending and receiving DTMF tones during SIP calls. * Added a setting to keep rooms open without WebRTC participants (for SIP-only scenarios). * Fixed the call transfer initiated event for SIP REFER. * Fixed dial-in events to be preserved when no peer is in the room yet. ### Recording * Removed the 3-hour limitation for raw-tracks recordings. * Improved raw-tracks recording reliability during meeting moves when the SFU connection changes. * Added `user_name` and `user_id` to raw-tracks recording events. * Recording finished webhooks are now reliably sent from the upload finisher for improved delivery. ### Webhooks * Webhook event IDs now include timestamps, matching the format shown in the documentation. ### Transcription * Added model parameters to the transcription info field. ### REST API * Fixed the `/domains/member` endpoint to return all non-revoked API tokens. Recording dataOutputs for transcripts and chat, cloud-audio-only recording, laptop lid session handling, CPU load detection improvements, and Kamailio call transfer support. ## Overview January is a feature-rich month with new recording capabilities including `dataOutputs` for recording-synchronized transcripts, chat messages, and event logs, plus a new `cloud-audio-only` recording type. We've also improved session handling for laptop lid closures, refined CPU load detection, and shipped call transfer support on our new telephony infrastructure. Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## daily-js [View full release notes →](https://github.com/daily-co/daily-js/releases) ### 0.87.0 * Fixed an issue in Chrome where the Compute Pressure API did not work inside an iframe due to permissions policy restrictions. ### 0.86.0 * Introduced a new `dataOutputs` option in `startRecording()` to capture auxiliary data alongside the recording media. Three output types are supported: * `event-json` — a JSON file describing recording session events (media file starts, track updates, etc.) * `transcript-webvtt` — a WebVTT file with transcription events, timecode-aligned with the recording media (requires an active live transcription) * `chat-webvtt` — a WebVTT file with chat messages, timecode-aligned with the recording media Data output files are included in the recording download link and written to your custom S3 bucket when configured. Note: these outputs capture events, they do not start services. * Added support for a new `cloud-audio-only` recording type. This produces m4a files with the `audio/mp4` content type. It can be used alongside other recording types and does not share an instance ID with RTMP/HLS streaming. * Fixed an issue where sessions would attempt to reconnect after long periods of laptop lid closure. Sessions now automatically disconnect after 5 minutes of detected inactivity. * Fixed uncaught errors in `join()` and `startCamera()` when no room URL is provided or the URL is invalid. These now properly throw an error and leave `meetingState()` in `"left-meeting"`. * Improved the `cpu-load-change` event by requiring multiple consecutive high CPU readings before reporting high load and by refining global decode time calculation to exclude videos with empty frame metrics. * Added [compute pressure state](https://developer.mozilla.org/en-US/docs/Web/API/Compute_Pressure_API) to the [`getCpuLoadStats()`](/reference/daily-js/instance-methods/get-cpu-load-stats) API when supported by the browser. * Added `max_app_message_size` to `DailyRoomInfo` types. *** ## react-native-daily-js [View full release notes →](https://github.com/daily-co/react-native-daily-js/releases) ### 0.83.0 * Introduced a new `dataOutputs` option in `startRecording()` to capture auxiliary data alongside the recording media. Supports `event-json`, `transcript-webvtt`, and `chat-webvtt` output types. * Added support for the new `cloud-audio-only` recording type. * Fixed an issue where sessions would attempt to reconnect after long periods of laptop lid closure. * Fixed uncaught errors in `join()` and `startCamera()` when no room URL is provided or the URL is invalid. * Improved the `cpu-load-change` event accuracy. * Added `max_app_message_size` to `DailyRoomInfo` types. * Added missing type definitions for streaming and recordings. *** ## Daily Prebuilt * Removed automatic camera turn-off behavior on bad network conditions. *** ## Dashboard * Updated Cloud recording storage tooltip. * Removed HIPAA 30-day trial wording. *** ## Media Services ### Telephony * Added call transfer support on the new Kamailio-based telephony infrastructure. * Added support for numeric SIP usernames. ### Recording * Added server-side support for `dataOutputs` recording option and the `cloud-audio-only` recording type. * Fixed download link file extensions for recordings stored in custom S3 buckets. Recording-synchronized transcripts, gapless audio for raw-tracks, CPU warning notifications, and Prebuilt session resume and background effects fixes. ## Overview December brings recording-synchronized transcripts, a new gapless audio pipeline for raw-tracks recording, and configurable CPU warning notifications in Daily Prebuilt. We've also improved session resume reliability after laptop lid closures and added automatic detection to disable background effects on underpowered hardware. Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## Daily Prebuilt * Added a new `enable_cpu_warning_notifications` domain and room property to control whether CPU warning notifications are shown to users. * Background effects are now automatically disabled when a WebGL software renderer is detected, preventing poor performance on underpowered hardware. * Fixed poor network state handling. * Fixed session resume issues that could occur after closing and reopening laptop lids. *** ## Dashboard * Added configuration for the new `enable_cpu_warning_notifications` property. * Updated settings deletion copy. *** ## Media Services ### Recording * Added support for recording-synchronized transcripts via `dataOutputs` in `startRecording()`. * New transcoded gapless audio pipeline for raw-tracks recording, producing continuous audio files without gaps. * Raw-tracks recording now works with the `force_audio_only_recording` flag. * Fixed file extension for WAV files in transcoded gapless audio mode. * Fixed a missing room state update during meeting move restore that could cause recording state inconsistencies. ### Transcription * Deepgram model improvement program opt-out is now enabled by default for improved data privacy. SIP codec negotiation, call transfer fixes, recording enhancements, and Prebuilt breakout room and screen sharing fixes. ## Overview November is a server-focused release with telephony improvements including SIP codec negotiation and call transfer reliability fixes, recording enhancements, and Daily Prebuilt bug fixes for breakout rooms and screen sharing on Chrome. Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## Daily Prebuilt * Fixed the "request for help" feature from the main room during breakout sessions. * Improved handling of blocked-by-browser screen share errors in Chrome. *** ## Dashboard * Removed Intercom chat widget from the dashboard. Contact us at `help@daily.co` for support. *** ## Media Services ### Telephony * Added support for preferred codec negotiation based on the SIP invite SDP. * Fixed room ID preservation during call transfers when a room is deleted mid-transfer. * Fixed SIP username filtering for pinless dial-in configuration. ### Recording * Fixed stop recording by instance ID to correctly detect the recording type. * Optimized audio bitrate to 96kbps for audio-only recordings. * The GET recordings API now includes `roomName` in the response. ### REST API * Fixed 404 errors for presence queries on deleted rooms. Narrower CSP for video backgrounds, audio-only recording property, dynamic SIP dial-in endpoints, and telephony and recording fixes. ## Overview October brings a Content Security Policy improvement for video background processing, removing the need for the broad `unsafe-eval` directive. We've also added a new `force_audio_only_recording` domain property, dynamic SIP dial-in endpoint creation, and several telephony and recording reliability fixes across our platform. Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## daily-js [View full release notes →](https://github.com/daily-co/daily-js/releases) ### 0.85.0 * You no longer need to specify the broad `unsafe-eval` in your Content Security Policy (CSP) when using video background processing (virtual backgrounds or background blur). You can now use the narrower `wasm-unsafe-eval` instead. * Fixed an issue where the initial send settings were not being respected for cam video. *** ## react-native-daily-js [View full release notes →](https://github.com/daily-co/react-native-daily-js/releases) ### 0.82.0 * Fixed an issue where the initial send settings were not being respected for cam video. *** ## Daily Prebuilt * Added a Krisp system overload notification to inform users when audio processing is under heavy CPU load. * Fixed recording handling when recording types are configured as comma-separated values. * Fixed dynamic chunk loading issues that could cause failures in certain deployment configurations. *** ## Dashboard * Removed Daily Bots dashboard links and unified domain handling. *** ## Media Services ### Telephony * Added support for dynamically creating SIP dial-in endpoints. * SIP participants are now properly ejected when a room is deleted. * Fixed presence data for dial-out participants who are the first to join a room. * Fixed pagination in the list phone numbers API. * Fixed error handling in the list available numbers API. ### Recording and Transcription * Added a new `force_audio_only_recording` domain property for audio-only recording. * Meeting-move-aware transcription is now enabled by default. * Fixed Deepgram extra metadata encoding for transcription sessions. Phone extension support for dial-out, Android 16KB page size compatibility, auto-start recording reliability, and new Picture-in-Picture events. ## Overview September adds phone extension support for dial-out operations across our client SDKs, Android 16KB page size compatibility for React Native, and improved reliability for auto-start recording and transcription. We've also added new Picture-in-Picture events for Daily Prebuilt and improved handling for unsupported browsers. Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## daily-js [View full release notes →](https://github.com/daily-co/daily-js/releases) ### 0.84.0 * Added support for calling phone extensions during dial-out operations. You can now specify an extension number and wait time before dialing the extension after the main call connects. * Added Prebuilt-specific `pip-started` and `pip-stopped` events. * Improved reliability of handling auto-start recording and transcription options when joining a room. * Improved handling for unsupported browsers. The Daily constructor will now throw an error of `WebRTC not supported or suppressed` if the browser cannot support a call. *** ## react-native-daily-js [View full release notes →](https://github.com/daily-co/react-native-daily-js/releases) ### 0.81.0 * Added support for Android 16KB page size. * Added support for calling phone extensions during dial-out operations. You can now specify an extension number and wait time before dialing the extension after the main call connects. * Improved reliability of handling auto-start recording and transcription options when joining a room. *** ## Dashboard * Added a cancel plan button to the billing interface. Chrome 140 and Firefox 142 compatibility fixes, pre-join screen sharing, Krisp CPU overload handling, telephony improvements, and Prebuilt accessibility updates. ## Overview August brings browser compatibility fixes for Chrome 140 and Firefox 142, new screen sharing capabilities before joining a call, and telephony improvements including dial-out extensions and cross-domain SIP transfers. We've also shipped Daily Prebuilt accessibility improvements, a high CPU usage notification, and stability fixes across our media services. Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## daily-js [View full release notes →](https://github.com/daily-co/daily-js/releases) ### 0.83.1 * Updated mediasoup-client for compatibility with Chrome 140. Without this fix, joining or reconnecting to a call without an audio track would fail on Chrome 140. * Added a workaround for a [Firefox 142 bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1981040) that caused `daily-js` to incorrectly report bad network conditions due to `roundTripTime`. * Fixed a regression in 0.82.0 where `captureOptions` passed to `startScreenShare()` were ignored. * Removed deprecated and unused `rtp-tracks` recording support. ### 0.82.0 * Added support for calling `startScreenShare()` prior to `join()`. A call to `preAuth()` or `startCamera()` is still required first to initialize state. * Fixed and improved error handling for `setOutputDevicesAsync()`. The method now throws an error if the call fails. * Updated dependencies to resolve security vulnerabilities. * Bumped Krisp to the latest version with updated models. Added handling for CPU overload which can cause choppy voice output. This will temporarily and silently pause audio processing while the CPU recovers. If overload continues, `daily-js` will emit an [`audio-processor-error`](/reference/daily-js/events/error-events#audio-processor-error) with `errorMsg` of `"Krisp error: system overload"`, turn off the processor, and mute the microphone. *** ## react-native-daily-js [View full release notes →](https://github.com/daily-co/react-native-daily-js/releases) ### 0.80.0 * Updated mediasoup-client dependency to latest. * Removed deprecated and unused `rtp-tracks` recording support. ### 0.79.0 * Updated dependencies to resolve security vulnerabilities. *** ## Daily Prebuilt * Fixed unblock instructions in embedded Prebuilt on Chrome on iOS. * Added a high CPU usage notification snackbar. * Improved accessibility throughout the Prebuilt interface. *** ## Media Services ### Telephony * Added support for providing a phone extension in `startDialout()`. * Enabled SIP call transfer to non-Daily domains. * Improved handling of dial-in properties when creating rooms with dial-in configured. * Added `roomName` to transcription webhook events. ### Recording * Fixed a bug where audio tracks were not being added to recordings. New network status callbacks, improved telephony error handling, and platform-wide stability fixes. ## Overview This month's updates bring new network status callbacks to our native mobile SDKs, giving you more insight into connection health. We've shipped a new `dialin-error` type (`start-failed`) that triggers when SIP registration fails, a critical audio playback fix for Safari in Daily React, and numerous stability improvements across our backend services. Complete release history for our client SDKs is available in our Github release notes: * [daily-js](https://github.com/daily-co/daily-js/releases) * [react-native-daily-js](https://github.com/daily-co/react-native-daily-js/releases) * [daily-react](https://github.com/daily-co/daily-react/releases) * [daily-ios](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) * [daily-android](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) * [daily-python](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) *** ## daily-js [View full release notes →](https://github.com/daily-co/daily-js/releases) ### 0.81.0 * Added `ignoreAudioLevel` parameter to `startCustomTrack()` to exclude a track from audio level calculations. * Improved `enumerateDevices()` logging and reduced noisy error reporting. *** ## react-native-daily-js [View full release notes →](https://github.com/daily-co/react-native-daily-js/releases) ### 0.78.0 * Added a new `dialin-error` type to `start-failed`. * Fixed a potential null pointer exception during module initialization. * Improved `enumerateDevices()` logging and reduced noisy error reporting. *** ## daily-react [View full release notes →](https://github.com/daily-co/daily-react/releases) ### 0.23.1 * Fixed an issue that could prevent audio playback in Safari after a participant leaves. * Added an optional `interval` parameter to the `useAudioLevelObserver()` hook. * Updated the `useNetwork()` hook to return the new `networkState` and `networkStateReasons` properties. * Added `user_name` and `rawResponse` to the `Transcription` type definition. *** ## daily-android [View full release notes →](https://github.com/daily-co/daily-client-android/blob/main/CHANGELOG.md) ### 0.32.0 * Added a new `onNetworkConnectionStatusUpdated()` callback to monitor call-critical network connections. * Added support for handling mid-call session ID changes. * Fixed a potential null pointer exception during device list changes. *** ## daily-ios [View full release notes →](https://github.com/daily-co/daily-client-ios/blob/main/CHANGELOG.md) ### 0.32.0 * Added a new delegate function (`networkConnectionStatusUpdated`) to monitor call-critical network connections. * Added support for handling mid-call session ID changes. * Fixed a bug where `userID` could be `nil` in the call configuration, even when present in the token. *** ## daily-python [View full release notes →](https://github.com/daily-co/daily-python/blob/main/CHANGELOG.md) ### 0.19.4 * Added `set_log_level()` and a `log_level` init argument for controlling log verbosity. *** ## Media Services ### Telephony and Recording/Livestreaming * Added a new `start-failed` error type to the `dialin-error` event. This fires when the SIP address on a room fails SIP registration with the SIP network. * Improved handling of non-unique 'from' numbers in PSTN dial-in, allowing multiple participants to join from the same caller ID. * Shipped numerous stability improvements for SIP registration, raw tracks recording, and connection reliability. ## Older updates 5 updates from 2025. 8 updates from 2024. 18 updates from 2023. 16 updates from 2022. 12 updates from 2021. 8 updates from 2020. # Daily Client SDK for Android Source: https://docs.daily.co/docs/android/index Build real-time video and audio calling into your native Android applications. The Daily Client SDK for Android is a native Kotlin/Java library built on WebRTC. It handles media negotiation, network traversal, and participant management so you can focus on building your UI. Build a working video call app step by step Add screen share to your Android app Full Kotlin/Java API docs ## Hello, world Add the SDK to your `build.gradle`, then join a call: ```groovy theme={null} dependencies { implementation 'co.daily:client:latest.release' } ``` ```kotlin theme={null} import co.daily.CallClient import co.daily.CallClientListener import co.daily.model.Participant val call = CallClient(applicationContext) val videoViews = mutableMapOf() call.addListener(object : CallClientListener { override fun onParticipantJoined(participant: Participant) { val videoView = VideoView(applicationContext) videoViews[participant.id] = videoView addVideoViewToMyUI(view = videoView) videoView.track = participant.media?.camera?.track } override fun onParticipantUpdated(participant: Participant) { videoViews[participant.id]?.track = participant.media?.camera?.track } }) call.join(url = "https://your-domain.daily.co/room-name") { it.error?.apply { Log.e(TAG, "Error joining: $msg") } it.success?.apply { Log.i(TAG, "Joined successfully") } } // Turn off camera call.setInputsEnabled(camera = false) // Leave and release when done call.leave { } call.release() ``` ## API structure The SDK is organized around the tasks involved in building a call. For each task you get methods to **update** state, methods to **get** current state, and **events** via `CallClientListener` to react to changes. `join()`, `leave()`, `callState()`, `CallStateUpdated` `participants()`, `ParticipantJoined`, `ParticipantUpdated`, `ParticipantLeft` `updateInputs()`, `setInputsEnabled()`, `startScreenShare()`, `InputsUpdated` `updatePublishing()`, `setIsPublishing()`, `PublishingUpdated` `updateSubscriptions()`, `updateSubscriptionProfiles()`, `setSubscriptionState()` `activeSpeaker()`, `ActiveSpeakerChanged` ## Resources A complete working example on GitHub Production-ready boilerplate from Daily's engineering team ## Requirements * minSdkVersion >= 23 (Android 6.0) * Kotlin 1.6 or later recommended # Installing the Daily Client SDK for Android Source: https://docs.daily.co/docs/android/installation Learn how to get set up with the Daily Client SDK for Android. ## Minimum SDK version The Daily Client SDK for Android requires a `minSdkVersion` of 21 or greater. ## Installing using Maven In your top-level `build.gradle` file, add the Maven Central repository: ```groovy theme={null} repositories { // ... mavenCentral() } ``` In your app’s `build.gradle` file, add the Daily Client SDK dependency: ```groovy theme={null} dependencies { // ... implementation 'co.daily:client:0.39.1' } ``` Optionally, to use the camera video processor (for background blur and replacement), also add the following dependency: ```groovy theme={null} dependencies { // ... implementation 'co.daily:client-videoprocessor-plugin:0.1.1' } ``` ## App setup Make sure the following permissions are in your app’s `AndroidManifest.xml`: ```xml theme={null} ``` > The `foregroundServiceType` `phoneCall`, `camera` and `microphone` are needed in order to give your app the ability to continue running calls while in the background > The `foregroundServiceType` `mediaProjection` is only needed if you wish to do screen sharing. ## Continuing calls in the background In order to give your app the ability to continue running calls while in the background, you can start a foreground service that shows an in-call notification while your call is active. You can read more about foreground services in the [Android documentation](https://developer.android.com/guide/components/foreground-services). For an example of a foreground service that customizes and shows a notification, see [the one that is bundled with react-native-daily-js](https://github.com/daily-co/react-native-daily-js/blob/main/android/src/main/java/com/daily/reactlibrary/DailyOngoingMeetingForegroundService.java). *** To learn more about building an app with the Daily Client SDK for Android, see our [Android Overview](/docs/android) and [Android demo app](https://github.com/daily-demos/daily-android-demo). # Daily Android Mobile SDK Kotlin Quickstart Source: https://docs.daily.co/docs/android/quickstart Learn how to get started using the Daily Android Mobile SDK with Kotlin to create a video mobile application In this quickstart, we will introduce you to [Daily's Client SDK for Android](/docs/android) by walking through the creation of a simple video call application. By the end of this walkthrough, you'll have a starting point for writing Daily-powered video apps for Android with Kotlin. This walkthrough will implement the following features: * Creating and joining a public Daily room * Asking for required application permissions * Displaying videos from local and remote room participants * Managing local microphone and camera input * Leaving the call You can find the full codebase of this Android app [on GitHub](http://github.com/daily-demos/quickstart-android). ## Prerequisites Before you get started, make sure you have a functioning Android development environment including [Android Studio](https://developer.android.com/studio). You will also need a [free Daily account](https://www.daily.co/signup). If you plan to test your app on a hardware device, make sure you have [debugging enabled on the device](https://developer.android.com/studio/debug/dev-options). If you plan to test your app on an emulator, make sure you [have a virtual device created](https://developer.android.com/studio/run/managing-avds). Note that by default, the emulator will use an emulated camera. ## Create a Daily room Begin by navigating to Daily's developer dashboard and [creating a new Daily room](https://dashboard.daily.co/rooms/create). Create a Room in the Daily Dashboard Give your room a name and click the "Create room" button. All other options can be left at their defaults. Make note of your Daily room URL. We'll be using it in the code shortly. ## Create a new Android project Next, open Android Studio. From the Welcome screen, create a new project using the "Empty Views Activity" template. Create a new project using the Empty Activity template Give your project a name and make sure the project language is set to Kotlin. Next, click the "Finish" button to have Android Studio set up your new project. Note that if this is your first Kotlin project setup, it can take a few minutes for Android Studio to download relevant dependencies and initialize the project for the first time. ## Add a dependency on Daily's Client SDK for Android With your new project created, you can add the Daily's Android SDK as a dependency. Android Studio uses [Gradle](https://gradle.org/) as its default build tool and dependency manager. Open the project's Gradle build script in the app directory: Add Daily's Client SDK for Android as a dependency to Gradle Once open, add `'co.daily:client:0.8.0'` to the dependencies object: ```kotlin theme={null} dependencies { implementation 'co.daily:client:0.8.0' //... } ``` The latest release of Daily's Client SDK for Android is 0.8.0 at the time of this publication. When developing your production application, we recommend referencing the latest stable version . Save the Gradle file and then sync the changes by using the `Sync Project with Gradle Files` command in the Android Studio File menu. Sync Project with Gradle Files ## Import namespaces and classes After adding a dependency on Daily's Android SDK, we can import relevant classes. Open `app/java/com.example.[YOUR_APP_NAME]/MainActivity.kt` and add the following `import` statements: ```kotlin theme={null} // ...Existing imports above... ``` ## Setting up the Main Activity The next step is modifying the `MainActivity` class to set up the basic structure of our application. Start by adding the `TAG` member variable and two functions: * `initializeCallClient()` * `checkPermissions()` Then, add a call to `checkPermissions()` from the `onCreate()` function. ```kotlin theme={null} class MainActivity : AppCompatActivity() { private val TAG: String = "MainActivity" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) checkPermissions() } private fun initializeCallClient() { // Initialize the Daily Call Client object once permissions have been granted } private fun checkPermissions() { // Check permissions have been granted and if not, ask for permissions } } ``` Build the application to verify that there are not errors or warnings. ## Configure application permissions The application will need a number of user permissions in order to access the internet, use the device camera, and record and play audio. To request these permissions, we have to declare what we need as part of our app manifest. Open `manifests/AndroidManifest.xml`. Then, add the following `uses-permission` and `uses-feature` elements inside the `` tag: ```xml theme={null} ``` Modifying the app manifest tells Android which permissions the app *intends* to use. However, we still need to request these permissions from the user. To do so, we'll add some additional code to `MainActivity.kt`, which uses Android's built-in tools for creating [runtime permission requests](https://developer.android.com/training/permissions/requesting). First, create a member variable named `requestPermissionLauncher` and use it to register an activity using the `RequestMultiplePermissions` [contract](https://developer.android.com/reference/androidx/activity/result/contract/ActivityResultContracts): ```kotlin theme={null} private val TAG: String = "MainActivity" private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> if (result.values.any { !it }) { checkPermissions() } else { // Permission is granted, we can initialize the call initializeCallClient() } } ``` The activity `RequestMultiplePermissions` takes an array of permissions the application would like to request. In the activity callback, we check to see if additional permissions are needed. If so, we run `checkPermissions()`. If not, the application can call `initializeCallClient()` to begin setting up the video call. Next, implement the `checkPermissions()` method you defined within the `MainActivity` class as follows: ```kotlin theme={null} private fun checkPermissions() { // Check whether permissions have been granted. // If not, ask for permissions val appContext: Context = applicationContext val permissionList: Array = appContext.packageManager.getPackageInfo( appContext.packageName, PackageManager.GET_PERMISSIONS ).requestedPermissions val notGrantedPermissions:MutableList = ArrayList() for(permission in permissionList) { if (ContextCompat.checkSelfPermission(appContext, permission) != PackageManager.PERMISSION_GRANTED) { notGrantedPermissions.add(permission) } } if (notGrantedPermissions.isNotEmpty()) { requestPermissionLauncher.launch(notGrantedPermissions.toTypedArray()) } else { // Permission is granted, we can initialize the call initializeCallClient() } } ``` The `checkPermissions()` method above gets the list of permissions the application needs from the app manifest and checks whether they have already been granted. If all of permissions have been granted, the application begins initializing the Daily `CallClient` object by calling the `initializeCallClient()` method. If the application requires additional permissions from the user, it launches the built-in request flow. For this quickstart we're not handling conditions where the user declines permissions, but in a real application you would want to do that. ## Define the user interface After implementing permission handling, we can define the user interface for our application. The user interface will include the following: * Displaying participants' video * Presenting call controls for toggling the camera/microphone and leaving the meeting To implement the above, we'll create two main UI elements: * A main UI that contains call controls and a participant list * A wrapper around the Daily [`VideoView`](https://reference-android.daily.co/daily-android/co.daily.view/-video-view/index.html) that holds each participant's video Open the `/res/layout/activity_main.xml` layout file and switch to Code view: Editing UI Code in Android Studio Replace the default UI XML with the following: ```xml theme={null}
``` ```css styles.css theme={null} body { font-family: sans-serif; background: #0f0f1a; color: #e0e0e0; margin: 0; padding: 1rem; } .controls { display: flex; gap: 0.5rem; margin-bottom: 1rem; } button { padding: 0.5rem 1rem; background: #e94560; color: white; border: none; border-radius: 4px; cursor: pointer; } button:disabled { opacity: 0.4; cursor: not-allowed; } .track-grid { display: flex; flex-wrap: wrap; gap: 0.5rem; } .track-grid video { width: 320px; height: 240px; background: #1a1a2e; border-radius: 4px; } ``` # Dial-in and dial-out Source: https://docs.daily.co/docs/daily-js/features/dialin-dialout Use daily-js to initiate outbound SIP/PSTN calls, handle inbound callers, send DTMF tones, and track telephony session lifecycle. This guide covers the daily-js APIs for SIP and PSTN telephony. For an overview of how dial-in and dial-out work across all APIs, see the [dial-in/dial-out guide](/docs/guides/features/dial-in-dial-out). ## Prerequisites Before calling any telephony methods: * Your Daily account must have a credit card on file. * For dial-out, the room must have `enable_dialout: true` and your account must be [approved for dial-out](https://forms.gle/Q5eaHLEosuKyzC7BA). * The local participant must join with an [owner meeting token](/reference/rest-api/meeting-tokens/create-meeting-token#body-properties-is-owner). ## Dial-out ### Starting a session [`startDialOut()`](/reference/daily-js/instance-methods/start-dial-out) initiates an outbound call to a SIP URI or PSTN phone number. The called party joins the room as a regular participant. ```javascript theme={null} const { session } = await call.startDialOut({ sipUri: 'sip:conference@pbx.example.com', displayName: 'Conference Bridge', video: false, codecs: { audio: ['OPUS', 'G722'] }, }); const sessionId = session?.sessionId; ``` ```javascript theme={null} const { session } = await call.startDialOut({ phoneNumber: '+15551234567', // E.164 format displayName: 'Jane Smith', callerId: '+18005551000', extension: '4321', waitBeforeExtensionDialSec: 3, }); const sessionId = session?.sessionId; ``` The returned `sessionId` identifies this telephony session. Hold onto it — you'll need it for `stopDialOut()`, `sendDTMF()`, and transfer methods. See [`startDialOut()`](/reference/daily-js/instance-methods/start-dial-out) for the full parameter reference, including `videoSettings` for SIP video calls and codec defaults. ### Stopping a session ```javascript theme={null} await call.stopDialOut({ sessionId }); ``` ### Sending DTMF tones [`sendDTMF()`](/reference/daily-js/instance-methods/send-dtmf) sends touch-tone signals into an active session — commonly used to navigate IVR menus: ```javascript theme={null} await call.sendDTMF({ sessionId, tones: '1234#', method: 'auto', digitDurationMs: 100, }); ``` The `method` parameter controls how tones are signaled: * `'telephone-event'`: RTP telephone-event packets per RFC 4733 (most compatible) * `'sip-info'`: SIP INFO requests with an `application/dtmf-relay` body * `'auto'` (default): Daily uses `'sip-info'` when the remote SIP peer lists `INFO` in the `Allow` header of its answer, and `'telephone-event'` when it does not Asking for `'sip-info'` on a call where the far end does not allow `INFO` returns an error. When you send more than one tone in a single `sendDTMF()` call over SIP INFO, Daily waits a fixed 100ms between tones. That gap is not configurable. `digitDurationMs` sets the duration Daily signals for each digit, not the gap between digits. There is no queueing or ordering guarantee across separate `sendDTMF()` calls, so two that overlap can interleave their tones. Pass the whole sequence in one call when you can. If you have to send digits one at a time, wait for each call to finish before starting the next one and add your own pacing between them. ### Dial-out events Listen to these events to track the lifecycle of an outbound session: ```javascript theme={null} call.on('dialout-connected', ({ sessionId, destination, sipCallId, provider }) => { // Call is established and ringing at the remote end }); call.on('dialout-answered', ({ sessionId, destination, callerId, sipCallId }) => { // Remote party answered — media is now flowing }); call.on('dialout-stopped', ({ sessionId, destination }) => { // Session ended (remote hung up or stopDialOut() was called) }); call.on('dialout-error', ({ sessionId, errorMsg, details }) => { // Fatal error — session will not recover console.error(`Dial-out failed: ${errorMsg}`); }); call.on('dialout-warning', ({ sessionId, errorMsg }) => { // Non-fatal warning (e.g. codec fallback applied) }); ``` `dialout-connected` fires when the SIP/PSTN network acknowledges the call; `dialout-answered` fires when the remote party actually picks up. Both events include `provider` and `sipCallId` for correlating with server-side logs. See the [telephony events reference](/reference/daily-js/events/telephony-events) for full payload details. ## Dial-in Dial-in allows external SIP or PSTN callers to join a Daily room. Set `enable_dialin: true` on the room via the REST API or dashboard. For setup details — including PIN dial-in, pinless dial-in, and SIP interconnect — see the [dial-in/dial-out guide](/docs/guides/features/dial-in-dial-out). ### Dial-in events ```javascript theme={null} call.on('dialin-ready', ({ sipEndpoint, provider }) => { // The room's SIP endpoint is ready to accept inbound calls console.log('SIP endpoint:', sipEndpoint); }); call.on('dialin-connected', ({ sessionId, sipFrom, sipCallId, provider }) => { // An inbound caller has connected and media negotiation is underway // Note: media is not yet flowing — wait for the participant-joined event console.log(`Caller ${sipFrom} connecting — session ${sessionId}`); }); call.on('dialin-stopped', ({ sessionId, sipCallId }) => { // Inbound caller disconnected }); call.on('dialin-error', ({ sessionId, errorMsg, type }) => { console.error(`Dial-in error [${type}]: ${errorMsg}`); }); call.on('dialin-warning', ({ sessionId, errorMsg }) => { // Non-fatal warning (e.g. codec fallback) }); ``` All dial-in events include `provider` and `sipCallId` fields for correlating sessions with server-side logs. See the [telephony events reference](/reference/daily-js/events/telephony-events) for full payload details. ## Call transfers To transfer an active SIP or PSTN session to another destination, use [`sipCallTransfer()`](/reference/daily-js/instance-methods/sip-call-transfer) or [`sipRefer()`](/reference/daily-js/instance-methods/sip-refer). See the [call transfers guide](/docs/guides/features/dial-in-dial-out/transfers) for a full explanation of when to use each method, including a decision table and scenario examples. ## Example: full dial-out flow ```javascript theme={null} let activeSessionId = null; async function callPhoneNumber(phoneNumber) { try { const { session } = await call.startDialOut({ phoneNumber }); if (session) { activeSessionId = session.sessionId; updateUI({ status: 'ringing' }); } } catch (err) { console.error('Failed to start dial-out:', err); updateUI({ status: 'error' }); } } call.on('dialout-answered', ({ sessionId }) => { if (sessionId === activeSessionId) { updateUI({ status: 'connected' }); } }); call.on('dialout-stopped', ({ sessionId }) => { if (sessionId === activeSessionId) { activeSessionId = null; updateUI({ status: 'idle' }); } }); call.on('dialout-error', ({ sessionId, errorMsg }) => { if (sessionId === activeSessionId) { activeSessionId = null; updateUI({ status: 'error', message: errorMsg }); } }); async function endCall() { if (activeSessionId) { await call.stopDialOut({ sessionId: activeSessionId }); } } async function pressKey(key) { if (activeSessionId) { await call.sendDTMF({ sessionId: activeSessionId, tones: key, method: 'auto' }); } } ``` ## See also * [startDialOut()](/reference/daily-js/instance-methods/start-dial-out) * [stopDialOut()](/reference/daily-js/instance-methods/stop-dial-out) * [sendDTMF()](/reference/daily-js/instance-methods/send-dtmf) * [sipCallTransfer()](/reference/daily-js/instance-methods/sip-call-transfer) * [sipRefer()](/reference/daily-js/instance-methods/sip-refer) * [Telephony events](/reference/daily-js/events/telephony-events) * [Dial-in/dial-out overview](/docs/guides/features/dial-in-dial-out) * [Call transfers](/docs/guides/features/dial-in-dial-out/transfers) # Live Streaming Source: https://docs.daily.co/docs/daily-js/features/live-streaming Use startLiveStreaming(), updateLiveStreaming(), and stopLiveStreaming() to control live streaming from daily-js. New to Daily live streaming? Start here — covers prerequisites, supported RTMP providers and URL formats, and HLS streaming. Only **meeting owners** or **streaming admins** (participants with `canAdmin: ['streaming']`) can start, update, or stop a live stream. See [prerequisites](/docs/guides/features/live-streaming#prerequisites) in the overview for how to create the right meeting token. All streaming — RTMP and HLS — goes through `startLiveStreaming()`. For RTMP, pass an ingest URL directly. For HLS, configure [`streaming_endpoints`](/docs/guides/features/live-streaming/hls) on the room first, then pass the endpoint name. The rest of this guide applies to both. ## Starting a live stream [`startLiveStreaming()`](/reference/daily-js/instance-methods/start-live-streaming) begins streaming to one or more RTMP endpoints: ```javascript theme={null} // Stream to a single RTMP URL call.startLiveStreaming({ rtmpUrl: 'rtmps://live.example.com/app/stream-key', }); // Stream to multiple destinations at once call.startLiveStreaming({ endpoints: [ { endpoint: 'rtmps://a.ingest.example.com/live/key-1' }, { endpoint: 'rtmps://b.ingest.example.com/live/key-2' }, ], }); ``` ## Options One or more RTMP/RTMPS ingest URLs. Mutually exclusive with `endpoints`. An array of endpoint objects `{ endpoint: string }`. Use this instead of `rtmpUrl` when you need to dynamically add or remove destinations mid-stream with [`addLiveStreamingEndpoints()`](/reference/daily-js/instance-methods/add-live-streaming-endpoints) and [`removeLiveStreamingEndpoints()`](/reference/daily-js/instance-methods/remove-live-streaming-endpoints). Output width in pixels. Default: `1280`. Can only be set at start. Output height in pixels. Default: `720`. Can only be set at start. Frames per second. Default: `30`. Video bitrate in kbps. Audio bitrate in kbps. Seconds of idle time before the stream automatically stops. Default: `60`. Maximum stream duration in seconds. Background color for empty areas in the composed frame. CSS hex string. Can only be set at start. Identifier for this streaming instance. Required when running multiple simultaneous streams. See [multiple simultaneous streams](#multiple-simultaneous-streams). Participant tile layout for the composed output. See [`DailyStreamingLayoutConfig`](/reference/daily-js/types/daily-streaming-layout-config) for the full type reference, or [layout options](#layout-options) below for usage examples. ## Layout options Live streaming supports the same layout presets as recording — `default`, `single-participant`, `active-participant`, `portrait`, and `custom` — except `audio-only`, which is recording-only. See [`DailyStreamingLayoutConfig`](/reference/daily-js/types/daily-streaming-layout-config) for field-level documentation on each preset. For screenshots, see the [live streaming overview](/docs/guides/features/live-streaming#live-streaming-layout-options). ### `default` ```javascript theme={null} call.startLiveStreaming({ rtmpUrl: 'rtmps://live.example.com/app/key', layout: { preset: 'default', max_cam_streams: 6, participants: { sort: 'active' }, }, }); ``` ### `single-participant` ```javascript theme={null} call.startLiveStreaming({ rtmpUrl: 'rtmps://live.example.com/app/key', layout: { preset: 'single-participant', session_id: presenterSessionId, }, }); ``` ### `active-participant` ```javascript theme={null} call.startLiveStreaming({ rtmpUrl: 'rtmps://live.example.com/app/key', layout: { preset: 'active-participant' }, }); ``` ### `portrait` ```javascript theme={null} call.startLiveStreaming({ rtmpUrl: 'rtmps://live.example.com/app/key', layout: { preset: 'portrait', variant: 'inset', // 'vertical' (default) or 'inset' }, }); ``` ### `custom` Use Daily's VCS baseline composition to fully control the layout programmatically — modes, overlays, labels, participant ordering, and more. The [`startLiveStreaming()` reference](/reference/daily-js/types/daily-streaming-layout-config#vcs-baseline-composition) documents all available `composition_params`. ```javascript theme={null} call.startLiveStreaming({ rtmpUrl: 'rtmps://live.example.com/app/key', layout: { preset: 'custom', composition_id: 'daily:baseline', composition_params: { mode: 'dominant', 'videoSettings.showParticipantLabels': true, }, }, }); ``` ### `participants` Several presets accept a `participants` field to filter which participants' video and audio are included in the stream. See [`DailyStreamingLayoutConfig`](/reference/daily-js/types/daily-streaming-layout-config#participants) for the full field list. You must resend `participants` on every `updateLiveStreaming()` call — it is not persisted from the previous call. If omitted on update, all participants will be included. ## Updating layout mid-stream Call [`updateLiveStreaming()`](/reference/daily-js/instance-methods/update-live-streaming) to switch layouts without interrupting the stream: ```javascript theme={null} call.updateLiveStreaming({ layout: { preset: 'active-participant' }, }); ``` ## Adding and removing endpoints You can add new RTMP destinations to a running stream or remove existing ones without stopping the stream. `addLiveStreamingEndpoints()` and `removeLiveStreamingEndpoints()` only work when the stream was started with the `endpoints` parameter — not `rtmpUrl`. If you need to add or remove destinations mid-stream, use `endpoints` when calling `startLiveStreaming()`. ```javascript theme={null} // Add a backup destination mid-stream call.addLiveStreamingEndpoints({ endpoints: [{ endpoint: 'rtmps://backup.cdn.example.com/live/key-3' }], }); // Remove a destination (stream continues to other destinations) call.removeLiveStreamingEndpoints({ endpoints: [{ endpoint: 'rtmps://a.ingest.example.com/live/key-1' }], }); ``` Do not use `removeLiveStreamingEndpoints()` to remove all endpoints — use `stopLiveStreaming()` instead. Attempting to remove the last endpoint will fail and trigger a `live-streaming-warning` nonfatal error. ## Stopping a live stream [`stopLiveStreaming()`](/reference/daily-js/instance-methods/stop-live-streaming) ends all active streaming endpoints: ```javascript theme={null} call.stopLiveStreaming(); ``` ## Multiple simultaneous streams Pass a unique `instanceId` to run parallel streams — for example, a high-quality CDN stream and a lower-bitrate social stream simultaneously: ```javascript theme={null} // High-quality CDN stream call.startLiveStreaming({ instanceId: 'cdn', rtmpUrl: 'rtmps://cdn.example.com/live/key', width: 1920, height: 1080, videoBitrate: 6000, layout: { preset: 'active-participant' }, }); // Social media stream at a lower bitrate call.startLiveStreaming({ instanceId: 'social', rtmpUrl: 'rtmps://live.twitch.tv/app/stream-key', width: 1280, height: 720, videoBitrate: 2500, layout: { preset: 'active-participant' }, }); // Update only the CDN stream's layout call.updateLiveStreaming({ instanceId: 'cdn', layout: { preset: 'default', max_cam_streams: 9 }, }); // Stop each instance independently call.stopLiveStreaming({ instanceId: 'cdn' }); call.stopLiveStreaming({ instanceId: 'social' }); ``` For full details including billing and `max_streaming_instances_per_room` configuration, see the [multi-instance guide](/docs/guides/features/live-streaming/multi-instance). ## Live streaming events Full payload details are in the [live streaming events reference](/reference/daily-js/events/live-streaming-events). ### `live-streaming-started` Fired when the stream is ready and the first video frame has been delivered to the ingest server. ```javascript theme={null} call.on('live-streaming-started', ({ instanceId, layout }) => { streamingBadge.hidden = false; }); ``` ### `live-streaming-updated` Fired when the first video frame is published to the RTMP server (`state: 'connected'`), when the connection is interrupted (`state: 'interrupted'`), or when it reconnects (`state: 'connected'`). If streaming was started with `endpoints`, the payload also includes the `endpoint` object. ```javascript theme={null} call.on('live-streaming-updated', ({ endpoint, state }) => { if (state === 'interrupted') { showReconnectingBanner(); } else if (state === 'connected') { hideReconnectingBanner(); } }); ``` ### `live-streaming-stopped` ```javascript theme={null} call.on('live-streaming-stopped', () => { streamingBadge.hidden = true; }); ``` ### `live-streaming-error` ```javascript theme={null} call.on('live-streaming-error', ({ errorMsg }) => { console.error('Streaming error:', errorMsg); }); ``` ## Complete example ```javascript theme={null} const call = Daily.createCallObject(); await call.join({ url: 'https://your-domain.daily.co/room', token: 'OWNER_OR_STREAMING_ADMIN_TOKEN', }); const RTMP_URL = 'rtmps://live.example.com/app/your-stream-key'; document.getElementById('go-live').onclick = () => { call.startLiveStreaming({ rtmpUrl: RTMP_URL, width: 1280, height: 720, fps: 30, videoBitrate: 3000, layout: { preset: 'active-participant' }, }); }; document.getElementById('stop-live').onclick = () => { call.stopLiveStreaming(); }; call.on('live-streaming-started', () => { document.getElementById('go-live').disabled = true; document.getElementById('stop-live').disabled = false; }); call.on('live-streaming-updated', ({ state }) => { const status = document.getElementById('stream-status'); status.textContent = state === 'connected' ? '● Live' : '⚠ Reconnecting…'; }); call.on('live-streaming-stopped', () => { document.getElementById('go-live').disabled = false; document.getElementById('stop-live').disabled = true; }); call.on('live-streaming-error', ({ errorMsg }) => { alert(`Stream error: ${errorMsg}`); }); ``` # Recording Source: https://docs.daily.co/docs/daily-js/features/recording Use startRecording(), stopRecording(), and updateRecording() to control Daily call recordings from daily-js. New to Daily recording? Start here — covers recording types, when to use each, how to enable recording on a room or meeting token, and how to retrieve recordings. Recording must be enabled on the room or meeting token (via `enable_recording`) before `startRecording()` will work. See the overview guide above for setup. ## Starting a recording [`startRecording()`](/reference/daily-js/instance-methods/start-recording) begins a recording with optional configuration: ```javascript theme={null} // Start a cloud recording with defaults call.startRecording(); // Start with explicit options call.startRecording({ type: 'cloud', width: 1920, height: 1080, fps: 30, videoBitrate: 4000, audioBitrate: 128, backgroundColor: '#000000', }); ``` ## Recording options The recording mode. Defaults to whichever type is enabled on the room or token. See the [recording overview](/docs/guides/features/recording) for a description of each type and when to use it. Output video width in pixels. Default: `1280`. Output video height in pixels. Default: `720`. Frames per second. Default: `30`. Video bitrate in kbps. Audio bitrate in kbps. Seconds of idle time (no active streams) before the recording automatically stops. Default: `60`. Maximum recording duration in seconds. Background color for the composed output when participant tiles do not fill the frame. Accepts a CSS hex color string, e.g. `'#1a1a2e'`. Identifier for this recording instance. Required when running multiple simultaneous recordings. See [multiple simultaneous recordings](#multiple-simultaneous-recordings). Controls how participant tiles are arranged in the composed output. See [`DailyStreamingLayoutConfig`](/reference/daily-js/types/daily-streaming-layout-config) for the full type reference, or [layout presets](#layout-presets) below for usage examples. ## Layout presets The `layout` option controls the visual composition of cloud recordings. See [`DailyStreamingLayoutConfig`](/reference/daily-js/types/daily-streaming-layout-config) for field-level documentation on each preset. For screenshots, see the [recording overview](/docs/guides/features/recording#customize-cloud-recording-layouts). ### `default` ```javascript theme={null} call.startRecording({ layout: { preset: 'default', max_cam_streams: 9, // maximum cameras shown participants: { video: ['session-id-1'], // specific participant always shown sort: 'active', // sort by speaking activity }, }, }); ``` ### `single-participant` ```javascript theme={null} call.startRecording({ layout: { preset: 'single-participant', session_id: 'abc123', // required: which participant to show }, }); ``` ### `active-participant` ```javascript theme={null} call.startRecording({ layout: { preset: 'active-participant' }, }); ``` ### `portrait` ```javascript theme={null} call.startRecording({ layout: { preset: 'portrait', variant: 'inset', // 'vertical' (default) or 'inset' max_cam_streams: 4, }, }); ``` ### `audio-only` ```javascript theme={null} call.startRecording({ type: 'cloud-audio-only', layout: { preset: 'audio-only' }, }); ``` ### `custom` Use Daily's VCS baseline composition to fully control the layout programmatically — modes, overlays, labels, participant ordering, and more. The [`startRecording()` reference](/reference/daily-js/types/daily-streaming-layout-config#baseline-composition-properties) documents all available `composition_params`. ```javascript theme={null} call.startRecording({ layout: { preset: 'custom', composition_id: 'daily:baseline', composition_params: { mode: 'dominant', 'videoSettings.showParticipantLabels': true, }, session_assets: { 'images/logo.png': 'https://example.com/logo.png', }, }, }); ``` ### `participants` Several presets accept a `participants` field to filter which participants' video and audio are included in the recording. See [`DailyStreamingLayoutConfig`](/reference/daily-js/types/daily-streaming-layout-config#participants) for the full field list. You must resend `participants` on every `updateRecording()` call — it is not persisted from the previous call. If omitted on update, all participants will be included. ## Updating layout during recording Call [`updateRecording()`](/reference/daily-js/instance-methods/update-recording) to switch layouts while a recording is in progress, without stopping and restarting: ```javascript theme={null} // Start in default grid layout call.startRecording({ layout: { preset: 'default' }, }); // Later, switch to follow the active speaker call.updateRecording({ layout: { preset: 'active-participant' }, }); ``` ## Stopping a recording [`stopRecording()`](/reference/daily-js/instance-methods/stop-recording) ends the active recording: ```javascript theme={null} call.stopRecording(); ``` ## Multiple simultaneous recordings Pass a unique `instanceId` to run more than one recording at the same time — for example, two cloud recordings with different layouts for different audiences: ```javascript theme={null} // Portrait layout for mobile call.startRecording({ instanceId: 'portrait', type: 'cloud', layout: { preset: 'portrait', variant: 'vertical' }, }); // Landscape layout for desktop call.startRecording({ instanceId: 'landscape', type: 'cloud', layout: { preset: 'active-participant' }, }); // Update and stop each independently call.updateRecording({ instanceId: 'landscape', layout: { preset: 'default' } }); call.stopRecording({ instanceId: 'portrait' }); call.stopRecording({ instanceId: 'landscape' }); ``` Multiple instances work best when each runs the same recording type. Mixing types (e.g., `cloud` + `raw-tracks` simultaneously) is possible but not a recommended pattern — if you need both, contact [Daily support](https://www.daily.co/contact/support) to discuss your use case. For full details on instance limits and billing, see the [multi-instance guide](/docs/guides/features/live-streaming/multi-instance). ## Recording events Full payload details for all recording events are in the [recording events reference](/reference/daily-js/events/recording-events). ### `recording-started` ```javascript theme={null} call.on('recording-started', (event) => { console.log('Recording started:', event.recordingId); console.log('Type:', event.type); // 'cloud' | 'raw-tracks' | ... console.log('Started by:', event.startedBy); // session ID console.log('Layout:', event.layout); }); ``` ### `recording-stopped` ```javascript theme={null} call.on('recording-stopped', () => { console.log('Recording stopped.'); }); ``` ### `recording-error` ```javascript theme={null} call.on('recording-error', ({ errorMsg }) => { console.error('Recording error:', errorMsg); }); ``` ## Retrying failed recording starts A recording start can fail for transient reasons, like a timeout while the recording infrastructure spins up. Sometimes the start command doesn't reach the recording server at all due to client network conditions, so `startRecording()` never fires a `recording-error` event: the recording just never starts. Because of this, don't rely on the error event alone. After you call `startRecording()`, wait for the `recording-started` event. If it doesn't arrive within a short timeout, call `startRecording()` again. Use exponential backoff between attempts (wait a bit, retry, then double the wait) and cap the number of attempts so you don't retry forever. ```javascript theme={null} const MAX_ATTEMPTS = 4; const START_TIMEOUT_MS = 5000; // how long to wait for 'recording-started' let attempt = 0; let startTimer = null; function startRecordingWithRetry() { attempt += 1; call.startRecording({ type: 'cloud' }); // If 'recording-started' doesn't arrive, the command may not have reached // the recording server, so try again. startTimer = setTimeout(() => retryOrGiveUp(), START_TIMEOUT_MS); } function retryOrGiveUp(errorMsg) { if (attempt >= MAX_ATTEMPTS) { console.error( 'Recording failed after retries:', errorMsg || 'no recording-started event' ); return; } const delayMs = 1000 * 2 ** (attempt - 1); // 1s, 2s, 4s setTimeout(startRecordingWithRetry, delayMs); } call.on('recording-started', () => { clearTimeout(startTimer); // it started, so stop waiting attempt = 0; // reset for the next recording }); call.on('recording-error', ({ errorMsg }) => { clearTimeout(startTimer); retryOrGiveUp(errorMsg); }); startRecordingWithRetry(); ``` If you start recordings from your server instead, apply the same pattern to the [`POST /rooms/:name/recordings/start`](/reference/rest-api/rooms/recordings/start) REST endpoint: ```python theme={null} import time import requests MAX_ATTEMPTS = 4 def start_recording(room_name): url = f"https://api.daily.co/v1/rooms/{room_name}/recordings/start" headers = {"Authorization": "Bearer DAILY_API_KEY"} delay = 1 # seconds for attempt in range(MAX_ATTEMPTS): response = requests.post(url, headers=headers, timeout=30) if response.ok: return response.json() # Sleep only between attempts, not after the last one. if attempt < MAX_ATTEMPTS - 1: time.sleep(delay) # 1s, 2s, 4s delay *= 2 raise RuntimeError("recording did not start after retries") ``` Only retry transient errors. If the error says recording is not enabled on the room or token, fix the configuration instead of retrying. ## Complete cloud recording example ```javascript theme={null} const call = Daily.createCallObject(); await call.join({ url: 'https://your-domain.daily.co/room' }); // Start a 1080p cloud recording with active-speaker layout call.startRecording({ type: 'cloud', width: 1920, height: 1080, fps: 30, layout: { preset: 'active-participant' }, }); call.on('recording-started', ({ recordingId }) => { console.log('Recording ID:', recordingId); recordingBadge.hidden = false; }); call.on('recording-stopped', () => { recordingBadge.hidden = true; }); call.on('recording-error', ({ errorMsg }) => { console.error('Recording failed:', errorMsg); }); // Switch layout mid-recording when a screen share starts call.on('local-screen-share-started', () => { call.updateRecording({ layout: { preset: 'single-participant', session_id: call.participants().local.session_id, }, }); }); call.on('local-screen-share-stopped', () => { call.updateRecording({ layout: { preset: 'active-participant' }, }); }); document.getElementById('stop-recording').onclick = () => call.stopRecording(); ``` # Screen Sharing Source: https://docs.daily.co/docs/daily-js/features/screen-sharing Start, stop, and configure screen sharing in a Daily call, including sharing a pre-captured MediaStream and fine-tuning video quality presets. Daily lets participants share their screen, an application window, or a browser tab with other call participants. `startScreenShare()` accepts one of two argument shapes — or no arguments at all: * **Let Daily handle capture** (`DailyStartScreenShare`) — Daily calls `getDisplayMedia()` for you, optionally using constraints and quality settings you provide. **This is the recommended option for most use cases.** * **Bring your own stream** (`DailyStartScreenShareFromStream`) — you call `getDisplayMedia()` yourself (or source a stream from Electron's `desktopCapturer`, a canvas, etc.) and hand the resulting `MediaStream` to Daily. To stop sharing: `call.stopScreenShare()`. `startScreenShare()` is non-blocking — the browser's native picker opens asynchronously. Listen for [the events below](#events) to know when sharing actually begins or is canceled. ## Let Daily handle capture Recommended Call `startScreenShare()` with no arguments to use Daily's defaults, or pass a `DailyStartScreenShare` object to configure the capture: Options forwarded directly to `navigator.mediaDevices.getDisplayMedia()`. Use these to request system audio, control surface switching, and more. Controls the video encoding quality for the shared screen. Accepts either a preset string or a custom `DailyVideoSendSettings` object. [See below](#send-settings) for details. ```javascript theme={null} // No arguments — Daily uses browser defaults call.startScreenShare(); // With options call.startScreenShare({ displayMediaOptions: { audio: true, video: { frameRate: { ideal: 30, max: 60 } }, selfBrowserSurface: 'exclude', systemAudio: 'include', }, screenVideoSendSettings: 'detail-optimized', }); ``` ### `DailyDisplayMediaStreamOptions` Request system audio in the screen share. Default is browser-dependent. Video constraints for the captured surface. Whether to include the current browser tab as a selectable source. Whether to show an in-call surface-switching control (Chrome 107+). Whether to offer system audio capture in the picker (Windows/Chrome only). ## Bring your own stream If you already have a `MediaStream` — captured with your own `getDisplayMedia()` call, sourced from an Electron `desktopCapturer`, or otherwise constructed — pass it via `DailyStartScreenShareFromStream`. Daily will not call `getDisplayMedia()` in this case. A `MediaStream` whose first video track (and optionally first audio track) will be used as the screen share source. Optional quality preset or custom settings for the provided video track. [See below](#send-settings). ```javascript theme={null} const stream = await navigator.mediaDevices.getDisplayMedia({ video: true }); call.startScreenShare({ mediaStream: stream }); ``` ### Screenshare streams vs. custom tracks If you have a `MediaStream` you want to send, you have two options: pass it to `startScreenShare({ mediaStream: stream })` or send it as a custom track via `startCustomTrack()`. These are not interchangeable — Daily treats screen share video tracks differently from other video tracks, and routing a stream through the wrong path will cause incorrect behavior. In Chromium-based browsers, a screen share video track becomes `muted` at the browser level whenever its content is not changing — for example, when a shared document is idle. This is normal browser behavior and does not mean the user stopped sharing or there are issues with the track. To avoid incorrectly firing `track-stopped` or transitioning to an `interrupted` state, Daily suppresses the `muted` signal on screen share video tracks. This suppression only applies to tracks sent via `startScreenShare()`. **Use `startScreenShare({ mediaStream: stream })` when your stream is screen content** — a `getDisplayMedia()` capture, an Electron `desktopCapturer` stream, or any source that may go idle between updates. The muted-suppression handling will prevent spurious interruptions. **Use `startCustomTrack()` when your stream is not screen content** — a canvas animation, a virtual camera, or any source that should respond normally to the `muted` signal. Routing this through `startScreenShare()` would mask legitimate track state changes. ## Send Settings Both methods of screensharing allow you to define the quality of the sending stream by either providing a preset or passing a custom `DailyVideoSendSettings` object. `DailyScreenVideoSendSettingsPreset` provides four named presets optimized for different content types: | Preset | Best for | | ------------------------------ | -------------------------------------------- | | `'default-screen-video'` | General purpose (Daily's default) | | `'detail-optimized'` | Text-heavy content — slides, IDEs, documents | | `'motion-optimized'` | Video playback, animations, fast UI | | `'motion-and-detail-balanced'` | Mixed content such as interactive demos | For full control, pass a `DailyVideoSendSettings` object: ```javascript theme={null} call.startScreenShare({ screenVideoSendSettings: { maxQuality: 'high', encodings: { low: { maxBitrate: 500_000, maxFramerate: 5 }, high: { maxBitrate: 4_000_000, maxFramerate: 30 }, }, }, }); ``` ## Updating screen share state mid-share Use `updateScreenShare()` to enable or disable the screen video or audio tracks after sharing has started — without stopping and restarting the entire screen share: ```javascript theme={null} // Mute screen audio while keeping screen video active call.updateScreenShare({ screenVideo: { enabled: true }, screenAudio: { enabled: false }, }); // Re-enable screen audio call.updateScreenShare({ screenVideo: { enabled: true }, screenAudio: { enabled: true }, }); ``` ## Reading local screen share state ```javascript theme={null} const isVideoActive = call.localScreenVideo(); // true if screen video is active const isAudioActive = call.localScreenAudio(); // true if screen audio is active ``` ## Events ### `local-screen-share-started` Fired when the local participant's screen share begins. Useful for updating UI to show a "stop sharing" button. ```javascript theme={null} call.on('local-screen-share-started', () => { stopSharingBtn.hidden = false; startSharingBtn.hidden = true; }); ``` ### `local-screen-share-stopped` Fired when screen sharing ends — either because your code called `stopScreenShare()` or because the user clicked the browser's built-in "Stop sharing" button. ```javascript theme={null} call.on('local-screen-share-stopped', () => { stopSharingBtn.hidden = true; startSharingBtn.hidden = false; }); ``` ### `local-screen-share-canceled` Fired when the user opens the browser picker but then dismisses it without selecting a source. ```javascript theme={null} call.on('local-screen-share-canceled', () => { console.log('User dismissed the screen share picker.'); }); ``` ### Detecting remote screen shares Remote screen share tracks arrive via the standard `track-started` / `track-stopped` events with `type === 'screenVideo'` or `type === 'screenAudio'`: ```javascript theme={null} call.on('track-started', ({ participant, track, type }) => { if (type === 'screenVideo' && !participant.local) { const video = document.createElement('video'); video.srcObject = new MediaStream([track]); video.autoplay = true; document.getElementById('screen-container').append(video); } }); call.on('track-stopped', ({ participant, type }) => { if (type === 'screenVideo' && !participant?.local) { document.getElementById('screen-container').innerHTML = ''; } }); ``` ## Complete example ```javascript theme={null} const call = Daily.createCallObject(); await call.join({ url: 'https://your-domain.daily.co/room' }); const startBtn = document.getElementById('start-share'); const stopBtn = document.getElementById('stop-share'); startBtn.onclick = () => { call.startScreenShare({ displayMediaOptions: { audio: true, selfBrowserSurface: 'exclude' }, screenVideoSendSettings: 'detail-optimized', }); }; stopBtn.onclick = () => call.stopScreenShare(); call.on('local-screen-share-started', () => { startBtn.disabled = true; stopBtn.disabled = false; }); call.on('local-screen-share-stopped', () => { startBtn.disabled = false; stopBtn.disabled = true; }); call.on('local-screen-share-canceled', () => { console.log('Picker was dismissed.'); }); ``` # Real-Time Transcription Source: https://docs.daily.co/docs/daily-js/features/transcription Transcribe speech in Daily calls in real time using Deepgram, with per-participant filtering, profanity control, and raw response access. Paid plans only New to Daily transcription? Start here — covers real-time vs. post-call transcription, pricing, permissions, and transcript storage. Daily's transcription feature streams speech-to-text output to all participants in a call. It is powered by [Deepgram](https://deepgram.com) and supports a wide range of languages, models, and configuration options. ## Transcription permissions Only meeting owners and participants with `canAdmin: 'transcription'` can start or stop transcription. See [Permissions](/docs/guides/features/transcription#permissions) in the overview for how to grant this. ## Transcript storage By default, transcripts are ephemeral — participants receive `transcription-message` events but nothing is persisted. To save transcripts as WebVTT files, enable `enable_transcription_storage` at the room or domain level. See [Storage](/docs/guides/features/transcription#storage) in the overview for setup details, including custom S3 configuration for HIPAA use cases. ## Starting transcription [`startTranscription()`](/reference/daily-js/instance-methods/start-transcription) starts a transcription session. All options are optional — call it with no arguments to use Deepgram defaults. ```javascript theme={null} // Start with Deepgram defaults call.startTranscription(); // Start with explicit language and model call.startTranscription({ language: 'en', model: 'nova-2', punctuate: true, profanity_filter: false, }); ``` Check [Deepgram's language and model overview](https://developers.deepgram.com/docs/models-languages-overview) for the `model`/`language` combination that works best for your use case. See [`startTranscription()`](/reference/daily-js/instance-methods/start-transcription) for the full list of options. ## Handling transcription messages Listen to [`transcription-message`](/reference/daily-js/events/transcription-events#transcription-message) to receive transcript segments in real time: ```javascript theme={null} call.on('transcription-message', (event) => { console.log('[transcript]', event.text); console.log(' participant:', event.participantId); console.log(' timestamp:', event.timestamp); console.log(' track type:', event.trackType); // 'cam-audio' | 'screen-audio' | ... console.log(' instance:', event.instanceId); if (event.rawResponse) { // Full Deepgram response — available when includeRawResponse: true console.log(' raw:', event.rawResponse); } }); ``` ## Updating transcription Use [`updateTranscription()`](/reference/daily-js/instance-methods/update-transcription) to change which participants are being transcribed while transcription is running: ```javascript theme={null} // Transcribe only the presenter call.updateTranscription({ participants: [presenterSessionId], }); // Restore transcription for everyone call.updateTranscription({ participants: null, }); ``` Note that participants who join after transcription starts are not automatically added — use `updateTranscription()` to add them. ## Stopping transcription [`stopTranscription()`](/reference/daily-js/instance-methods/stop-transcription) ends a transcription session. If storage is enabled, the final transcript file is written at this point. ```javascript theme={null} // Stop the default transcription instance call.stopTranscription(); // Stop a named instance call.stopTranscription({ instanceId: 'primary' }); ``` ## Events Full payload details are in the [transcription events reference](/reference/daily-js/events/transcription-events). ```javascript theme={null} call.on('transcription-started', ({ instanceId, language, model, startedBy }) => { console.log(`Transcription started (${model}/${language})`); captionsIndicator.hidden = false; }); call.on('transcription-stopped', ({ instanceId, updatedBy }) => { captionsIndicator.hidden = true; }); call.on('transcription-error', ({ instanceId, errorMsg }) => { console.error('Transcription error:', errorMsg); }); ``` ## Post-call transcription To transcribe a recording after a call ends, use the [Batch Processor API](/reference/rest-api/batch-processor/index). See [Post-call transcription](/docs/guides/features/transcription#post-call-transcription) in the overview for details. ## Complete example ```javascript theme={null} const call = Daily.createCallObject(); call.on('joined-meeting', () => { call.startTranscription({ language: 'en', model: 'nova-2', punctuate: true, }); }); await call.join({ url: 'https://your-domain.daily.co/room' }); ``` ```javascript theme={null} const captionsDiv = document.getElementById('captions'); call.on('transcription-message', ({ participantId, text }) => { const name = call.participants()[participantId]?.user_name || participantId.slice(0, 8); const line = document.createElement('p'); line.textContent = `${name}: ${text}`; captionsDiv.appendChild(line); captionsDiv.scrollTop = captionsDiv.scrollHeight; }); ``` ```javascript theme={null} call.on('transcription-started', ({ language, model }) => { document.getElementById('captions-status').textContent = `Live captions on (${model}/${language})`; }); call.on('transcription-stopped', () => { document.getElementById('captions-status').textContent = ''; }); call.on('transcription-error', ({ errorMsg }) => { console.error('Transcription error:', errorMsg); }); ``` ```javascript theme={null} document.getElementById('leave').onclick = async () => { call.stopTranscription(); await call.leave(); }; ``` # Audio and Video Controls Source: https://docs.daily.co/docs/daily-js/guides/audio-video Manage local and remote audio/video tracks, switch input and output devices, and monitor audio levels in a Daily call. Daily gives you fine-grained control over microphone and camera state, device selection, and audio level monitoring — both before and during a call. ## Muting and unmuting Check the current mute state with `localAudio()` and `localVideo()`, then toggle with the matching setters. ```javascript theme={null} const call = Daily.createCallObject(); // Read current state const isMicOn = call.localAudio(); // true = mic is sending const isCamOn = call.localVideo(); // true = camera is sending // Mute the microphone call.setLocalAudio(false); // Unmute the microphone call.setLocalAudio(true); // Turn the camera off call.setLocalVideo(false); ``` `setLocalAudio` accepts an optional second argument: When `true`, the underlying `MediaStreamTrack` is discarded (stopped) on mute rather than kept alive. Use this when you want the microphone indicator to turn off on mute. If not set, defaults to the `keepCamIndicatorLightOn` value from `dailyConfig`, or `false` if that is not set. ```javascript theme={null} // Stop the camera track entirely when muting call.setLocalAudio(false, { forceDiscardTrack: true }); ``` Setting `forceDiscardTrack: true` for audio can cause the beginning of a participant's audio to be clipped when they unmute. Because the track is fully discarded, remote participants must complete the entire track subscription process again when the sender unmutes — rather than simply resuming a paused track. ## Starting camera before joining Call `startCamera()` to acquire device permissions and start the camera/mic pipeline before the user joins a room. This lets you display a preview and confirm device selection on a pre-join screen. ```javascript theme={null} // Request permissions and start preview const devices = await call.startCamera(); console.log('Camera:', devices.camera); console.log('Mic:', devices.mic); // Later, join with camera already running await call.join({ url: 'https://your-domain.daily.co/room-name' }); ``` You can also pass `DailyCallOptions` to `startCamera()` to pre-configure devices or input settings: ```javascript theme={null} await call.startCamera({ inputSettings: { video: { settings: { facingMode: { exact: 'environment' } } }, audio: { processor: { type: 'noise-cancellation' }}, }, }); ``` ## Joining with audio or video off Set `startAudioOff` and/or `startVideoOff` to `false` in your call configuration before joining or starting the camera to have the participant join with their mic and/or camera off and override any room or token settings. ```javascript theme={null} await call.join({ url: 'https://your-domain.daily.co/room-name', startAudioOff: false, startVideoOff: false, }); ``` ## Enumerating available devices Use `enumerateDevices()` to get all available media devices: ```javascript theme={null} const { devices } = await call.enumerateDevices(); const cameras = devices.filter(d => d.kind === 'videoinput'); const mics = devices.filter(d => d.kind === 'audioinput'); const speakers = devices.filter(d => d.kind === 'audiooutput'); cameras.forEach(cam => { console.log(cam.label, cam.deviceId, cam.facing); // facing: 'user'|'environment' }); ``` Each entry is a `DailyMediaDeviceInfo` — a superset of [`MediaDeviceInfo`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo) with an optional `facing` field (`'user'` | `'environment'`) for cameras on mobile devices. ## Reading the active devices `getInputDevices()` returns the devices currently in use (or expected to be used): ```javascript theme={null} const { camera, mic, speaker } = await call.getInputDevices(); // Each is {} if no device is active, or a MediaDeviceInfo object console.log('Active camera:', camera); ``` The return type is `DailyDeviceInfos`: ```typescript theme={null} interface DailyDeviceInfos { camera: {} | DailyMediaDeviceInfo; mic: {} | MediaDeviceInfo; speaker: {} | MediaDeviceInfo; } ``` ## Switching input devices ### By device ID Use `setInputDevicesAsync()` to switch the active camera or microphone by `deviceId`: The `deviceId` of the microphone to use. Setting to `false` will turn off the microphone and disallow the user to turn it back on until a valid `deviceId` or `true` is passed. The `deviceId` of the camera to use. Setting to `false` will turn off the camera and disallow the user to turn it back on until a valid `deviceId` or `true` is passed. ```javascript theme={null} document.getElementById('camera-select').addEventListener('change', async (e) => { await call.setInputDevicesAsync({ videoDeviceId: e.target.value }); }); document.getElementById('mic-select').addEventListener('change', async (e) => { await call.setInputDevicesAsync({ audioDeviceId: e.target.value }); }); ``` ### With constraints or a custom track For anything beyond a simple device ID swap — such as applying media constraints or supplying a custom `MediaStreamTrack` — use [`updateInputSettings()`](/reference/daily-js/instance-methods/update-input-settings) with `audio.settings` or `video.settings` instead: ```javascript theme={null} // Apply custom constraints to the camera await call.updateInputSettings({ video: { settings: { deviceId: { exact: 'your-camera-device-id' }, aspectRatio: 1.0 }, }, }); // Use a pre-captured MediaStreamTrack as the camera source const [track] = canvasStream.getVideoTracks(); await call.updateInputSettings({ video: { settings: { customTrack: track } }, }); ``` ## Switching the output device Use `setOutputDeviceAsync()` to change the speaker or audio output device: ```javascript theme={null} const speakers = devices.filter(d => d.kind === 'audiooutput'); const headphones = speakers.find(d => d.label.includes('Headphones')); if (headphones) { await call.setOutputDeviceAsync({ outputDeviceId: headphones.deviceId }); } ``` `setOutputDeviceAsync()` is only supported in browsers that implement [`HTMLMediaElement.setSinkId()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId). Most non-Android browsers now support this. Safari only recently added support in 18.4 and may have some quirks. Safari also requires a user gesture to switch audio output devices, so be sure to call this from a click handler or similar. ## Cycling camera and mic For mobile or simple use cases, `cycleCamera()` and `cycleMic()` rotate through available devices without you having to enumerate them manually: ```javascript theme={null} // Flip between front and rear camera const { device } = await call.cycleCamera(); console.log('Now using:', device?.label); // Prefer switching to a different facing mode (front ↔ rear) const { device: nextCam } = await call.cycleCamera({ preferDifferentFacingMode: true, }); // Cycle to the next microphone const { device: nextMic } = await call.cycleMic(); ``` ## Automatic device switching Daily automatically handles two common device disruption scenarios without any code on your part: * **System default changes** — if the active device is set to the system default (Chrome uses `deviceId: "default"`; other browsers use the first device in the list) and the user changes their system default, Daily switches to the new default. * **Active device disconnected** — if the currently selected device is unplugged or becomes unavailable, Daily switches to the next available device. To disable this behavior and manage device changes yourself, set `noAutoDefaultDeviceChange: true` in `dailyConfig`: ```javascript theme={null} const call = Daily.createCallObject({ dailyConfig: { noAutoDefaultDeviceChange: true }, }); ``` When disabled, you can listen for [`available-devices-updated`](/reference/daily-js/events/settings-events#available-devices-updated) to respond to device changes manually. ## Device change events Listen for `available-devices-updated` when the system's device list changes (e.g., a USB camera is plugged in), and `selected-devices-updated` when the active devices change: ```javascript theme={null} call.on('available-devices-updated', ({ availableDevices }) => { // availableDevices: MediaDeviceInfo[] console.log('Device list changed:', availableDevices); refreshDeviceSelectors(availableDevices); }); call.on('selected-devices-updated', ({ devices }) => { // devices: DailyDeviceInfos console.log('Active camera:', devices.camera); console.log('Active mic:', devices.mic); console.log('Active speaker:', devices.speaker); }); ``` ## Monitoring local audio level Daily can poll the local participant's microphone volume and emit it as an event, which is useful for building a mic-level indicator. ```javascript theme={null} // Poll every 100 ms (default) await call.startLocalAudioLevelObserver(); // Or specify a custom interval in milliseconds await call.startLocalAudioLevelObserver(200); ``` ```javascript theme={null} call.on('local-audio-level', ({ audioLevel }) => { // audioLevel is a number from 0.0 to 1.0 micMeter.style.width = `${audioLevel * 100}%`; }); ``` ```javascript theme={null} // Any time after the observer is started: const level = call.getLocalAudioLevel(); // 0.0–1.0 ``` ```javascript theme={null} call.stopLocalAudioLevelObserver(); ``` ```javascript theme={null} // Check whether the observer is currently running const isRunning = call.isLocalAudioLevelObserverRunning(); ``` ## Monitoring remote participants' audio levels To monitor the audio levels of all remote participants simultaneously (useful for displaying speaking indicators): ```javascript theme={null} // Start polling remote audio levels every 150 ms await call.startRemoteParticipantsAudioLevelObserver(150); call.on('remote-participants-audio-level', ({ participantsAudioLevel }) => { // participantsAudioLevel: { [sessionId: string]: number } for (const [sessionId, level] of Object.entries(participantsAudioLevel)) { const tile = document.getElementById(`tile-${sessionId}`); if (tile) { tile.classList.toggle('speaking', level > 0.05); } } }); // Read levels imperatively const levels = call.getRemoteParticipantsAudioLevel(); // Stop when done call.stopRemoteParticipantsAudioLevelObserver(); ``` ```javascript theme={null} const isRemoteRunning = call.isRemoteParticipantsAudioLevelObserverRunning(); ``` ## Audio and video processing [`updateInputSettings()`](/reference/daily-js/instance-methods/update-input-settings) applies real-time processing to your audio and video tracks. You can pass `inputSettings` directly to `join()` or `startCamera()` to enable processing before the first frame is captured, or call it at any time during a call. ### Noise cancellation ```javascript theme={null} await call.updateInputSettings({ audio: { processor: { type: 'noise-cancellation' } }, }); // Disable await call.updateInputSettings({ audio: { processor: { type: 'none' } }, }); ``` ### Background blur `strength` ranges from `0` (no blur) to `1` (maximum blur): ```javascript theme={null} await call.updateInputSettings({ video: { processor: { type: 'background-blur', config: { strength: 0.75 } }, }, }); ``` ### Background image replacement ```javascript theme={null} await call.updateInputSettings({ video: { processor: { type: 'background-image', config: { source: 'https://example.com/office.jpg' }, }, }, }); ``` ### Face detection Enables face detection processing. Detected face counts are reported via the `face-counts-updated` event: ```javascript theme={null} await call.updateInputSettings({ video: { processor: { type: 'face-detection' } }, }); ``` ### Enabling at join time ```javascript theme={null} await call.join({ url: roomUrl, inputSettings: { audio: { processor: { type: 'noise-cancellation' } }, video: { processor: { type: 'background-blur', config: { strength: 0.5 } } }, }, }); ``` Listen for [`input-settings-updated`](/reference/daily-js/events/settings-events#input-settings-updated) to track changes: ```javascript theme={null} call.on('input-settings-updated', ({ inputSettings }) => { console.log('Input settings changed:', inputSettings); }); ``` Video processors require browser support for `OffscreenCanvas` and WebGL. Check `Daily.supportedBrowser().supportsVideoProcessing` before enabling them. ## Complete device-switcher example ```javascript theme={null} async function buildDeviceSwitcher(call) { const { devices } = await call.enumerateDevices(); const camSelect = document.getElementById('camera-select'); const micSelect = document.getElementById('mic-select'); const spkSelect = document.getElementById('speaker-select'); // Populate selects for (const d of devices) { const opt = new Option(d.label || d.deviceId, d.deviceId); if (d.kind === 'videoinput') camSelect.append(opt.cloneNode(true)); if (d.kind === 'audioinput') micSelect.append(opt.cloneNode(true)); if (d.kind === 'audiooutput') spkSelect.append(opt.cloneNode(true)); } camSelect.onchange = () => call.setInputDevicesAsync({ videoDeviceId: camSelect.value }); micSelect.onchange = () => call.setInputDevicesAsync({ audioDeviceId: micSelect.value }); spkSelect.onchange = () => call.setOutputDeviceAsync({ outputDeviceId: spkSelect.value }); // Keep in sync with hot-plug events call.on('available-devices-updated', ({ availableDevices }) => { // Re-populate selects with the updated device list [camSelect, micSelect, spkSelect].forEach(s => (s.innerHTML = '')); for (const d of availableDevices) { const opt = new Option(d.label || d.deviceId, d.deviceId); if (d.kind === 'videoinput') camSelect.append(opt.cloneNode(true)); if (d.kind === 'audioinput') micSelect.append(opt.cloneNode(true)); if (d.kind === 'audiooutput') spkSelect.append(opt.cloneNode(true)); } }); } ``` # Custom messages and shared data Source: https://docs.daily.co/docs/daily-js/guides/custom-messages Send real-time messages between participants, attach state to participants, and maintain room-wide shared data using daily-js. Daily provides three mechanisms for sharing custom data between participants. They have different persistence, scope, and delivery characteristics — picking the right one for a given use case matters. | | `sendAppMessage()` | `setUserData()` | `setMeetingSessionData()` | | ------------------------- | ------------------- | --------------------------- | ------------------------------------- | | Scope | Per message | Per participant | Entire room | | Persists for late joiners | No | Yes | Yes | | Size limit | 4KB | 4KB | 100KB | | Rate limiting | None | Eventual consistency | \~1 update/sec | | Custom mode only | No | No | Yes | | Received via | `app-message` event | `participant-updated` event | `meeting-session-state-updated` event | ## sendAppMessage() — ephemeral real-time messages [`sendAppMessage()`](/reference/daily-js/instance-methods/send-app-message) delivers a JSON payload to one or more participants currently in the call. Messages are ephemeral — they are not stored and participants who join after a message is sent will never see it. ```javascript theme={null} // Broadcast to everyone in the room call.sendAppMessage({ type: 'reaction', emoji: '🎉' }); // Send to a specific participant call.sendAppMessage({ type: 'dm', text: 'Can you hear me?' }, participantSessionId); // Send to a subset of participants call.sendAppMessage({ type: 'alert', text: 'You are now on stage' }, [id1, id2]); ``` Use `session_id` to address recipients, not `user_id`. Broadcast messages (`'*'`) are not delivered to the sender. Receive messages by listening for `app-message`: ```javascript theme={null} call.on('app-message', ({ data, fromId }) => { if (data.type === 'reaction') { showReaction(fromId, data.emoji); } }); ``` `sendAppMessage` is also available server-side via the [REST API](/reference/rest-api/rooms/session/send-app-message), which is useful for injecting messages from your backend — for example, sending a system notification when a server-side event occurs. **Good for:** chat, emoji reactions, hand raise notifications, one-time alerts, any event that only matters to participants currently in the room. **Not suitable for:** state that late joiners need to see, or anything requiring persistence. ## setUserData() — per-participant state [`setUserData()`](/reference/daily-js/instance-methods/set-user-data) attaches arbitrary data to the local participant's entry in the participants map. It is automatically synced to all other participants and is visible to anyone who joins later — they receive it as part of the participants snapshot when they join. ```javascript theme={null} // Set on join await call.join({ url: roomUrl, userData: { role: 'moderator', handRaised: false }, }); // Update during the call await call.setUserData({ role: 'moderator', handRaised: true }); ``` Any participant can read any other participant's `userData`: ```javascript theme={null} // Read your own const { userData } = call.participants().local; // Read a remote participant's const { userData } = call.participants()[sessionId]; ``` Changes trigger `participant-updated` for all participants: ```javascript theme={null} call.on('participant-updated', ({ participant }) => { if (participant.userData?.handRaised) { showHandRaisedIndicator(participant.session_id); } }); ``` The local copy of `userData` is updated immediately, but propagation to other participants is throttled. All participants are guaranteed to converge on the same final value. **Good for:** per-person state that all participants need to see, including new joiners — raised hand status, custom role, display name supplements, avatar URL, speaking queue position. **Not suitable for:** high-frequency updates (e.g. cursor position), or state that belongs to the room rather than a specific person. ## setMeetingSessionData() — room-wide shared state [`setMeetingSessionData()`](/reference/daily-js/instance-methods/set-meeting-session-data) writes to a single shared data object scoped to the meeting session. All participants receive updates in near real-time, and the data persists as participants join and leave — new joiners receive the current state immediately. `setMeetingSessionData()` is available in custom call object mode only, not in Daily Prebuilt. ```javascript theme={null} // Replace the entire session data object call.setMeetingSessionData({ activeScene: 'lobby', pollActive: false }); // Shallow-merge: only update specific top-level keys call.setMeetingSessionData({ pollActive: true }, 'shallow-merge'); ``` Read the current state synchronously at any time: ```javascript theme={null} const { data } = call.meetingSessionState(); console.log('Current scene:', data.activeScene); ``` Or reactively via event: ```javascript theme={null} call.on('meeting-session-state-updated', ({ meetingSessionState }) => { const { activeScene, pollActive } = meetingSessionState.data; updateRoomUI(activeScene, pollActive); }); ``` Updates are batched and synced at most once per second. If multiple participants write concurrently, precise ordering is not guaranteed. This makes `setMeetingSessionData()` unsuitable for high-frequency updates where sub-second ordering matters — use `sendAppMessage()` for those instead. **Good for:** room-level state shared by all participants — the current scene or layout, whether a poll is active, feature flags, a shared queue, any configuration that needs to survive participant churn. **Not suitable for:** per-participant state (use `setUserData()`), high-frequency real-time events (use `sendAppMessage()`), or data that needs to outlive the session (use your own backend). ## Choosing the right mechanism A few common scenarios: **Chat messages** → `sendAppMessage()`. Messages are ephemeral by nature; you likely want your own backend for history anyway. **Emoji reactions** → `sendAppMessage()`. One-shot, ephemeral, addressed to everyone. **Raised hand indicator** → `setUserData()`. It's per-person state, and new joiners should see who has their hand up. **Active speaker layout / scene** → `setMeetingSessionData()`. It's room-level state, and late joiners should start in the right scene. **Server-triggered notification** → `sendAppMessage()` via the REST API. Your backend sends it directly without needing a participant to relay it. **Shared poll state** → `setMeetingSessionData()`. The poll and its results belong to the room, not a person. ## Example: combining all three A live Q\&A session is a natural fit for all three mechanisms. Participants raise their hand (per-person state), the host brings someone on stage (room-wide state), and the selected participant gets a private cue to unmute (ephemeral targeted message). ```javascript theme={null} // On join — each participant declares their role await call.join({ url: roomUrl, userData: { role: 'audience', handRaised: false }, }); // Host initializes room-wide state call.setMeetingSessionData( { currentSpeaker: null, queueOpen: true }, 'replace' ); // --- Audience: raise a hand --- async function raiseHand() { const { userData } = call.participants().local; await call.setUserData({ ...userData, handRaised: true }); } // --- Host: watch for raised hands --- call.on('participant-updated', ({ participant }) => { if (participant.userData?.handRaised) { addToSpeakerQueue(participant.session_id, participant.user_name); } else { removeFromSpeakerQueue(participant.session_id); } }); // --- Host: bring someone on stage --- function bringOnStage(sessionId) { // Update room-wide state so everyone's UI reflects the current speaker call.setMeetingSessionData({ currentSpeaker: sessionId }, 'shallow-merge'); // Send a private cue so the participant knows to unmute call.sendAppMessage({ type: 'you-have-the-floor' }, sessionId); } // --- Participant: receive the cue and clear their hand raise --- call.on('app-message', async ({ data }) => { if (data.type === 'you-have-the-floor') { call.setLocalAudio(true); const { userData } = call.participants().local; await call.setUserData({ ...userData, handRaised: false }); } }); // --- Everyone: highlight the current speaker --- call.on('meeting-session-state-updated', ({ meetingSessionState }) => { const { currentSpeaker } = meetingSessionState.data; highlightActiveSpeaker(currentSpeaker); }); ``` Why each mechanism was chosen here: * **`setUserData`** for `handRaised` — it's per-person, and participants who join mid-session should immediately see who has their hand up. * **`setMeetingSessionData`** for `currentSpeaker` — it's room-level state with one authoritative value, and late joiners need to know who's on stage. * **`sendAppMessage`** for the floor cue — it's a private one-time signal to a single participant, not state that belongs on the room or the participant object. ## See also * [sendAppMessage()](/reference/daily-js/instance-methods/send-app-message) * [setUserData()](/reference/daily-js/instance-methods/set-user-data) * [setMeetingSessionData()](/reference/daily-js/instance-methods/set-meeting-session-data) * [meetingSessionState()](/reference/daily-js/instance-methods/meeting-session-state) * [participants()](/reference/daily-js/instance-methods/participants) * [app-message](/reference/daily-js/events/messaging-events#app-message) * [participant-updated](/reference/daily-js/events/participant-events#participant-updated) * [meeting-session-state-updated](/reference/daily-js/events/messaging-events#meeting-session-state-updated) * [Send app message](/reference/rest-api/rooms/session/send-app-message) # Handling device permissions Source: https://docs.daily.co/docs/daily-js/guides/device-permissions Learn about common device permission errors and best practices for handling camera and microphone access in Daily applications. Accessing media input devices — cams, mics, and screens — can involve many and sometimes conflicting permission layers. Browsers, operating systems, devices, and participants are all factors that can complicate whether audio or video is shared or blocked as intended. This guide covers common media device errors and how to help participants resolve them, plus general best practices for handling media devices in your app. ## Common media device errors Daily accesses participant media input devices through the browser's [`MediaDevices`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices) interface. The interface's [`getUserMedia()`](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia) method prompts the user for device permissions and creates a [`MediaStream`](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream) with the corresponding tracks if permission is granted. If something goes wrong, the browser throws an [exception](https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia#exceptions) and Daily emits a [`camera-error`](/reference/daily-js/events/settings-events#camera-error) event. Browsers are inconsistent in their implementations of these exceptions for any given root issue — for example, if there is no camera plugged in, Chrome and Firefox throw a `NotFoundError`, but Safari throws an `OverconstrainedError`. Daily normalizes these inconsistencies so the same underlying issue generates the same error type regardless of browser or OS. The most common device errors are detailed below. For full details on all error categories, see the [`camera-error` reference](/reference/daily-js/events/settings-events#camera-error). ### Device in use Most common on Windows when another app is using the participant's camera. The `camera-error` event's `error.type` will be `cam-in-use`, `mic-in-use`, or `cam-mic-in-use`: ```json theme={null} { "action": "camera-error", "error": { "type": "cam-in-use", "msg": "Camera in use by another application." }, "errorMsg": { "errorMsg": "not allowed" }, "callClientId": "17225364729060.9442072768918943" } ``` Alert the participant that another application is using their device and ask them to quit it and reload the call. Prompt in a video call asks participant to restart an application ### Permissions denied Happens when the call hasn't been granted access to the participant's device — either the participant clicked "Block" in the browser permissions prompt, or the browser hasn't been granted access in OS settings. This is one of the most common `getUserMedia()` errors. The `camera-error` event's `error.type` will be `'permissions'`. The `error.blockedBy` field indicates whether the block came from the user or the OS, and `error.blockedMedia` indicates which media is blocked: ```json theme={null} { "action": "camera-error", "error": { "type": "permissions", "msg": "Permissions denied by user.", "blockedBy": "user", "blockedMedia": ["video", "audio"] }, "errorMsg": { "errorMsg": "not allowed" }, "callClientId": "17225364729060.9442072768918943" } ``` Whether the participant blocked access intentionally or by accident, the call UI should reflect that the device is blocked. Daily Prebuilt, for example, displays media icons in red with "Allow" underneath. When clicked, the participant sees instructions for how to unblock their devices. Video call displays a prompt that instructs a participant how to unblock their camera How to unblock a device depends on the device's manufacturer and the participant's browser. On some devices like iOS, a page refresh is enough to re-trigger the browser permission prompt. On others like some Android phones, participants may need to go into device settings to allow camera and microphone permissions. Testing on multiple devices and browsers is recommended. ### No available devices If the browser can't find any input devices for a given media type, the `camera-error` event's `error.type` will be `"not-found"`, with an `error.missingMedia` field detailing which devices were not found: ```json theme={null} { "action": "camera-error", "error": { "type": "not-found", "msg": "No device found.", "missingMedia": ["video"] }, "errorMsg": { "errorMsg": "devices error" }, "callClientId": "17225364729060.9442072768918943" } ``` Prompt the participant to check their devices and reload the call. Video call displays prompt to participant to check their input devices ## Best practices for handling media devices ### Device Permission Prompts In daily-js, permissions prompts will be triggered whenever media is first requested, typically on `startCamera()` or `join()`. By default, if the camera is enabled, daily-js will prompt for permission for both the camera and microphone, regardless of whether the microphone is enabled. This ensures a smoother user experience by reducing the number of prompts the participant sees. However, if the participant joins/startsCamera with only the mic enabled, the browser will only prompt for microphone access. This avoids the camera light turning on before the participant explicitly enables the camera, which can be a jarring experience. To change these default behaviors, see [`alwaysIncludeMicInPermissionPrompt`](/reference/daily-js/types/daily-call-options#param-always-include-mic-in-permission-prompt) and [`alwaysIncludeCamInPermissionPrompt`](/reference/daily-js/types/daily-call-options#param-always-include-cam-in-permission-prompt). ### Adjust instructions based on browser and device Device manufacturers and browsers don't always handle device permissions the same way. For example, the steps to re-allow access after a permissions error differ by OS, device, and browser. For the best experience, your call UI should show device-specific instructions. Granting access prompt on iOS and Android after a NotAllowedError Granting access prompt on iOS and Android after a NotAllowedError ### Create a pre-call lobby for device setup A test call page, lobby, or "green room" where participants can set up their devices before joining gives them a safe space to read and respond to permission prompts, configure their devices, and resolve any mistakes before going live. To enable Daily Prebuilt's prejoin UI, set the [`enable_prejoin_ui`](/reference/rest-api/rooms/update-room#body-properties-enable-people-ui) property to `true` at the room or domain level. Lobby interface of Daily Prebuilt call prompts a participant to set up camera and mic For custom apps, [`startCamera()`](/reference/daily-js/instance-methods/start-camera) is a good starting point for building a lobby — it requests device access and starts the camera before the participant joins the call. To give participants visual confirmation that their microphone is working, call [`startLocalAudioLevelObserver()`](/reference/daily-js/instance-methods/start-local-audio-level-observer) after `startCamera()`. Daily will then emit [`local-audio-level`](/reference/daily-js/events/media-events#local-audio-level) events at a regular interval with a volume value between `0` and `1`, which you can use to drive a mic activity indicator in your lobby UI: ```javascript theme={null} await call.startCamera(); await call.startLocalAudioLevelObserver(500); // interval in ms call.on('local-audio-level', ({ audioLevel }) => { updateMicIndicator(audioLevel); // e.g. animate a level meter }); ``` Call [`stopLocalAudioLevelObserver()`](/reference/daily-js/instance-methods/stop-local-audio-level-observer) when the participant enters the call or leaves the lobby. ### iOS development and device permissions Media device [permission requests](https://developer.apple.com/documentation/avfoundation/avcapturedevice/1624584-requestaccess) are async when using [AVFoundation](https://developer.apple.com/av-foundation/), Apple's audiovisual framework. The completion handler for these requests is called on an arbitrary dispatch queue. These permissions should never be blocked, as that can lead to deadlocks. An API that was designed to be synchronous and blocking may need to be made async with a callback when adapted for iOS. # Working with multiple call instances Source: https://docs.daily.co/docs/daily-js/guides/multi-instance A guide to using multiple call client instances simultaneously. Multiple call client support requires `daily-js` version >= `0.67.0`. `daily-js` and `react-native-daily-js` support multiple simultaneous call client instances. The ability to use multiple instances simultaneously affords various use cases both simple and complex. One simple example is having a secondary call instance for building a background effects preview. In a single call, setting the background processing effect applies the effect immediately, disallowing any preview step. With a second call instance, you can use prejoin functionality to start the camera, set the background effect, and preview the track without affecting the primary call until you choose to apply the effect. Another use case we've heard is for call coordinators, where a special set of participants monitor or coordinate a larger call and may want to be in a separate call simultaneously for communicating behind the scenes. Breakout rooms could also be solved using multiple call instances, [though we generally encourage the use of track subscriptions](https://www.daily.co/blog/why-you-probably-dont-need-multiple-daily-call-objects/#breakout-rooms) for smoother transitioning, communication between rooms, and call quality monitoring. There are doubtless other use cases that could be solved with multiple clients. Always feel free to [reach out to Daily](mailto:help@daily.co) for help determining the best approach for solving yours. ## Limitations There are a few known limitations when working with multiple call instances. 1. Each additional call instance will use additional bandwidth and CPU, as it does not simply piggyback on resources already in use. Take this into consideration to ensure instances are properly limited so as not to overwhelm a client's network or device. 2. `setOutputDeviceAsync()` will affect all audio elements on the window regardless of which instance the underlying tracks stem from. This function is a helper for rounding up all audio elements on the window and setting their `sinkId` to the matching device. If you use this function, call it on all call instances so that the underlying state management stays in sync with reality. 3. Multiple call client instances are not yet supported in `Daily React`. ## Opt-in flag Most use cases are still best suited for a single call client instance and the existence of multiple often indicates a bug. For this reason, support for multiple call instances is opt-in. Without opting in, the SDK will throw an error in the constructor when it detects a second call instance. To opt in, add `allowMultipleCallInstances: true` to the properties passed to your constructors. ```javascript theme={null} const call1 = Daily.createCallObject({ allowMultipleCallInstances: true }); const call2 = Daily.createCallObject({ allowMultipleCallInstances: true }); ``` If you were using multiple call instances before official support, you may have been setting `strictMode: false` in your constructor. This flag still exists and is used for other validation work but will soon no longer allow the multiple-instance behavior. Replace it with `allowMultipleCallInstances`. ## Retrieving a call instance If you need to access a call instance created elsewhere in your codebase, use the static `getCallInstance()` method: ```typescript theme={null} // Returns the first (and usually only) instance const call = Daily.getCallInstance(); // Returns a specific instance by ID const call = Daily.getCallInstance(callClientId); ``` This is most useful in single-instance apps where you want to avoid threading the call object through your component tree. For multi-instance apps, each instance has a unique `callClientId` string you can use to target a specific one. ## `callClientId` Each call client instance has a public property identifying it: `callClientId`. You can use this for identification, and you can also pass it to `getCallInstance()` to retrieve a handle to the corresponding instance. ```javascript theme={null} const call1Id = call1.callClientId; const _call1 = Daily.getCallInstance(call1Id); assert(call1 === _call1); ``` ## Event handling All events include `callClientId` to indicate which client emitted the event. ```javascript theme={null} call1.on('joined-meeting', handleJoin); call2.on('joined-meeting', handleJoin); function handleJoin(event) { switch (event.callClientId) { case call1.callClientId: showJoinedDialog(call1); break; case call2.callClientId: showJoinedDialog(call2); break; } } ``` ## Logging To help match up logs and metrics across multiple calls for a single user, the SDK logs all user session IDs for every simultaneous session. An example log: `Simultaneous call client instance created, with participant session_id: 47365350-8116-4ec3-b4eb-f39a21a9ead8` If one instance has not yet joined a room (like in the background effects preview use case), the other instance will log: `Simultaneous call client instance created, with participant session_id: not yet set` Once the second instance joins, the first will log again with the correct session ID. ## Demos * [Using a second instance to preview camera effects](https://github.com/daily-co/cam-processing-preview-demo) # Network and call quality Source: https://docs.daily.co/docs/daily-js/guides/network-quality Monitor network quality and CPU load during a call, and test conditions before a user joins. Daily provides events and methods for tracking call quality in real time and for running pre-call tests before a user joins. This guide covers the most common patterns for each. ## Monitoring quality during a call ### Reacting to quality changes The [`network-quality-change`](/reference/daily-js/events/network-events#network-quality-change) event fires whenever Daily's assessment of the local participant's network changes. This is the recommended way to drive a quality indicator in your UI — no polling required. ```javascript theme={null} call.on('network-quality-change', ({ networkState, networkStateReasons }) => { switch (networkState) { case 'good': showQualityIndicator('good'); break; case 'warning': showQualityIndicator('warning'); console.log('Degraded due to:', networkStateReasons); break; case 'bad': showQualityIndicator('bad'); notifyUser('Your connection quality is poor.'); break; } }); ``` `networkState` is calculated from packet loss, round-trip time, and available outgoing bitrate, averaged over a \~30-second rolling window. When the state is `'warning'` or `'bad'`, `networkStateReasons` tells you which metrics are responsible — `'sendPacketLoss'`, `'recvPacketLoss'`, `'roundTripTime'`, or `'availableOutgoingBitrate'` — which can help you decide how to respond (for example, prompting a user to check their connection vs. proactively reducing video quality). In Daily Prebuilt, `'warning'` triggers automatic bandwidth reduction and `'bad'` disables the local camera. In a custom UI you're responsible for any adaptive behavior. ### Getting detailed stats on demand [`getNetworkStats()`](/reference/daily-js/instance-methods/get-network-stats) returns a point-in-time snapshot with per-stream bitrates, packet loss, jitter, and RTT — useful if you want to log detailed diagnostics, build an advanced quality panel, or inspect conditions at a specific moment. ```javascript theme={null} const { networkState, networkStateReasons, stats } = await call.getNetworkStats(); if (stats.latest) { console.log('Send packet loss:', stats.latest.totalSendPacketLoss); console.log('RTT (ms):', stats.latest.networkRoundTripTime); console.log('Worst video recv packet loss this session:', stats.worstVideoRecvPacketLoss); } ``` Stats in `stats.latest` are updated approximately every two seconds. The object is empty early in the call before enough data has been collected. Firefox does not report send-side packet loss or audio jitter, and does not provide `availableOutgoingBitrate` or `networkRoundTripTime`. ### Handling connection interruptions The [`network-connection`](/reference/daily-js/events/network-events#network-connection) event fires when a connection is established or interrupted. Every call uses two connections — `signaling` (call management) and `sfu` (audio/video) — and both reconnect automatically as long as signaling is alive. ```javascript theme={null} call.on('network-connection', ({ type, event }) => { if (event === 'interrupted') { if (type === 'signaling') { // Participant will be ejected if signaling doesn't recover within ~20s showReconnectingBanner(); } else { // SFU interruption — media will pause but reconnect automatically showMediaPausedIndicator(); } } else if (event === 'connected') { hideReconnectingBanner(); } }); ``` ## Monitoring CPU load High CPU load can degrade video quality independently of network conditions, particularly on lower-powered devices or in large calls with many incoming video streams. Listen for [`cpu-load-change`](/reference/daily-js/events/network-events#cpu-load-change) to detect this: ```javascript theme={null} call.on('cpu-load-change', ({ cpuLoadState, cpuLoadStateReason }) => { if (cpuLoadState === 'high') { console.log('High CPU load — reason:', cpuLoadStateReason); if (cpuLoadStateReason === 'encode') { // Reduce outgoing video quality call.updateSendSettings({ video: { maxQuality: 'low' } }); } else if (cpuLoadStateReason === 'decode') { // Reduce number of incoming video streams call.updateReceiveSettings({ '*': { video: { layer: 0 } } }); } } }); ``` `cpuLoadStateReason` identifies whether encoding, decoding, or scheduling is the bottleneck, so you can target the right response. Use [`getCpuLoadStats()`](/reference/daily-js/instance-methods/get-cpu-load-stats) to poll for per-track decode stats or inspect frame encode/decode times in detail. CPU load monitoring applies to video calls only. ## Testing quality before joining Running a pre-call test lets you warn users about poor conditions before they enter a call, or initialize bandwidth settings appropriately. ### testCallQuality() [`testCallQuality()`](/reference/daily-js/instance-methods/test-call-quality) is the recommended pre-call test. It connects to a private Daily room, streams video to Daily's infrastructure for up to 30 seconds, and returns a verdict based on packet loss, round-trip time, and available bitrate. Call it after [`preAuth()`](/reference/daily-js/instance-methods/pre-auth) or [`startCamera()`](/reference/daily-js/instance-methods/start-camera) and before [`join()`](/reference/daily-js/instance-methods/join): ```javascript theme={null} const call = Daily.createCallObject(); await call.preAuth({ url: DAILY_ROOM_URL }); const results = await call.testCallQuality(); switch (results.result) { case 'good': proceedToJoin(); break; case 'warning': showWarning('Your connection may cause choppy audio or video.'); offerToJoinAnyway(); break; case 'bad': showWarning('Your connection is poor. Try a different network.'); offerToJoinAnyway(); break; case 'failed': showError('Could not reach Daily servers. Check your network and try again.'); break; case 'aborted': // join() or stopTestCallQuality() was called before data collection finished break; } ``` The test runs for up to 30 seconds but you can stop it early: ```javascript theme={null} call.stopTestCallQuality(); ``` If [Adaptive Bitrate](/docs/guides/architecture-and-monitoring/adaptive-bitrate) is enabled (the default for 1:1 calls), the test results are used automatically at join time to initialize the highest simulcast layer appropriately — so running the test improves the call start even if the result is `'good'`. ### Other pre-call tests Three additional tests are available for more targeted diagnostics: * **[`testWebsocketConnectivity()`](/reference/daily-js/instance-methods/test-websocket-connectivity)** — Checks whether WebSocket connections can be established with Daily's signaling servers across AWS regions, without requiring a camera or a room. This is the right first check if you're building for environments where corporate firewalls or VPNs might block WebSockets — a `'failed'` result here means the call can't even get started. * **[`testNetworkConnectivity(videoTrack)`](/reference/daily-js/instance-methods/test-network-connectivity)** — Checks whether a TURN connection can be established with Daily's infrastructure. A step beyond WebSocket connectivity, useful for stricter network environments. Returns a simple `'passed'` / `'failed'` rather than a quality grade. * **[`testPeerToPeerCallQuality(options)`](/reference/daily-js/instance-methods/test-peer-to-peer-call-quality)** — Returns a quality verdict (`'good'` / `'warning'` / `'bad'`) using TURN servers rather than an actual Daily room session. Runs faster (15 seconds by default) and doesn't require a room URL, but it measures receive-side metrics rather than send-side, and unlike `testCallQuality()`, it doesn't seed Adaptive Bitrate for the subsequent call. ### Listening for results via event If you're building on top of Daily Prebuilt, it runs `testCallQuality()` automatically in its prejoin UI. You can read the results without calling the method yourself by listening for the [`test-completed`](/reference/daily-js/events/network-events#test-completed) event: ```javascript theme={null} call.on('test-completed', ({ test, results }) => { if (test === 'call-quality') { console.log('Pre-call quality result:', results.result); } }); ``` ## See also * [testCallQuality()](/reference/daily-js/instance-methods/test-call-quality) * [getNetworkStats()](/reference/daily-js/instance-methods/get-network-stats) * [getCpuLoadStats()](/reference/daily-js/instance-methods/get-cpu-load-stats) * [updateSendSettings()](/reference/daily-js/instance-methods/update-send-settings) * [updateReceiveSettings()](/reference/daily-js/instance-methods/update-receive-settings) * [network-quality-change](/reference/daily-js/events/network-events#network-quality-change) * [network-connection](/reference/daily-js/events/network-events#network-connection) * [cpu-load-change](/reference/daily-js/events/network-events#cpu-load-change) * [test-completed](/reference/daily-js/events/network-events#test-completed) * [Adaptive bitrate](/docs/guides/architecture-and-monitoring/adaptive-bitrate) * [Logging and metrics](/docs/guides/architecture-and-monitoring/logging-and-metrics) # Permissions Source: https://docs.daily.co/docs/daily-js/guides/permissions Control what participants can send, receive, and administer in a Daily call. Daily's permissions system lets you control exactly what each participant is allowed to do in a call: which media they can publish, which streams they can receive, and which administrative actions they can perform. Permissions can be set at join time via a meeting token or updated dynamically at runtime. ## DailyParticipantPermissions Every participant object exposes a `permissions` field of type `DailyParticipantPermissions`: See the [`DailyParticipantPermissions` reference](/reference/daily-js/types/daily-participant-permissions) for the full interface definition and field descriptions. ## Updating permissions at runtime Use `updateParticipant()` to change a participant's permissions mid-call. The caller must themselves have `canAdmin: 'participants'` (or `canAdmin: true`) or be an owner. ```typescript theme={null} call.updateParticipant(sessionId, { updatePermissions: { hasPresence: true, canSend: new Set(['audio', 'video']), canAdmin: false, canReceive: { base: true, }, }, }); ``` The `updatePermissions` field accepts a `DailyParticipantPermissionsUpdate`, where every field is optional — only those provided are changed: ```typescript theme={null} type DailyParticipantPermissionsUpdate = { hasPresence?: boolean; canSend?: Array | Set | boolean; canReceive?: Partial; canAdmin?: Array | Set | boolean; }; ``` ### Restricting what a participant can send The example below restricts a participant to audio-only (no camera or screen share): ```typescript theme={null} call.updateParticipant(participantSessionId, { updatePermissions: { canSend: new Set(['audio']), }, }); ``` To re-enable video later: ```typescript theme={null} call.updateParticipant(participantSessionId, { updatePermissions: { canSend: new Set(['audio', 'video']), }, }); ``` ### Restricting what a participant can receive The example below allows a participant to receive audio from everyone but video only from a specific user: ```typescript theme={null} call.updateParticipant(participantSessionId, { updatePermissions: { canReceive: { base: { video: false, audio: true, screenVideo: false, screenAudio: false, customVideo: { '*': false }, customAudio: { '*': false }, }, byUserId: { 'presenter-user-id': { video: true }, }, }, }, }); ``` ## Events Fired when a participant's permissions change. Check `participant.permissions` to see the updated values. ```typescript theme={null} call.on('participant-updated', ({ participant }) => { console.log('Updated permissions:', participant.permissions); }); ``` ## Complete example Demonstrates the full permissions lifecycle: new participants join locked out, request presence via an app message, get admitted by the owner, and then have their `canSend` permissions managed through a panel. * Open `https://localhost?t=YOUR_OWNER_TOKEN` for the owner view * Open `https://localhost` (no token) for a participant view ```javascript app.js theme={null} const ROOM_URL = 'https://your-domain.daily.co/room'; const token = new URLSearchParams(window.location.search).get('t') ?? undefined; let canAdmin = false; const call = Daily.createCallObject(); window.call = call; // for debugging const participantIds = new Set(); window.participantIds = participantIds; // for debugging const SEND_TRACKS = ['audio', 'video']; // ── Helpers ────────────────────────────────────────────────────────────────── function _canAdmin(permissions) { const ca = permissions?.canAdmin; if (ca === true) return true; if (!ca) return false; return ca instanceof Set ? ca.has('participants') : false; } function canSendSet(permissions) { const cs = permissions?.canSend; if (cs === true) return new Set(SEND_TRACKS); if (!cs) return new Set(); return cs instanceof Set ? new Set(cs) : new Set(cs); } // ── Track rendering (shared) ───────────────────────────────────────────────── function addParticipantTile(participant) { const sessionId = participant.session_id; let tile = document.getElementById(`tile-${sessionId}`); if (tile) return tile; tile = document.createElement('div'); tile.id = `tile-${sessionId}`; tile.className = 'tile'; const label = document.createElement('span'); label.className = 'tile-label'; console.log(call.participants()); label.textContent = participant.local ? 'You' : sessionId.slice(0, 8); tile.append(label); document.getElementById('tracks').appendChild(tile); return tile; } function addVideoTile(participant, track) { const tile = addParticipantTile(participant); if (!tile) return; let video = tile.querySelector('video'); if (video) { video.srcObject = new MediaStream([track]); return; } video = document.createElement('video'); video.autoplay = true; video.playsInline = true; video.srcObject = new MediaStream([track]); tile.append(video); } function addAudioTile(participant, track) { const tile = addParticipantTile(participant); if (!tile) return; let audio = tile.querySelector('audio'); if (audio) { audio.srcObject = new MediaStream([track]); return; } audio = document.createElement('audio'); audio.autoplay = true; audio.srcObject = new MediaStream([track]); tile.append(audio); } call.on('track-started', ({ participant, track, type }) => { console.log('track started', { participant, track, type }); if (type === 'video') addVideoTile(participant, track); if (type === 'audio' && !participant.local) addAudioTile(participant, track); }); call.on('track-stopped', ({ participant, type }) => { if (type === 'video') document.getElementById(`tile-${participant.session_id}`)?.remove(); }); // ── Owner panel ────────────────────────────────────────────────────────────── let pendingSessionId = null; function populateDropdown() { const select = document.getElementById('participant-select'); let prev = select.value; console.log(`Select.value: ${select.value}`); select.innerHTML = ''; for (const [id, p] of Object.entries(call.participants())) { if (p.local) continue; const opt = document.createElement('option'); opt.value = id; opt.textContent = p.user_name || id.slice(0, 8); select.appendChild(opt); } select.value = prev; syncCheckboxes(); } function syncCheckboxes() { const id = document.getElementById('participant-select').value; const participant = id ? call.participants()[id] : null; const perms = participant?.permissions; const sending = canSendSet(perms); document.getElementById('check-hasPresence').checked = perms?.hasPresence ?? false; for (const track of SEND_TRACKS) { document.getElementById(`check-${track}`).checked = sending.has(track); } } document .getElementById('participant-select') .addEventListener('change', syncCheckboxes); document.getElementById('update-btn').addEventListener('click', async () => { const id = document.getElementById('participant-select').value; if (!id) return; const hasPresence = document.getElementById('check-hasPresence').checked; const canSend = new Set( SEND_TRACKS.filter((t) => document.getElementById(`check-${t}`).checked), ); console.log('Updating permissions for', id, { hasPresence, canSend }); await call.updateParticipant(id, { updatePermissions: { hasPresence, canSend }, }); }); call.on('app-message', ({ data }) => { if (data?.type !== 'request-presence') return; pendingSessionId = data.sessionId; document.getElementById('request-text').textContent = `Participant ${data.sessionId.slice(0, 8)} is requesting presence.`; document.getElementById('request-popup').hidden = false; }); document.getElementById('approve-btn').addEventListener('click', async () => { if (!pendingSessionId) return; // Grant presence only — canSend stays false until the owner enables it via the panel console.log('Granting presence to', pendingSessionId); await call.updateParticipant(pendingSessionId, { updatePermissions: { hasPresence: true }, }); }); document.getElementById('deny-btn').addEventListener('click', () => { pendingSessionId = null; document.getElementById('request-popup').hidden = true; }); // ── Participant panel ───────────────────────────────────────────────────────── function updatePermsDisplay(permissions) { const sending = canSendSet(permissions); console.log('canSend:', sending); const rows = [ ['Has presence', permissions?.hasPresence], ['Audio', sending.has('audio')], ['Video', sending.has('video')], ]; document.getElementById('perms-list').innerHTML = rows .map( ([label, on]) => `
  • ${label}: ${on ? '✓' : '✗'}
  • `, ) .join(''); } document.getElementById('request-btn').addEventListener('click', () => { call.sendAppMessage( { type: 'request-presence', sessionId: call.participants().local.session_id, }, '*', ); document.getElementById('request-btn').textContent = 'Request sent…'; document.getElementById('request-btn').disabled = true; }); // ── Shared event handlers ───────────────────────────────────────────────────── call.on('participant-joined', (e) => { const participant = e.participant; console.log('Participant joined', e); if (canAdmin) { console.log( 'Participant joined:', participantIds, _canAdmin(participant.permissions), ); if (!participantIds.has(participant.session_id)) { if (!_canAdmin(participant.permissions)) { // Lock out every new non-admin participant immediately console.log('Denying presence to', participant.session_id); call.updateParticipant(participant.session_id, { updatePermissions: { hasPresence: false, canSend: false }, }); } participantIds.add(participant.session_id); } else if (pendingSessionId === participant.session_id) { pendingSessionId = null; document.getElementById('request-popup').hidden = true; } populateDropdown(); } }); call.on('joined-meeting', () => { canAdmin = _canAdmin(call.participants().local.permissions); if (canAdmin) { document.getElementById('owner-panel').hidden = false; populateDropdown(); } else { document.getElementById('participant-panel').hidden = false; updatePermsDisplay(call.participants().local?.permissions); } }); call.on('participant-updated', ({ participant }) => { if (canAdmin && !participant.local) { populateDropdown(); } if (!canAdmin && participant.local) { updatePermsDisplay(participant.permissions); // Re-enable request button if presence was revoked if (!participant.permissions?.hasPresence) { document.getElementById('request-btn').textContent = 'Request presence'; document.getElementById('request-btn').disabled = false; } const canSend = canSendSet(participant.permissions); if (canSend.has('video') && !call.localVideo()) call.setLocalVideo(true); if (canSend.has('audio') && !call.localAudio()) call.setLocalAudio(true); } }); call.on('participant-left', ({ participant, reason }) => { document.getElementById(`tile-${participant.session_id}`)?.remove(); if (canAdmin) { if (reason !== 'hidden') { participantIds.delete(participant.session_id); } populateDropdown(); } }); // ── Join ────────────────────────────────────────────────────────────────────── if (token) { await call.join({ url: ROOM_URL, token }); } else { await call.join({ url: ROOM_URL }); } ``` ```html index.html theme={null} Permissions Demo
    ``` ```css styles.css theme={null} *, *::before, *::after { box-sizing: border-box; } [hidden] { display: none !important; } body { font-family: sans-serif; background: #0f0f1a; color: #e0e0e0; margin: 0; height: 100vh; overflow: hidden; } #app { display: flex; height: 100%; } /* ── Tracks area ── */ #tracks { flex: 1; display: flex; flex-wrap: wrap; align-content: flex-start; gap: 0.5rem; padding: 1rem; overflow-y: auto; } .tile { position: relative; width: 280px; height: 210px; background: #1a1a2e; border-radius: 6px; overflow: hidden; } .tile video { width: 100%; height: 100%; object-fit: cover; } .tile-label { position: absolute; bottom: 6px; left: 8px; font-size: 0.75rem; background: rgba(0, 0, 0, 0.55); padding: 2px 6px; border-radius: 3px; } /* ── Side panel ── */ aside { width: 260px; flex-shrink: 0; background: #13131f; border-left: 1px solid #2a2a3e; padding: 1rem; overflow-y: auto; display: flex; flex-direction: column; gap: 0.75rem; } h2 { margin: 0; font-size: 0.9rem; text-transform: uppercase; letter-spacing: 0.06em; color: #a0a0c0; } select { width: 100%; background: #1a1a2e; color: #e0e0e0; border: 1px solid #2a2a3e; border-radius: 4px; padding: 0.4rem; font-size: 0.875rem; } #permission-checkboxes { display: flex; flex-direction: column; gap: 0.4rem; font-size: 0.875rem; } #permission-checkboxes label { display: flex; align-items: center; gap: 0.5rem; cursor: pointer; } button { padding: 0.45rem 0.9rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; background: #e94560; color: #fff; } button:disabled { opacity: 0.45; cursor: not-allowed; } #update-btn { background: #2ecc71; color: #fff; } /* ── Request popup ── */ #request-popup { background: #1a1a2e; border: 1px solid #2a2a3e; border-radius: 6px; padding: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; } #request-text { margin: 0; font-size: 0.875rem; } #request-popup div { display: flex; gap: 0.5rem; } #approve-btn { background: #2ecc71; } #deny-btn { background: #e94560; } /* ── Permissions list ── */ #perms-list { list-style: none; margin: 0; padding: 0; font-size: 0.875rem; display: flex; flex-direction: column; gap: 0.3rem; } #perms-list li.on { color: #2ecc71; } #perms-list li.off { color: #555577; } ```
    # Waiting room (knock-to-join) Source: https://docs.daily.co/docs/daily-js/guides/waiting-room Build a lobby where participants request access and owners admit or deny them in real time. Daily's knock-to-join feature lets you gate entry to a room. Participants who arrive are held in a waiting state until an owner explicitly admits them. This guide covers room configuration, the waiting participant flow, and the admin flow. ## Room configuration Knock-to-join requires two properties on the room: * [`privacy: 'private'`](/reference/rest-api/rooms/create-room#body-properties-privacy) — places tokenless participants in the lobby instead of admitting them directly * [`enable_knocking: true`](/reference/rest-api/rooms/create-room#body-properties-enable-knocking) — allows those lobby participants to ring for admission ```bash theme={null} POST /v1/rooms { "properties": { "privacy": "private", "enable_knocking": true } } ``` Any participant who joins with a meeting token — no matter their permissions — bypasses the lobby and enters the call directly. Only tokenless participants are placed in the lobby. ### Using Prebuilt If you're using Daily Prebuilt, the lobby UI is built in. Once the room is configured above, also set `enable_prejoin_ui` on the [room](/reference/rest-api/rooms/create-room#body-properties-enable-prejoin-ui), [domain](/reference/rest-api/domain/set-domain-config#body-properties-enable-prejoin-ui), or [meeting token](/reference/rest-api/meeting-tokens/create-meeting-token#body-properties-enable-prejoin-ui) and you're done — Daily handles the waiting room experience for you. The rest of this guide covers building a custom lobby UI with the call object API. ## Waiting participant flow Call `join()` without a token. The participant lands in the lobby and [`accessState()`](/reference/daily-js/instance-methods/access-state) returns `{ access: { level: 'lobby' } }` — they are connected but hidden from other participants. ```typescript theme={null} await call.join({ url: 'https://your-domain.daily.co/your-room' }); // accessState() === { access: { level: 'lobby' } } ``` Call [`requestAccess()`](/reference/daily-js/instance-methods/request-access) with a display name. This notifies admins and adds the participant to the waiting list. The returned Promise **blocks until the request is decided** — you can use the return value directly: ```typescript theme={null} const { granted } = await call.requestAccess({ name: 'Alice' }); if (granted) { showCallUI(); } else { // Access denied — participant has been ejected from the call showRejectedUI(); } ``` If you need to react to access changes in multiple places (e.g. updating different parts of your UI), the event-based approach may be a better fit. [`access-state-updated`](/reference/daily-js/events/lifecycle-events#access-state-updated) fires when access is granted; a fatal [`error`](/reference/daily-js/events/error-events#error) event with `error.type === 'not-allowed'` fires when access is denied. ```typescript theme={null} call.on('access-state-updated', ({ access }) => { if (access.level === 'full') { showCallUI(); } }); call.on('error', ({ error }) => { if (error.type === 'not-allowed') { // Access was denied — participant has been removed from the call showRejectedUI(); } }); await call.requestAccess({ name: 'Alice' }); ``` ## Admin flow Join with a meeting token that grants `canAdmin: ['participants']` (or `is_owner: true`). Either bypasses the lobby and enters the call directly. ```typescript theme={null} await call.join({ url: 'https://your-domain.daily.co/your-room', token: 'ADMIN_TOKEN', // token with canAdmin: ['participants'] or is_owner: true }); ``` See [meeting token permissions](/reference/rest-api/meeting-tokens/create-meeting-token#body-properties-permissions) for how to create a token with `canAdmin`. Call [`waitingParticipants()`](/reference/daily-js/instance-methods/waiting-participants) immediately after joining to catch anyone who arrived before you. ```typescript theme={null} for (const [id, p] of Object.entries(call.waitingParticipants())) { showAdmitPrompt(p.id, p.name); } ``` Each waiting participant is a `DailyWaitingParticipant`: ```typescript theme={null} interface DailyWaitingParticipant { id: string; // unique ID for this waiting session name: string; // display name passed to requestAccess() awaitingAccess: { level: 'full' }; } ``` Subscribe to lobby events to keep your UI in sync: | Event | Fires when | | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | [`waiting-participant-added`](/reference/daily-js/events/participant-events#waiting-participant-added) | A participant calls `requestAccess()` | | [`waiting-participant-updated`](/reference/daily-js/events/participant-events#waiting-participant-updated) | A waiting participant calls `requestAccess()` again with a new name | | [`waiting-participant-removed`](/reference/daily-js/events/participant-events#waiting-participant-removed) | A participant is admitted, denied, or leaves on their own | ```typescript theme={null} call.on('waiting-participant-added', ({ participant }) => { showAdmitPrompt(participant.id, participant.name); }); call.on('waiting-participant-removed', ({ participant }) => { removeAdmitPrompt(participant.id); }); ``` Use [`updateWaitingParticipant()`](/reference/daily-js/instance-methods/update-waiting-participant) to act on one participant, or [`updateWaitingParticipants()`](/reference/daily-js/instance-methods/update-waiting-participants) to act on many at once. ```typescript theme={null} // Admit one await call.updateWaitingParticipant(id, { grantRequestedAccess: true }); // Deny one await call.updateWaitingParticipant(id, { grantRequestedAccess: false }); // Admit all but one currently waiting await call.updateWaitingParticipants([ id: { grantRequestedAccess: false }, '*': { grantRequestedAccess: true } ]); ``` ## Complete example Open `https://localhost?t=YOUR_ADMIN_TOKEN` for the admin view, or `https://localhost` (no token) for the waiting participant view. ```javascript app.js theme={null} const ROOM_URL = 'https://your-domain.daily.co/your-room'; // Pass an admin token via ?t=... to join as admin; omit for waiting participant const token = new URLSearchParams(window.location.search).get('t') ?? undefined; const isAdmin = Boolean(token); const call = Daily.createCallObject(); if (isAdmin) { // --- Admin side --- document.getElementById('admin-view').hidden = false; document.getElementById('admin-status').textContent = 'Joining…'; call.on('waiting-participant-added', ({ participant }) => { document.getElementById('no-waiters').hidden = true; const notice = document.createElement('div'); notice.id = `waiting-${participant.id}`; notice.className = 'notice'; notice.innerHTML = ` ${participant.name || 'Someone'} wants to join `; document.getElementById('lobby-notices').appendChild(notice); }); call.on('waiting-participant-removed', ({ participant }) => { document.getElementById(`waiting-${participant.id}`)?.remove(); if (Object.keys(call.waitingParticipants()).length === 0) { document.getElementById('no-waiters').hidden = false; } }); document.getElementById('lobby-notices').addEventListener('click', async (e) => { const btn = e.target.closest('button'); if (!btn) return; const { id, action } = btn.dataset; await call.updateWaitingParticipant(id, { grantRequestedAccess: action === 'admit', }); }); await call.join({ url: ROOM_URL, token }); document.getElementById('admin-status').textContent = "You're in the call."; // Handle anyone already waiting when the admin joins const alreadyWaiting = call.waitingParticipants(); if (Object.keys(alreadyWaiting).length === 0) { document.getElementById('no-waiters').hidden = false; } for (const p of Object.values(alreadyWaiting)) { call.emit('waiting-participant-added', { participant: p }); } } else { // --- Waiting participant side --- document.getElementById('waiting-view').hidden = false; document.getElementById('waiting-status').textContent = 'Joining…'; call.on('access-state-updated', ({ access }) => { if (access.level === 'full') { document.getElementById('waiting-view').hidden = true; document.getElementById('call-view').hidden = false; } }); call.on('error', ({ error }) => { if (error.type === 'not-allowed') { document.getElementById('waiting-view').hidden = true; document.getElementById('rejected-view').hidden = false; } }); await call.join({ url: ROOM_URL }); // accessState() === { access: { level: 'lobby' } } const name = prompt('Your name') ?? 'Guest'; document.getElementById('waiting-status').textContent = `Waiting to be admitted, ${name}…`; await call.requestAccess({ name }); // accessState() === { access: { level: 'lobby' }, awaitingAccess: { level: 'full' } } } ``` ```html index.html theme={null} Waiting Room Demo ``` ```css styles.css theme={null} body { font-family: sans-serif; background: #0f0f1a; color: #e0e0e0; margin: 0; padding: 1.5rem; } h2 { margin: 1rem 0 0.5rem; font-size: 1rem; text-transform: uppercase; letter-spacing: 0.05em; color: #a0a0c0; } .status { font-style: italic; color: #a0a0c0; margin: 0 0 0.5rem; } .status.admitted { color: #2ecc71; font-style: normal; font-weight: bold; } .status.rejected { color: #e94560; font-style: normal; font-weight: bold; } #no-waiters { color: #555577; font-style: italic; } .notice { display: flex; align-items: center; gap: 0.75rem; padding: 0.6rem 0.75rem; background: #1a1a2e; border-radius: 6px; margin-bottom: 0.5rem; } .notice span { flex: 1; } button { padding: 0.4rem 0.9rem; border: none; border-radius: 4px; cursor: pointer; font-size: 0.875rem; } button[data-action="admit"] { background: #2ecc71; color: #fff; } button[data-action="deny"] { background: #e94560; color: #fff; } ``` # Installation Source: https://docs.daily.co/docs/daily-js/installation Install @daily-co/daily-js via a package manager or load it from a CDN. ## Requirements daily-js runs in any browser that supports WebRTC. See [here](/reference/daily-js/static-methods/supported-browser#supported-browsers) for a full list of supported browsers and versions. ```javascript theme={null} const { supported, name, version } = Daily.supportedBrowser(); if (!supported) { alert(`${name} ${version} does not support WebRTC. Please use a modern browser.`); } ``` ## Package manager Install the `@daily-co/daily-js` package using your preferred package manager: The `@daily-co/daily-js` package is small by design — the call bundle is fetched from Daily's CDN at runtime when you join. See [How it works](/docs/daily-js/introduction#how-it-works) for details. ```bash npm theme={null} npm install @daily-co/daily-js ``` ```bash yarn theme={null} yarn add @daily-co/daily-js ``` ```bash pnpm theme={null} pnpm add @daily-co/daily-js ``` ## Importing the library The package's default export is declared as `Daily` in the TypeScript types, while, for historical reasons, the underlying class is named `DailyIframe`. In our documentation and examples, we refer to the default export as `Daily` for clarity, but you may see `DailyIframe` in some contexts, especially in older code snippets. Both refer to the same default export. Use a default import. The exported default is the `Daily` class, which acts as both a factory and a namespace for static utilities. ```javascript theme={null} import Daily from '@daily-co/daily-js'; const call = Daily.createCallObject(); ``` You can also import named exports for constants and types: ```javascript theme={null} Daily, { DAILY_STATE_JOINED, DAILY_STATE_LEFT, DAILY_TRACK_STATE_PLAYABLE, } from '@daily-co/daily-js'; ``` Require the package as you would any CommonJS module. The default export is available on the `.default` property: ```javascript theme={null} const Daily = require('@daily-co/daily-js').default; // or, if your bundler resolves the `main` field directly: // const Daily = require('@daily-co/daily-js'); const call = Daily.createCallObject(); ``` Load daily-js directly from [unpkg](https://unpkg.com) without a build step. After the script loads, `DailyIframe` is available as a global on `window`. ```html theme={null} ``` To pin a specific version, append it to the URL: ```html theme={null} ``` Pinning a version in production prevents your app from being affected by future breaking changes. ## TypeScript `@daily-co/daily-js` ships with TypeScript definitions in `index.d.ts`. No additional `@types` package is needed. ```typescript theme={null} Daily, { DailyCall, DailyParticipant, DailyEventObjectParticipants, DailyBrowserInfo, } from '@daily-co/daily-js'; // Factory methods return DailyCall const call: DailyCall = Daily.createCallObject(); // Event callbacks are fully typed call.on('joined-meeting', (event: DailyEventObjectParticipants) => { Object.values(event.participants).forEach((p: DailyParticipant) => { console.log(p.user_name, p.session_id); }); }); // Browser support check const info: DailyBrowserInfo = Daily.supportedBrowser(); if (!info.supported) { throw new Error('WebRTC not supported in this browser'); } ``` While `Daily` is the default export and namespace, `DailyCall` is the interface describing an active call instance returned by the factory methods (`createFrame`, `createCallObject`, `wrap`). ## Named exports In addition to the default export (the `DailyIframe` class), the package exports a set of named constants you can use for comparisons: ```javascript theme={null} Daily, { // Meeting state constants DAILY_STATE_NEW, DAILY_STATE_JOINING, DAILY_STATE_JOINED, DAILY_STATE_LEFT, DAILY_STATE_ERROR, // Track state constants DAILY_TRACK_STATE_BLOCKED, DAILY_TRACK_STATE_OFF, DAILY_TRACK_STATE_SENDABLE, DAILY_TRACK_STATE_LOADING, DAILY_TRACK_STATE_INTERRUPTED, DAILY_TRACK_STATE_PLAYABLE, // Access level constants DAILY_ACCESS_UNKNOWN, DAILY_ACCESS_LEVEL_FULL, DAILY_ACCESS_LEVEL_LOBBY, DAILY_ACCESS_LEVEL_NONE, // Receive settings special keys DAILY_RECEIVE_SETTINGS_BASE_KEY, DAILY_RECEIVE_SETTINGS_ALL_PARTICIPANTS_KEY, // Fatal error type constants DAILY_FATAL_ERROR_EJECTED, DAILY_FATAL_ERROR_NBF_ROOM, DAILY_FATAL_ERROR_NBF_TOKEN, DAILY_FATAL_ERROR_EXP_ROOM, DAILY_FATAL_ERROR_EXP_TOKEN, DAILY_FATAL_ERROR_NO_ROOM, DAILY_FATAL_ERROR_MEETING_FULL, DAILY_FATAL_ERROR_NOT_ALLOWED, DAILY_FATAL_ERROR_CONNECTION, DAILY_FATAL_ERROR_EOL, // Camera error type constants DAILY_CAMERA_ERROR_CAM_IN_USE, DAILY_CAMERA_ERROR_MIC_IN_USE, DAILY_CAMERA_ERROR_CAM_AND_MIC_IN_USE, DAILY_CAMERA_ERROR_PERMISSIONS, DAILY_CAMERA_ERROR_UNDEF_MEDIADEVICES, DAILY_CAMERA_ERROR_NOT_FOUND, DAILY_CAMERA_ERROR_CONSTRAINTS, DAILY_CAMERA_ERROR_UNKNOWN, } from '@daily-co/daily-js'; ``` These constants match the string literal values used in the `DailyMeetingState`, `DailyFatalErrorType`, and `DailyCameraErrorType` types. Using constants instead of raw strings helps avoid typos: ```javascript theme={null} call.on('error', ({ error }) => { if (error?.type === DAILY_FATAL_ERROR_EJECTED) { showMessage('You were removed from the call.'); } }); if (call.meetingState() === DAILY_STATE_JOINED) { // safe to interact with the call } ``` ## Verifying the installation After installing, run the following snippet in your browser console or application to confirm that daily-js loaded correctly: ```javascript theme={null} import Daily from '@daily-co/daily-js'; console.log('daily-js version:', Daily.version()); // → "0.89.1" const { supported, name, version } = Daily.supportedBrowser(); console.log(`Browser: ${name} ${version}, supported: ${supported}`); ``` # Introduction Source: https://docs.daily.co/docs/daily-js/introduction daily-js is the front-end JavaScript SDK for adding real-time video and audio calls to your web applications. ## What is daily-js? `daily-js` (`@daily-co/daily-js`) is Daily's JavaScript library that lets you embed WebRTC-based video and audio calls into any web application. It handles all the complexity of WebRTC — signaling, media negotiation, network traversal, device handling,and more — so you can focus on building your product. ## Key features Track participants, control audio/video, manage permissions, and handle waiting rooms Error handling, automatic device switching, and permission management — abstracted across browsers with their differing specs, bugs, and quirks Cloud and local recording with custom layout presets and compositing Stream to any RTMP endpoint with multiple layout options Real-time transcription powered by Deepgram with language and model selection Background blur and virtual backgrounds via Banuba, plus AI noise cancellation via Krisp Send custom audio and video tracks alongside camera/mic streams Monitor network quality and run pre-call connectivity tests ## Integration modes daily-js supports two primary ways to integrate video calls into your app: ### Iframe mode (Prebuilt UI) Daily manages a full-featured call UI inside an `