diff --git a/.github/workflows/react-native-workflow.yml b/.github/workflows/react-native-workflow.yml index 1f823b281c..c7ae241ec6 100644 --- a/.github/workflows/react-native-workflow.yml +++ b/.github/workflows/react-native-workflow.yml @@ -130,7 +130,7 @@ jobs: name: Deploy iOS needs: build_ios timeout-minutes: 60 - if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/PBE-5855-feat/react-native-video-design-v2' }} + if: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/use-vp8-on-ios' }} runs-on: macos-latest steps: - uses: actions/checkout@v4 diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md index 1d1eb419c9..74a9eaab38 100644 --- a/packages/client/CHANGELOG.md +++ b/packages/client/CHANGELOG.md @@ -2,6 +2,13 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [1.11.8](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.7...@stream-io/video-client-1.11.8) (2024-11-27) + + +### Bug Fixes + +* **ios:** use vp8 when h264 constrainted baseline isn't available ([#1597](https://github.com/GetStream/stream-video-js/issues/1597)) ([6281216](https://github.com/GetStream/stream-video-js/commit/62812161cef5e9917c504dbc4cd9257709ea5fa1)) + ## [1.11.7](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-client-1.11.6...@stream-io/video-client-1.11.7) (2024-11-26) diff --git a/packages/client/package.json b/packages/client/package.json index 74d19ed7d0..1313d19378 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@stream-io/video-client", - "version": "1.11.7", + "version": "1.11.8", "packageManager": "yarn@3.2.4", "main": "dist/index.cjs.js", "module": "dist/index.es.js", diff --git a/packages/client/src/helpers/__tests__/sdp-munging.test.ts b/packages/client/src/helpers/__tests__/sdp-munging.test.ts index 922f97f1ba..fcd5017d8e 100644 --- a/packages/client/src/helpers/__tests__/sdp-munging.test.ts +++ b/packages/client/src/helpers/__tests__/sdp-munging.test.ts @@ -188,4 +188,96 @@ a=simulcast:send q;h;f`; expect(target).not.toContain('VP9'); expect(target).not.toContain('AV1'); }); + + it('works with iOS RN vp8', () => { + const sdp = `v=0 +o=- 2055959380019004946 2 IN IP4 127.0.0.1 +s=- +t=0 0 +a=group:BUNDLE 0 +a=extmap-allow-mixed +a=msid-semantic: WMS FE2B3B06-61D7-4ACC-A4EF-76441C116E47 +m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 127 103 35 36 104 105 106 +c=IN IP4 0.0.0.0 +a=rtcp:9 IN IP4 0.0.0.0 +a=ice-ufrag:gCgh +a=ice-pwd:bz18EOLBL9+kSJfLiVOyU4RP +a=ice-options:trickle renomination +a=fingerprint:sha-256 6B:04:36:6D:E6:92:B5:68:DA:30:CF:53:46:14:49:5B:48:3E:B9:F7:06:B4:E8:85:B1:8C:B3:1C:EB:E8:F8:16 +a=setup:actpass +a=mid:0 +a=extmap:1 urn:ietf:params:rtp-hdrext:toffset +a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time +a=extmap:3 urn:3gpp:video-orientation +a=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01 +a=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay +a=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type +a=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing +a=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space +a=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid +a=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id +a=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id +a=extmap:12 https://aomediacodec.github.io/av1-rtp-spec/#dependency-descriptor-rtp-header-extension +a=extmap:14 http://www.webrtc.org/experiments/rtp-hdrext/video-layers-allocation00 +a=sendonly +a=msid:FE2B3B06-61D7-4ACC-A4EF-76441C116E47 93FCE555-1DA2-4721-901C-5D263E11DF23 +a=rtcp-mux +a=rtcp-rsize +a=rtpmap:96 H264/90000 +a=rtcp-fb:96 goog-remb +a=rtcp-fb:96 transport-cc +a=rtcp-fb:96 ccm fir +a=rtcp-fb:96 nack +a=rtcp-fb:96 nack pli +a=fmtp:96 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c29 +a=rtpmap:97 rtx/90000 +a=fmtp:97 apt=96 +a=rtpmap:98 H264/90000 +a=rtcp-fb:98 goog-remb +a=rtcp-fb:98 transport-cc +a=rtcp-fb:98 ccm fir +a=rtcp-fb:98 nack +a=rtcp-fb:98 nack pli +a=fmtp:98 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e029 +a=rtpmap:99 rtx/90000 +a=fmtp:99 apt=98 +a=rtpmap:100 VP8/90000 +a=rtcp-fb:100 goog-remb +a=rtcp-fb:100 transport-cc +a=rtcp-fb:100 ccm fir +a=rtcp-fb:100 nack +a=rtcp-fb:100 nack pli +a=rtpmap:101 rtx/90000 +a=fmtp:101 apt=100 +a=rtpmap:127 VP9/90000 +a=rtcp-fb:127 goog-remb +a=rtcp-fb:127 transport-cc +a=rtcp-fb:127 ccm fir +a=rtcp-fb:127 nack +a=rtcp-fb:127 nack pli +a=rtpmap:103 rtx/90000 +a=fmtp:103 apt=127 +a=rtpmap:35 AV1/90000 +a=rtcp-fb:35 goog-remb +a=rtcp-fb:35 transport-cc +a=rtcp-fb:35 ccm fir +a=rtcp-fb:35 nack +a=rtcp-fb:35 nack pli +a=rtpmap:36 rtx/90000 +a=fmtp:36 apt=35 +a=rtpmap:104 red/90000 +a=rtpmap:105 rtx/90000 +a=fmtp:105 apt=104 +a=rtpmap:106 ulpfec/90000 +a=rid:q send +a=rid:h send +a=rid:f send +a=simulcast:send q;h;f`; + const target = preserveCodec(sdp, '0', { + clockRate: 90000, + mimeType: 'video/VP8', + }); + expect(target).toContain('VP8'); + expect(target).not.toContain('VP9'); + }); }); diff --git a/packages/client/src/helpers/sdp-munging.ts b/packages/client/src/helpers/sdp-munging.ts index 317c1a8e7a..656e197a43 100644 --- a/packages/client/src/helpers/sdp-munging.ts +++ b/packages/client/src/helpers/sdp-munging.ts @@ -156,12 +156,16 @@ export const preserveCodec = ( // find the payload id of the desired codec const payloads = new Set(); for (const rtp of media.rtp) { - if ( - rtp.codec.toLowerCase() === codecName && - media.fmtp.some( - (f) => f.payload === rtp.payload && equal(toSet(f.config), codecFmtp), - ) - ) { + if (rtp.codec.toLowerCase() !== codecName) continue; + const match = + // vp8 doesn't have any fmtp, we preserve it without any additional checks + codecName === 'vp8' + ? true + : media.fmtp.some( + (f) => + f.payload === rtp.payload && equal(toSet(f.config), codecFmtp), + ); + if (match) { payloads.add(rtp.payload); } } diff --git a/packages/client/src/rtc/Publisher.ts b/packages/client/src/rtc/Publisher.ts index 15d51192b5..f13feb1918 100644 --- a/packages/client/src/rtc/Publisher.ts +++ b/packages/client/src/rtc/Publisher.ts @@ -29,6 +29,8 @@ import { Dispatcher } from './Dispatcher'; import { VideoLayerSetting } from '../gen/video/sfu/event/events'; import { TargetResolutionResponse } from '../gen/shims'; import { withoutConcurrency } from '../helpers/concurrency'; +import { isReactNative } from '../helpers/platforms'; +import { isFirefox } from '../helpers/browsers'; export type PublisherConstructorOpts = { sfuClient: StreamSfuClient; @@ -256,6 +258,7 @@ export class Publisher { const codecPreferences = this.getCodecPreferences( trackType, trackType === TrackType.VIDEO ? codecInUse : undefined, + 'receiver', ); if (!codecPreferences) return; @@ -458,13 +461,14 @@ export class Publisher { private getCodecPreferences = ( trackType: TrackType, - preferredCodec?: string, - codecPreferencesSource?: 'sender' | 'receiver', + preferredCodec: string | undefined, + codecPreferencesSource: 'sender' | 'receiver', ) => { if (trackType === TrackType.VIDEO) { return getPreferredCodecs( 'video', preferredCodec || 'vp8', + undefined, codecPreferencesSource, ); } @@ -475,6 +479,7 @@ export class Publisher { 'audio', preferredCodec ?? defaultAudioCodec, codecToRemove, + codecPreferencesSource, ); } }; @@ -578,7 +583,9 @@ export class Publisher { private removeUnpreferredCodecs(sdp: string, trackType: TrackType): string { const opts = this.publishOptsForTrack.get(trackType); - if (!opts || !opts.forceSingleCodec) return sdp; + const forceSingleCodec = + !!opts?.forceSingleCodec || isReactNative() || isFirefox(); + if (!opts || !forceSingleCodec) return sdp; const codec = opts.forceCodec || getOptimalVideoCodec(opts.preferredCodec); const orderedCodecs = this.getCodecPreferences(trackType, codec, 'sender'); diff --git a/packages/client/src/rtc/__tests__/codecs.test.ts b/packages/client/src/rtc/__tests__/codecs.test.ts index afa8a1be94..2154276d58 100644 --- a/packages/client/src/rtc/__tests__/codecs.test.ts +++ b/packages/client/src/rtc/__tests__/codecs.test.ts @@ -5,7 +5,7 @@ import './mocks/webrtc.mocks'; describe('codecs', () => { it('should return preferred audio codec', () => { RTCRtpReceiver.getCapabilities = vi.fn().mockReturnValue(audioCodecs); - const codecs = getPreferredCodecs('audio', 'red'); + const codecs = getPreferredCodecs('audio', 'red', undefined, 'receiver'); expect(codecs).toBeDefined(); expect(codecs?.map((c) => c.mimeType)).toEqual([ 'audio/red', @@ -20,7 +20,7 @@ describe('codecs', () => { it('should return preferred video codec', () => { RTCRtpReceiver.getCapabilities = vi.fn().mockReturnValue(videoCodecs); - const codecs = getPreferredCodecs('video', 'vp8'); + const codecs = getPreferredCodecs('video', 'vp8', undefined, 'receiver'); expect(codecs).toBeDefined(); // prettier-ignore expect(codecs?.map((c) => [c.mimeType, c.sdpFmtpLine])).toEqual([ @@ -40,12 +40,12 @@ describe('codecs', () => { it('should pick the baseline H264 codec', () => { RTCRtpReceiver.getCapabilities = vi.fn().mockReturnValue(videoCodecs); - const codecs = getPreferredCodecs('video', 'h264'); + const codecs = getPreferredCodecs('video', 'h264', undefined, 'receiver'); expect(codecs).toBeDefined(); // prettier-ignore expect(codecs?.map((c) => [c.mimeType, c.sdpFmtpLine])).toEqual([ - ['video/H264', 'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f'], ['video/H264', 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f'], + ['video/H264', 'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f'], ['video/H264', 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f'], ['video/H264', 'level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=640c1f'], ['video/rtx', undefined], @@ -62,12 +62,12 @@ describe('codecs', () => { RTCRtpReceiver.getCapabilities = vi .fn() .mockReturnValue(videoCodecsFirefox); - const codecs = getPreferredCodecs('video', 'h264'); + const codecs = getPreferredCodecs('video', 'h264', undefined, 'receiver'); expect(codecs).toBeDefined(); // prettier-ignore expect(codecs?.map((c) => [c.mimeType, c.sdpFmtpLine])).toEqual([ - ['video/H264', 'profile-level-id=42e01f;level-asymmetry-allowed=1'], ['video/H264', 'profile-level-id=42e01f;level-asymmetry-allowed=1;packetization-mode=1'], + ['video/H264', 'profile-level-id=42e01f;level-asymmetry-allowed=1'], ['video/VP8', 'max-fs=12288;max-fr=60'], ['video/rtx', undefined], ['video/VP9', 'max-fs=12288;max-fr=60'], diff --git a/packages/client/src/rtc/codecs.ts b/packages/client/src/rtc/codecs.ts index cda7cefd58..e1c769bbb2 100644 --- a/packages/client/src/rtc/codecs.ts +++ b/packages/client/src/rtc/codecs.ts @@ -14,8 +14,8 @@ import type { PreferredCodec } from '../types'; export const getPreferredCodecs = ( kind: 'audio' | 'video', preferredCodec: string, - codecToRemove?: string, - codecPreferencesSource: 'sender' | 'receiver' = 'receiver', + codecToRemove: string | undefined, + codecPreferencesSource: 'sender' | 'receiver', ): RTCRtpCodec[] | undefined => { const source = codecPreferencesSource === 'receiver' ? RTCRtpReceiver : RTCRtpSender; @@ -60,12 +60,7 @@ export const getPreferredCodecs = ( continue; } - // packetization-mode mode is optional; when not present it defaults to 0: - // https://datatracker.ietf.org/doc/html/rfc6184#section-6.2 - if ( - sdpFmtpLine.includes('packetization-mode=0') || - !sdpFmtpLine.includes('packetization-mode') - ) { + if (sdpFmtpLine.includes('packetization-mode=1')) { preferred.unshift(codec); } else { preferred.push(codec); @@ -107,7 +102,9 @@ export const getOptimalVideoCodec = ( if (isReactNative()) { const os = getOSInfo()?.name.toLowerCase(); if (os === 'android') return preferredOr(preferredCodec, 'vp8'); - if (os === 'ios' || os === 'ipados') return 'h264'; + if (os === 'ios' || os === 'ipados') { + return supportsH264Baseline() ? 'h264' : 'vp8'; + } return preferredOr(preferredCodec, 'h264'); } if (isSafari()) return 'h264'; @@ -139,6 +136,20 @@ const preferredOr = ( : fallback; }; +/** + * Returns whether the platform supports the H264 baseline codec. + */ +const supportsH264Baseline = (): boolean => { + if (!('getCapabilities' in RTCRtpSender)) return false; + const capabilities = RTCRtpSender.getCapabilities('video'); + if (!capabilities) return false; + return capabilities.codecs.some( + (c) => + c.mimeType.toLowerCase() === 'video/h264' && + c.sdpFmtpLine?.includes('profile-level-id=42e01f'), + ); +}; + /** * Returns whether the codec is an SVC codec. * diff --git a/packages/react-bindings/CHANGELOG.md b/packages/react-bindings/CHANGELOG.md index f6cca80b5e..9dd6c524b5 100644 --- a/packages/react-bindings/CHANGELOG.md +++ b/packages/react-bindings/CHANGELOG.md @@ -2,6 +2,11 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [1.2.2](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-bindings-1.2.1...@stream-io/video-react-bindings-1.2.2) (2024-11-27) + +### Dependency Updates + +* `@stream-io/video-client` updated to version `1.11.8` ## [1.2.1](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-bindings-1.2.0...@stream-io/video-react-bindings-1.2.1) (2024-11-26) ### Dependency Updates diff --git a/packages/react-bindings/package.json b/packages/react-bindings/package.json index ddf884c113..e0cb6c397f 100644 --- a/packages/react-bindings/package.json +++ b/packages/react-bindings/package.json @@ -1,6 +1,6 @@ { "name": "@stream-io/video-react-bindings", - "version": "1.2.1", + "version": "1.2.2", "packageManager": "yarn@3.2.4", "main": "./dist/index.cjs.js", "module": "./dist/index.es.js", diff --git a/packages/react-native-sdk/CHANGELOG.md b/packages/react-native-sdk/CHANGELOG.md index 4a718fa332..7c4f8c835a 100644 --- a/packages/react-native-sdk/CHANGELOG.md +++ b/packages/react-native-sdk/CHANGELOG.md @@ -2,6 +2,12 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [1.4.3](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-native-sdk-1.4.2...@stream-io/video-react-native-sdk-1.4.3) (2024-11-27) + +### Dependency Updates + +* `@stream-io/video-client` updated to version `1.11.8` +* `@stream-io/video-react-bindings` updated to version `1.2.2` ## [1.4.2](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-native-sdk-1.4.1...@stream-io/video-react-native-sdk-1.4.2) (2024-11-27) diff --git a/packages/react-native-sdk/package.json b/packages/react-native-sdk/package.json index 63b2ade071..899d92894e 100644 --- a/packages/react-native-sdk/package.json +++ b/packages/react-native-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@stream-io/video-react-native-sdk", - "version": "1.4.2", + "version": "1.4.3", "packageManager": "yarn@3.2.4", "main": "dist/commonjs/index.js", "module": "dist/module/index.js", diff --git a/packages/react-sdk/CHANGELOG.md b/packages/react-sdk/CHANGELOG.md index 470382ead0..fd24b276e6 100644 --- a/packages/react-sdk/CHANGELOG.md +++ b/packages/react-sdk/CHANGELOG.md @@ -2,6 +2,12 @@ This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). +## [1.7.24](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-sdk-1.7.23...@stream-io/video-react-sdk-1.7.24) (2024-11-27) + +### Dependency Updates + +* `@stream-io/video-client` updated to version `1.11.8` +* `@stream-io/video-react-bindings` updated to version `1.2.2` ## [1.7.23](https://github.com/GetStream/stream-video-js/compare/@stream-io/video-react-sdk-1.7.22...@stream-io/video-react-sdk-1.7.23) (2024-11-26) ### Dependency Updates diff --git a/packages/react-sdk/package.json b/packages/react-sdk/package.json index 5faa6603f8..216ca152f4 100644 --- a/packages/react-sdk/package.json +++ b/packages/react-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@stream-io/video-react-sdk", - "version": "1.7.23", + "version": "1.7.24", "packageManager": "yarn@3.2.4", "main": "./dist/index.cjs.js", "module": "./dist/index.es.js", diff --git a/sample-apps/react-native/dogfood/package.json b/sample-apps/react-native/dogfood/package.json index dbd3047b1c..53c007c621 100644 --- a/sample-apps/react-native/dogfood/package.json +++ b/sample-apps/react-native/dogfood/package.json @@ -1,6 +1,6 @@ { "name": "@stream-io/video-react-native-dogfood", - "version": "4.8.1", + "version": "4.8.2", "private": true, "scripts": { "android": "react-native run-android", diff --git a/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx b/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx index 5b42b6cc6e..406067da11 100644 --- a/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx +++ b/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx @@ -61,7 +61,7 @@ export const MeetingUI = ({ callId, navigation, route }: Props) => { try { call?.updatePublishOptions({ preferredCodec: 'vp9', - preferredBitrate: 1500000, + forceSingleCodec: true, }); await call?.join({ create: true }); appStoreSetState({ chatLabelNoted: false });