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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,13 @@ reconnect screen instead, for up to the time you pick (30 s to 5 min).
gets its own silence, and OBS reconnecting is handed the same Twitch
session, so the destination carries on instead of restarting. Twitch
VOD-audio sessions are kept the same way.
- **HEVC and AV1 hold the last frame.** The screen is H.264, so those
tracks re-send their last keyframe once a second instead.
- **2K (HEVC) channels get the screen too.** Twitch streams Partners and
Affiliates at 1440p in HEVC under Enhanced Broadcasting, so the screen
is encoded in HEVC for HEVC tracks, at each track's own resolution and
as light as the H.264 one. The log names each track's codec when it
goes on air, for example `reconnect screen on air (2560x1440 HEVC,
1920x1080 HEVC)`. 10-bit HEVC and AV1 hold the last frame instead,
re-sent once a second.
- **Stopping in OBS never opens the screen,** even while it is up for a
frozen OBS: the stream ends once your delay has aired. Only a drop
without a goodbye opens it. **Protect every disconnect** (under
Expand Down Expand Up @@ -486,6 +491,10 @@ stream settings: h264_texture_amf, 6000 kbps`.

### Fixes

- **Integrations: a clear error when OBS wants a WebSocket password you
haven't saved.** It used to try an empty one, and OBS closed the
connection with an unclear code. The same goes for a malformed OBS
greeting that asks for a password without saying how.
- **Stopping in OBS no longer cuts the end of a delayed stream.** With a
delay on and no crash protection hold (protection off, or a stop it
lets through), the last seconds of the delay used to vanish: viewers
Expand Down
278 changes: 190 additions & 88 deletions src/crash_hold.rs

Large diffs are not rendered by default.

128 changes: 128 additions & 0 deletions src/h264.rs
Original file line number Diff line number Diff line change
Expand Up @@ -667,6 +667,98 @@ pub fn sps_dimensions(seq_header: &[u8]) -> Option<(u32, u32)> {
Some((width, height))
}

/// Picture size of an 8-bit 4:2:0 HEVC stream with 4-byte NAL lengths,
/// read from its HEVCDecoderConfigurationRecord (hvcC) with the
/// conformance window applied: the HEVC the reconnect screen can be
/// encoded for. `None` for any other HEVC, or a malformed or truncated
/// record. Never panics.
pub fn hevc_8bit_420_dimensions(hvcc: &[u8]) -> Option<(u32, u32)> {
const CHROMA_420: u32 = 1;
// The record repeats the SPS's chroma format and bit depths in
// bytes 16..19, and its NAL length size in byte 21.
let record_is_8bit_420 = u32::from(hvcc.get(16)? & 0x03) == CHROMA_420
&& hvcc.get(17)? & 0x07 == 0
&& hvcc.get(18)? & 0x07 == 0;
if !record_is_8bit_420 || hvcc.get(21)? & 0x03 != 3 {
return None;
}
// Drop the 2-byte NAL header, as `sps_dimensions` drops H.264's one.
let rbsp = strip_emulation_prevention(hevc_first_sps(hvcc)?.get(2..)?);
let mut r = BitReader::new(&rbsp);
let _vps_id = r.read_bits(4)?;
let max_sub_layers_minus1 = r.read_bits(3)?;
let _temporal_id_nesting = r.read_bit()?;
skip_hevc_profile_tier_level(&mut r, max_sub_layers_minus1)?;
let _sps_id = r.read_ue()?;
if r.read_ue()? != CHROMA_420 {
return None;
}
let mut width = r.read_ue()?;
let mut height = r.read_ue()?;
if r.read_bit()? == 1 {
// conformance_window offsets count chroma samples: 2 luma in 4:2:0.
let crop = |a: u32, b: u32| a.checked_add(b)?.checked_mul(2);
let (left, right, top, bottom) = (r.read_ue()?, r.read_ue()?, r.read_ue()?, r.read_ue()?);
width = width.checked_sub(crop(left, right)?)?;
height = height.checked_sub(crop(top, bottom)?)?;
}
let is_8bit = r.read_ue()? == 0 && r.read_ue()? == 0;
(is_8bit && width > 0 && height > 0).then_some((width, height))
}

/// The first SPS NAL unit (header included) in an hvcC's arrays.
fn hevc_first_sps(hvcc: &[u8]) -> Option<&[u8]> {
const NAL_SPS: u8 = 33;
let read_u16 = |at: usize| Some(u16::from_be_bytes([*hvcc.get(at)?, *hvcc.get(at + 1)?]));
let array_count = *hvcc.get(22)?;
let mut at = 23;
for _ in 0..array_count {
let nal_type = hvcc.get(at)? & 0x3F;
let nal_count = read_u16(at + 1)?;
at += 3;
for _ in 0..nal_count {
let len = read_u16(at)? as usize;
let nal = hvcc.get(at + 2..at + 2 + len)?;
if nal_type == NAL_SPS {
return Some(nal);
}
at += 2 + len;
}
}
None
}

/// Skip profile_tier_level(1, max_sub_layers_minus1) (H.265 7.3.3).
fn skip_hevc_profile_tier_level(r: &mut BitReader, max_sub_layers_minus1: u32) -> Option<()> {
// general_profile_space through general_level_idc: 96 bits.
for _ in 0..3 {
r.read_bits(32)?;
}
let mut present = Vec::new();
for _ in 0..max_sub_layers_minus1 {
let profile = r.read_bit()? == 1;
let level = r.read_bit()? == 1;
present.push((profile, level));
}
if max_sub_layers_minus1 > 0 {
for _ in max_sub_layers_minus1..8 {
r.read_bits(2)?; // reserved_zero_2bits
}
}
for (profile, level) in present {
if profile {
// sub_layer profile fields: 88 bits.
r.read_bits(32)?;
r.read_bits(32)?;
r.read_bits(24)?;
}
if level {
r.read_bits(8)?;
}
}
Some(())
}

/// Best-effort codec of a sequence-header tag, across the framings OBS
/// emits (legacy AVC, Enhanced single-track, Enhanced OneTrack multi-track).
/// Used for the per-destination "1080x1920 · H.264" readout. Returns
Expand Down Expand Up @@ -1776,6 +1868,42 @@ mod tests {
assert_eq!(sps_dimensions(&hevc), None);
}

/// Read back the size from the reconnect screen's own HEVC parameter
/// sets, cropping included (1080 and 360 aren't whole CTBs).
#[test]
fn hevc_dimensions_read_the_visible_size() {
for (width, height) in [(1920, 1080), (1080, 1920), (2560, 1440), (200, 360)] {
let config = crate::slate::test_hevc_config(width, height);
assert_eq!(
hevc_8bit_420_dimensions(&config),
Some((width as u32, height as u32)),
"{width}x{height}"
);
}
}

/// 10-bit, 4:4:4 or 2-byte NAL lengths are HEVC the screen can't stand
/// in for; every truncation of a good record fails cleanly.
#[test]
fn hevc_dimensions_refuse_what_the_screen_cannot_encode() {
let config = crate::slate::test_hevc_config(1920, 1080);
for (byte, value) in [(17, 0xFA), (18, 0xFA), (16, 0xFF), (21, 0x0D)] {
let mut other = config.clone();
other[byte] = value;
assert_eq!(
hevc_8bit_420_dimensions(&other),
None,
"byte {byte} = {value:#x}"
);
}
// Any truncation returns without panicking (these are wire bytes),
// and one cut before the SPS's size fields reads nothing.
for len in 0..config.len() {
let _ = hevc_8bit_420_dimensions(&config[..len]);
}
assert_eq!(hevc_8bit_420_dimensions(&config[..30]), None);
}

/// The SPS fields that change how many bits sit in front of the picture
/// size. Everything else is fixed by `build_rich_sps_rbsp`.
struct SpsFields {
Expand Down
39 changes: 33 additions & 6 deletions src/slate/bitstream.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//! H.264 bit-level writing: fixed-width and Exp-Golomb fields, RBSP
//! trailing bits, and NAL unit framing with emulation prevention.
//! H.264 and HEVC bit-level writing: fixed-width and Exp-Golomb fields,
//! RBSP trailing bits, and NAL unit framing with emulation prevention.

/// MSB-first bit writer for one RBSP.
#[derive(Default)]
Expand Down Expand Up @@ -69,6 +69,12 @@ impl BitWriter {
/// Close the RBSP with rbsp_stop_one_bit and alignment zeros.
pub fn finish(mut self) -> Vec<u8> {
self.bit(true);
self.into_aligned_bytes()
}

/// Pad with zeros to a byte boundary and return the bytes, for an RBSP
/// whose stop bit is already written (a CABAC flush writes its own).
pub fn into_aligned_bytes(mut self) -> Vec<u8> {
self.align_with_zeros();
self.bytes
}
Expand All @@ -79,11 +85,28 @@ pub const NAL_IDR_SLICE: u8 = 5;
pub const NAL_SPS: u8 = 7;
pub const NAL_PPS: u8 = 8;

/// NAL header plus the RBSP with emulation-prevention bytes inserted, so
/// no 00 00 0x (x <= 3) start-code pattern appears inside the payload.
pub const HEVC_NAL_TRAIL_R: u8 = 1;
pub const HEVC_NAL_IDR_N_LP: u8 = 20;
pub const HEVC_NAL_VPS: u8 = 32;
pub const HEVC_NAL_SPS: u8 = 33;
pub const HEVC_NAL_PPS: u8 = 34;

/// H.264 NAL unit: header byte plus the escaped RBSP.
pub fn nal_unit(nal_ref_idc: u8, nal_type: u8, rbsp: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(rbsp.len() + rbsp.len() / 64 + 1);
out.push((nal_ref_idc << 5) | nal_type);
escaped(&[(nal_ref_idc << 5) | nal_type], rbsp)
}

/// HEVC NAL unit: two header bytes (layer 0, temporal id 0) plus the
/// escaped RBSP.
pub fn hevc_nal_unit(nal_type: u8, rbsp: &[u8]) -> Vec<u8> {
escaped(&[nal_type << 1, 1], rbsp)
}

/// `header` plus the RBSP with emulation-prevention bytes inserted, so no
/// 00 00 0x (x <= 3) start-code pattern appears inside the payload.
fn escaped(header: &[u8], rbsp: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(header.len() + rbsp.len() + rbsp.len() / 64 + 1);
out.extend_from_slice(header);
let mut zeros = 0;
for &byte in rbsp {
if zeros >= 2 && byte <= 3 {
Expand Down Expand Up @@ -145,5 +168,9 @@ mod tests {
vec![0x67, 0, 0, 3, 1, 0, 0, 3, 0, 0]
);
assert_eq!(nal_unit(0, NAL_SLICE, &[0, 0, 4]), vec![0x01, 0, 0, 4]);
assert_eq!(
hevc_nal_unit(HEVC_NAL_SPS, &[0, 0, 2]),
vec![0x42, 0x01, 0, 0, 3, 2]
);
}
}
Loading
Loading