Repository navigation
Seed the E2EE sender cryptor with the publish codec #2131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| 'livekit-client': patch | ||
| --- | ||
|
|
||
| Seed the E2EE sender cryptor with the publish codec for h264 and h265. When a non-slice NALU comes before the slice, read an h264 frame as h264. If NALU processing falls back to VP8 handling, log the first bytes of the frame. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -620,11 +620,15 @@ export class E2EEManager | |
| if (!sender) this.log.warn('early return because sender is not ready'); | ||
| return; | ||
| } | ||
| const isVideo = isVideoTrack(track); | ||
| // Safari exposes no per-frame codec info until trackInfo arrives. Seed only h264/h265: | ||
| // a wrong vp8 seed would skip the NALU path, a wrong NALU seed only costs a logged fallback. | ||
| const publishedCodec = isVideo ? track.publishOptions?.videoCodec : undefined; | ||
| this.handleSender( | ||
| sender, | ||
| track.mediaStreamID, | ||
| undefined, | ||
| isVideoTrack(track) | ||
| publishedCodec === 'h264' || publishedCodec === 'h265' ? publishedCodec : undefined, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question: I'm not sure if I understand why this has to only be h264 / h265 - based on the issue description, wouldn't be ok to be able to pass in any codec here and let downstream code fall back to this always if
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The seed never reaches The value available here is |
||
| isVideo | ||
| ? (track.publishOptions?.frameMetadata ?? track.publishOptions?.packetTrailer) | ||
| : undefined, | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { processNALUsForEncryption } from './naluUtils'; | ||
|
|
||
| /** Annex-B frame: a 4-byte start code before every NALU. */ | ||
| function annexB(...nalus: number[][]) { | ||
| return Uint8Array.from(nalus.flatMap((nalu) => [0x00, 0x00, 0x00, 0x01, ...nalu])); | ||
| } | ||
|
|
||
| // h264: 0x06 SEI, 0x65 IDR slice, 0x67 SPS. h265: 0x26 IDR_W_RADL slice. | ||
| // An h264 SEI byte (0x06) reads as h265 TSA_R, which is why the SEI leads here. | ||
| const h264KeyFrame = annexB([0x06, 0x11, 0x22], [0x65, 0x33, 0x44, 0x55, 0x66]); | ||
| const h265KeyFrame = annexB([0x26, 0x11, 0x22], [0x02, 0x33, 0x44, 0x55, 0x66]); | ||
| const noSliceFrame = annexB([0x67, 0x11, 0x22]); | ||
|
|
||
| describe('processNALUsForEncryption', () => { | ||
| it('slices at the h264 slice NALU even when seeded with h265', () => { | ||
| // The whole point: the h265 seed must not win, or the offset lands at 6 (inside the | ||
| // SEI) and the slice header gets encrypted with no error raised. | ||
| expect(processNALUsForEncryption(h264KeyFrame, 'h265')).toEqual({ | ||
| unencryptedBytes: 13, | ||
| detectedCodec: 'h264', | ||
| requiresNALUProcessing: true, | ||
| }); | ||
| }); | ||
|
|
||
| it('reads an h264 frame whose SEI precedes the slice as h264', () => { | ||
| expect(processNALUsForEncryption(h264KeyFrame)).toEqual({ | ||
| unencryptedBytes: 13, | ||
| detectedCodec: 'h264', | ||
| requiresNALUProcessing: true, | ||
| }); | ||
| }); | ||
|
|
||
| it('still reads an h265 frame as h265', () => { | ||
| expect(processNALUsForEncryption(h265KeyFrame)).toEqual({ | ||
| unencryptedBytes: 6, | ||
| detectedCodec: 'h265', | ||
| requiresNALUProcessing: true, | ||
| }); | ||
| expect(processNALUsForEncryption(h265KeyFrame, 'h264')).toEqual({ | ||
| unencryptedBytes: 6, | ||
| detectedCodec: 'h265', | ||
| requiresNALUProcessing: true, | ||
| }); | ||
| }); | ||
|
|
||
| it('throws with a seed, and returns quietly without one, when no slice NALU is present', () => { | ||
| expect(() => processNALUsForEncryption(noSliceFrame, 'h264')).toThrow('Could not find NALU'); | ||
| expect(processNALUsForEncryption(noSliceFrame)).toEqual({ | ||
| unencryptedBytes: 0, | ||
| detectedCodec: 'unknown', | ||
| requiresNALUProcessing: false, | ||
| }); | ||
| }); | ||
| }); |
Uh oh!
There was an error while loading. Please reload this page.