Skip to main content

zvidlib_container/
webm.rs

1//! Deterministic seekable WebM muxing.
2//!
3//! [`WebmMuxer`] writes the same guarantees [`crate::mp4::Mp4Muxer`] does:
4//! payload bytes go to the sink as each sample arrives, and `finish` seeks
5//! back to fill in what only the end of the stream knows - the Segment and
6//! Cluster sizes, the `Duration`, and a `SeekHead` naming the `Cues` written
7//! after the last Cluster - so the file is seekable in a browser and in
8//! [`crate::WebmDemuxer`] without a remux.
9//!
10//! WebM permits only VP8, VP9 and AV1 video and Vorbis or Opus audio. zvidlib
11//! encodes VP8, VP9 and AV1 video and Opus and Vorbis audio, so those are what
12//! it writes: an AAC track is refused rather than written into a file no WebM
13//! player would accept.
14//!
15//! An audio track's encoder delay is written as its `CodecDelay` and its end
16//! padding as its last block's `DiscardPadding`, so a reader trims the decoded
17//! stream to exactly the samples that were encoded. The padding is only known
18//! once the encoder drains, after its last packets are handed over, so each
19//! audio track's newest block is held back until a later block or `finish`
20//! writes it.
21
22use crate::codec::{AudioGapless, EncodedSample, TrackKind};
23use crate::ebml::{
24    self, write_element, write_float, write_id, write_int, write_string, write_uint,
25};
26use crate::io::ByteSink;
27use crate::media::Codec;
28use crate::mp4::{Mp4TrackConfig, Mp4TrackFormat};
29use crate::opus::OpusHead;
30use crate::vorbis::VorbisConfig;
31use crate::{Error, ErrorKind, Result, Vp9CodecConfig};
32
33/// One tick of every block timestamp written: a millisecond, Matroska's
34/// default and the scale every WebM player expects.
35const TIMESTAMP_SCALE_NS: u64 = 1_000_000;
36const TICKS_PER_SECOND: u64 = 1_000;
37/// Track numbers are written as one-byte block headers.
38const MAX_TRACKS: usize = 126;
39/// The Seek entries `finish` fills in, in `SeekHead` order.
40const SEEK_TARGETS: [u32; 3] = [ebml::INFO, ebml::TRACKS, ebml::CUES];
41const MUXING_APP: &str = concat!("zvidlib ", env!("CARGO_PKG_VERSION"));
42const NANOSECONDS_PER_SECOND: u64 = 1_000_000_000;
43/// How much Opus to decode ahead of a seek target, the `SeekPreRoll` the
44/// Matroska Opus mapping asks for.
45const OPUS_SEEK_PRE_ROLL_NS: u64 = 80_000_000;
46/// How long a Cluster of a file with no video runs before the next one opens.
47/// A video file opens one at each random-access sample instead.
48const AUDIO_CLUSTER_TICKS: u64 = 5_000;
49
50struct TrackState {
51    number: u64,
52    timescale: u32,
53    is_video: bool,
54    samples: usize,
55    audio: Option<AudioTrackState>,
56}
57
58struct AudioTrackState {
59    /// The decoded samples the track's `CodecDelay` declares.
60    codec_delay: u32,
61    padding: u32,
62    /// The track's newest block, not yet written.
63    held: Option<HeldBlock>,
64    /// Whether a block of the track has been written.
65    wrote_block: bool,
66    /// Whether [`WebmMuxer::set_audio_gapless`] has ended the track, so the
67    /// held block is its last.
68    complete: bool,
69}
70
71struct HeldBlock {
72    timestamp: u64,
73    data: Vec<u8>,
74    /// End of the block's samples, in the track's timescale.
75    end: u64,
76}
77
78struct OpenCluster {
79    /// Absolute offset of the Cluster element.
80    start: u64,
81    /// Absolute offset of the Cluster's payload, after its fixed-size size.
82    data_start: u64,
83    timestamp: u64,
84    blocks: usize,
85}
86
87struct CueRecord {
88    time: u64,
89    track: u64,
90    /// Relative to the Segment payload, as `CueClusterPosition` is.
91    cluster_position: u64,
92    /// Relative to the Cluster payload, as `CueRelativePosition` is.
93    relative_position: u64,
94}
95
96/// A seekable WebM muxer that writes payload bytes immediately.
97///
98/// Track declarations are the same [`Mp4TrackConfig`] values
99/// [`crate::mp4::Mp4Muxer`] takes, so one encoder configuration feeds either
100/// container. Samples from every track are written in one nondecreasing
101/// presentation-time order, because a WebM Cluster interleaves its tracks
102/// rather than storing each in a run of its own.
103pub struct WebmMuxer<S> {
104    sink: S,
105    tracks: Vec<TrackState>,
106    segment_size_position: u64,
107    segment_data_start: u64,
108    header_position: u64,
109    header_length: usize,
110    info_position: u64,
111    tracks_position: u64,
112    cluster: Option<OpenCluster>,
113    cues: Vec<CueRecord>,
114    last_timestamp: u64,
115    end_ticks: f64,
116    max_samples_per_track: usize,
117    has_video: bool,
118    finished: bool,
119}
120
121impl<S: ByteSink> WebmMuxer<S> {
122    pub async fn new(
123        mut sink: S,
124        configs: Vec<Mp4TrackConfig>,
125        max_samples_per_track: usize,
126    ) -> Result<Self> {
127        if !sink.is_seekable() {
128            return Err(Error::new(
129                ErrorKind::Unsupported,
130                "seekable WebM output requires a seekable sink",
131            ));
132        }
133        if configs.is_empty() {
134            return Err(invalid("a WebM must declare at least one track"));
135        }
136        if configs.len() > MAX_TRACKS {
137            return Err(Error::new(ErrorKind::ResourceLimit, "too many WebM tracks"));
138        }
139        if max_samples_per_track == 0 {
140            return Err(invalid("the WebM sample limit must be nonzero"));
141        }
142        for config in &configs {
143            validate_track_config(config)?;
144        }
145
146        sink.write(&ebml_header()).await?;
147        let mut segment = Vec::new();
148        write_id(&mut segment, ebml::SEGMENT);
149        let segment_size_position = sink.position() + segment.len() as u64;
150        ebml::write_vint_fixed(&mut segment, 0, 8);
151        sink.write(&segment).await?;
152        let segment_data_start = sink.position();
153
154        let header_position = sink.position();
155        let placeholder = header_region([0; 3], 0.0, true);
156        let info_offset = placeholder.info_offset;
157        sink.write(&placeholder.bytes).await?;
158        let info_position = header_position + info_offset as u64;
159        let tracks_position = sink.position();
160        sink.write(&tracks_element(&configs)?).await?;
161
162        let tracks: Vec<TrackState> = configs
163            .iter()
164            .enumerate()
165            .map(|(index, config)| {
166                Ok(TrackState {
167                    number: index as u64 + 1,
168                    timescale: config.encoder.timescale,
169                    is_video: config.kind() == TrackKind::Video,
170                    samples: 0,
171                    audio: match config.format {
172                        Mp4TrackFormat::Video(_) => None,
173                        Mp4TrackFormat::Audio { .. } => Some(AudioTrackState {
174                            codec_delay: codec_delay(config)?,
175                            padding: 0,
176                            held: None,
177                            wrote_block: false,
178                            complete: false,
179                        }),
180                    },
181                })
182            })
183            .collect::<Result<_>>()?;
184        let has_video = tracks.iter().any(|track| track.is_video);
185        Ok(Self {
186            sink,
187            tracks,
188            segment_size_position,
189            segment_data_start,
190            header_position,
191            header_length: placeholder.bytes.len(),
192            info_position,
193            tracks_position,
194            cluster: None,
195            cues: Vec::new(),
196            last_timestamp: 0,
197            end_ticks: 0.0,
198            max_samples_per_track,
199            has_video,
200            finished: false,
201        })
202    }
203
204    pub fn track_count(&self) -> usize {
205        self.tracks.len()
206    }
207
208    /// Writes one sample. A video random-access sample starts a new Cluster
209    /// and gets a cue point, so every Cluster opens on one; in a file without
210    /// video, a Cluster opens every few seconds and its first audio block is
211    /// cued instead. An audio sample is held back until a later sample or
212    /// [`Self::finish`] writes it, so its track's last block can carry the
213    /// end padding [`Self::set_audio_gapless`] declares.
214    pub async fn write_sample(&mut self, track: usize, sample: EncodedSample) -> Result<()> {
215        if self.finished {
216            return Err(invalid_state("cannot write a WebM after finish"));
217        }
218        let state = self
219            .tracks
220            .get(track)
221            .ok_or_else(|| invalid("WebM track index is out of range"))?;
222        if state.samples >= self.max_samples_per_track {
223            return Err(Error::new(
224                ErrorKind::ResourceLimit,
225                "WebM track sample limit exceeded",
226            ));
227        }
228        if state.audio.as_ref().is_some_and(|audio| audio.complete) {
229            return Err(invalid_state(
230                "a WebM audio track's gapless trim was set, which ends the track",
231            ));
232        }
233        // A Vorbis stream's first packet decodes no samples, so an audio
234        // sample may last no time at all.
235        if sample.duration == 0 && state.audio.is_none() {
236            return Err(invalid("encoded sample duration must be nonzero"));
237        }
238        if sample.data.is_empty() {
239            return Err(invalid("encoded sample data must be nonempty"));
240        }
241        let pts = u64::try_from(sample.pts)
242            .map_err(|_| invalid("negative presentation timestamps are not supported"))?;
243        let timestamp = to_ticks(pts, state.timescale)?;
244        if timestamp < self.last_timestamp {
245            return Err(invalid(
246                "WebM samples must be written in nondecreasing presentation-time order across tracks",
247            ));
248        }
249        self.flush_held(Some(timestamp)).await?;
250        let state = &mut self.tracks[track];
251        state.samples += 1;
252        let number = state.number;
253        if let Some(audio) = state.audio.as_mut() {
254            audio.held = Some(HeldBlock {
255                timestamp,
256                data: sample.data,
257                end: pts + u64::from(sample.duration),
258            });
259            self.last_timestamp = timestamp;
260            return Ok(());
261        }
262        let end = (pts as f64 + f64::from(sample.duration)) * TICKS_PER_SECOND as f64
263            / f64::from(state.timescale);
264        self.write_block(number, timestamp, &sample.data, sample.is_sync, true, None)
265            .await?;
266        self.end_ticks = self.end_ticks.max(end);
267        Ok(())
268    }
269
270    /// Declares an audio track's gapless trim and ends the track. The
271    /// priming must be the delay its codec configuration already declares - an
272    /// Opus stream's pre-skip, zero for Vorbis - and the padding is written as
273    /// the track's last block's `DiscardPadding`, so it has to be set after the
274    /// track's last sample and before a sample of another track is written past
275    /// that sample's time.
276    pub fn set_audio_gapless(&mut self, track: usize, gapless: AudioGapless) -> Result<()> {
277        let state = self
278            .tracks
279            .get_mut(track)
280            .ok_or_else(|| invalid("WebM track index is out of range"))?;
281        let audio = state
282            .audio
283            .as_mut()
284            .ok_or_else(|| invalid("gapless metadata belongs to an audio track"))?;
285        if gapless.priming != audio.codec_delay {
286            return Err(Error::new(
287                ErrorKind::Unsupported,
288                "WebM audio priming must equal the codec's own delay, which its CodecDelay declares",
289            ));
290        }
291        if gapless.padding != 0 && audio.held.is_none() && audio.wrote_block {
292            return Err(invalid_state(
293                "a WebM audio track's end padding must be set before later samples write its last block",
294            ));
295        }
296        audio.padding = gapless.padding;
297        audio.complete = true;
298        Ok(())
299    }
300
301    pub async fn finish(mut self) -> Result<S> {
302        if self.finished {
303            return Err(invalid_state("WebM muxer was already finished"));
304        }
305        self.flush_held(None).await?;
306        self.close_cluster().await?;
307        let cues_position = (!self.cues.is_empty()).then(|| self.sink.position());
308        if !self.cues.is_empty() {
309            let cues = cues_element(&self.cues);
310            self.sink.write(&cues).await?;
311        }
312        let end = self.sink.position();
313
314        let relative = |position: u64| position - self.segment_data_start;
315        let header = header_region(
316            [
317                relative(self.info_position),
318                relative(self.tracks_position),
319                cues_position.map_or(0, relative),
320            ],
321            self.end_ticks,
322            cues_position.is_some(),
323        );
324        if header.bytes.len() != self.header_length {
325            return Err(internal("WebM header region changed size"));
326        }
327        self.sink.seek(self.header_position).await?;
328        self.sink.write(&header.bytes).await?;
329        self.patch_size(self.segment_size_position, end - self.segment_data_start)
330            .await?;
331        self.sink.seek(end).await?;
332        self.sink.flush().await?;
333        self.finished = true;
334        Ok(self.sink)
335    }
336
337    /// Writes every held audio block timestamped at or before `before`, or
338    /// every one when it is `None` - then each is its track's last, and
339    /// carries the track's end padding.
340    async fn flush_held(&mut self, before: Option<u64>) -> Result<()> {
341        loop {
342            let next = self
343                .tracks
344                .iter()
345                .enumerate()
346                .filter_map(|(index, track)| {
347                    let held = track.audio.as_ref()?.held.as_ref()?;
348                    before
349                        .is_none_or(|before| held.timestamp <= before)
350                        .then_some((held.timestamp, index))
351                })
352                .min();
353            let Some((_, index)) = next else {
354                return Ok(());
355            };
356            let state = &mut self.tracks[index];
357            let number = state.number;
358            let timescale = state.timescale;
359            let audio = state.audio.as_mut().expect("held blocks are audio");
360            let held = audio.held.take().expect("found above");
361            audio.wrote_block = true;
362            let last = before.is_none() || audio.complete;
363            let padding = if last { audio.padding } else { 0 };
364            let discard_padding = (padding > 0)
365                .then(|| {
366                    let nanoseconds = u128::from(padding) * u128::from(NANOSECONDS_PER_SECOND)
367                        / u128::from(timescale);
368                    i64::try_from(nanoseconds)
369                        .map_err(|_| Error::new(ErrorKind::ResourceLimit, "WebM padding overflow"))
370                })
371                .transpose()?;
372            let presented_end = held
373                .end
374                .saturating_sub(u64::from(audio.codec_delay) + u64::from(padding));
375            let end = presented_end as f64 * TICKS_PER_SECOND as f64 / f64::from(timescale);
376            self.write_block(
377                number,
378                held.timestamp,
379                &held.data,
380                true,
381                false,
382                discard_padding,
383            )
384            .await?;
385            self.end_ticks = self.end_ticks.max(end);
386        }
387    }
388
389    /// Writes one block, opening a Cluster first when it needs one.
390    async fn write_block(
391        &mut self,
392        number: u64,
393        timestamp: u64,
394        data: &[u8],
395        is_sync: bool,
396        is_video: bool,
397        discard_padding_ns: Option<i64>,
398    ) -> Result<()> {
399        let starts_cluster = is_sync && is_video;
400        let needs_cluster = match &self.cluster {
401            None => true,
402            Some(cluster) => {
403                (starts_cluster && cluster.blocks > 0)
404                    || timestamp - cluster.timestamp > i16::MAX as u64
405                    || (!self.has_video && timestamp - cluster.timestamp >= AUDIO_CLUSTER_TICKS)
406            }
407        };
408        if needs_cluster {
409            self.close_cluster().await?;
410            self.open_cluster(timestamp).await?;
411        }
412        let cluster = self.cluster.as_mut().expect("opened above");
413        let relative = i16::try_from(timestamp - cluster.timestamp).expect("checked above");
414        // Every video random-access sample is cued. Audio is cued once a
415        // Cluster, at its first block, in a file with no video to cue.
416        let cued = if is_video {
417            is_sync
418        } else {
419            !self.has_video && cluster.blocks == 0
420        };
421        if cued {
422            self.cues.push(CueRecord {
423                time: timestamp,
424                track: number,
425                cluster_position: cluster.start - self.segment_data_start,
426                relative_position: self.sink.position() - cluster.data_start,
427            });
428        }
429        let mut block = Vec::with_capacity(32);
430        match discard_padding_ns {
431            None => {
432                write_id(&mut block, ebml::SIMPLE_BLOCK);
433                ebml::write_vint(&mut block, 4 + data.len() as u64);
434                ebml::write_vint(&mut block, number);
435                block.extend_from_slice(&relative.to_be_bytes());
436                block.push(if is_sync { 0x80 } else { 0x00 });
437                self.sink.write(&block).await?;
438                self.sink.write(data).await?;
439            }
440            // `DiscardPadding` belongs to a BlockGroup, whose Block carries
441            // no keyframe flag.
442            Some(padding) => {
443                let mut padding_element = Vec::new();
444                write_int(&mut padding_element, ebml::DISCARD_PADDING, padding);
445                let mut block_header = Vec::new();
446                write_id(&mut block_header, ebml::BLOCK);
447                ebml::write_vint(&mut block_header, 4 + data.len() as u64);
448                ebml::write_vint(&mut block_header, number);
449                block_header.extend_from_slice(&relative.to_be_bytes());
450                block_header.push(0x00);
451                let group_length = (block_header.len() + data.len() + padding_element.len()) as u64;
452                write_id(&mut block, ebml::BLOCK_GROUP);
453                ebml::write_vint(&mut block, group_length);
454                block.extend_from_slice(&block_header);
455                self.sink.write(&block).await?;
456                self.sink.write(data).await?;
457                self.sink.write(&padding_element).await?;
458            }
459        }
460        cluster.blocks += 1;
461        self.last_timestamp = self.last_timestamp.max(timestamp);
462        Ok(())
463    }
464
465    async fn open_cluster(&mut self, timestamp: u64) -> Result<()> {
466        let start = self.sink.position();
467        let mut header = Vec::with_capacity(20);
468        write_id(&mut header, ebml::CLUSTER);
469        ebml::write_vint_fixed(&mut header, 0, 8);
470        let data_start = start + header.len() as u64;
471        write_uint(&mut header, ebml::TIMESTAMP, timestamp);
472        self.sink.write(&header).await?;
473        self.cluster = Some(OpenCluster {
474            start,
475            data_start,
476            timestamp,
477            blocks: 0,
478        });
479        Ok(())
480    }
481
482    async fn close_cluster(&mut self) -> Result<()> {
483        let Some(cluster) = self.cluster.take() else {
484            return Ok(());
485        };
486        let end = self.sink.position();
487        self.patch_size(cluster.data_start - 8, end - cluster.data_start)
488            .await?;
489        self.sink.seek(end).await
490    }
491
492    /// Rewrites the eight-byte size at `position`.
493    async fn patch_size(&mut self, position: u64, size: u64) -> Result<()> {
494        let mut bytes = Vec::with_capacity(8);
495        ebml::write_vint_fixed(&mut bytes, size, 8);
496        self.sink.seek(position).await?;
497        self.sink.write(&bytes).await
498    }
499}
500
501fn validate_track_config(config: &Mp4TrackConfig) -> Result<()> {
502    if config.encoder.timescale == 0 {
503        return Err(invalid("a WebM track timescale must be nonzero"));
504    }
505    match (config.encoder.codec, config.format) {
506        (Codec::Av1 | Codec::Vp8 | Codec::Vp9, Mp4TrackFormat::Video(_)) => {}
507        (Codec::Opus | Codec::Vorbis, Mp4TrackFormat::Audio { channels }) => {
508            let declared = match config.encoder.codec {
509                Codec::Opus => {
510                    u16::from(OpusHead::from_dops(&config.encoder.decoder_config)?.channels)
511                }
512                _ => u16::from(
513                    VorbisConfig::from_codec_private(&config.encoder.decoder_config)?.channels,
514                ),
515            };
516            if declared != channels {
517                return Err(invalid(
518                    "the audio codec configuration's channel count disagrees with the track format",
519                ));
520            }
521        }
522        (Codec::Aac, _) | (_, Mp4TrackFormat::Audio { .. }) => {
523            return Err(Error::new(
524                ErrorKind::Unsupported,
525                "WebM permits only Vorbis or Opus audio; write AAC to MP4",
526            ));
527        }
528        (Codec::Hevc | Codec::H264, _) => {
529            return Err(Error::new(
530                ErrorKind::Unsupported,
531                "WebM permits only VP8, VP9 or AV1 video; write HEVC or H.264 to MP4",
532            ));
533        }
534        _ => {
535            return Err(Error::new(
536                ErrorKind::Unsupported,
537                "codec is not a WebM output codec",
538            ));
539        }
540    }
541    codec_private(config).map(|_| ())
542}
543
544/// The decoded samples a track's `CodecDelay` declares: an Opus stream's
545/// pre-skip, and none for any other codec.
546fn codec_delay(config: &Mp4TrackConfig) -> Result<u32> {
547    match config.encoder.codec {
548        Codec::Opus => Ok(u32::from(
549            OpusHead::from_dops(&config.encoder.decoder_config)?.pre_skip,
550        )),
551        _ => Ok(0),
552    }
553}
554
555/// The track's `CodecID` and `CodecPrivate`.
556fn codec_private(config: &Mp4TrackConfig) -> Result<(&'static str, Vec<u8>)> {
557    match config.encoder.codec {
558        Codec::Av1 => {
559            let whole = &config.encoder.decoder_config;
560            let declared = whole
561                .get(..4)
562                .map(|size| u32::from_be_bytes(size.try_into().expect("four bytes")));
563            if whole.len() < 12
564                || declared.and_then(|size| usize::try_from(size).ok()) != Some(whole.len())
565                || &whole[4..8] != b"av1C"
566            {
567                return Err(invalid(
568                    "AV1 codec configuration must be a complete av1C box",
569                ));
570            }
571            // WebM's CodecPrivate is the AV1CodecConfigurationRecord itself.
572            Ok(("V_AV1", whole[8..].to_vec()))
573        }
574        // VP8 carries everything a decoder needs in its key frames, so the
575        // Matroska VP8 mapping has no CodecPrivate.
576        Codec::Vp8 => {
577            if !config.encoder.decoder_config.is_empty() {
578                return Err(invalid("VP8 has no codec configuration record"));
579            }
580            Ok(("V_VP8", Vec::new()))
581        }
582        Codec::Vp9 => {
583            // The WebM VP9 codec mapping's CodecPrivate is a list of
584            // (ID, length, value) features: profile (1), level (2), bit
585            // depth (3) and chroma subsampling (4), all from the `vpcC`.
586            let vpcc = Vp9CodecConfig::parse_vpcc(&config.encoder.decoder_config)
587                .map_err(|_| invalid("VP9 codec configuration must be a complete vpcC box"))?;
588            let mut features = Vec::with_capacity(12);
589            for (id, value) in [
590                (1, vpcc.profile),
591                (2, vpcc.level),
592                (3, vpcc.bit_depth),
593                (4, vpcc.chroma_subsampling),
594            ] {
595                features.extend_from_slice(&[id, 1, value]);
596            }
597            Ok(("V_VP9", features))
598        }
599        // The Matroska Opus mapping's CodecPrivate is the RFC 7845 OpusHead.
600        Codec::Opus => Ok((
601            "A_OPUS",
602            OpusHead::from_dops(&config.encoder.decoder_config)?.to_identification_header(),
603        )),
604        // A Vorbis encoder's configuration is already the Xiph-laced
605        // headers Matroska carries.
606        Codec::Vorbis => Ok(("A_VORBIS", config.encoder.decoder_config.clone())),
607        _ => Err(Error::new(
608            ErrorKind::Unsupported,
609            "codec is not a WebM output codec",
610        )),
611    }
612}
613
614/// The `Color` element for a VP9 track: the range and color description its
615/// `vpcC` declares, which the WebM `CodecPrivate` has no field for.
616///
617/// Chrome takes a WebM VP9 track's color space from this element rather than
618/// from the bitstream, and without it rejects a full-range stream such as its
619/// own `WebCodecs` encoder writes from RGBA canvas frames with a decode error
620/// on the first frame after the key frame (issue #655). The descriptions the
621/// `vpcC` leaves unspecified are left out.
622fn vp9_color(config: &Mp4TrackConfig) -> Result<Vec<u8>> {
623    let vpcc = Vp9CodecConfig::parse_vpcc(&config.encoder.decoder_config)
624        .map_err(|_| invalid("VP9 codec configuration must be a complete vpcC box"))?;
625    let mut color = Vec::new();
626    for (id, value) in [
627        (ebml::MATRIX_COEFFICIENTS, vpcc.matrix_coefficients),
628        (
629            ebml::TRANSFER_CHARACTERISTICS,
630            vpcc.transfer_characteristics,
631        ),
632        (ebml::PRIMARIES, vpcc.color_primaries),
633    ] {
634        if value != 2 {
635            write_uint(&mut color, id, u64::from(value));
636        }
637    }
638    // Matroska's Range is 1 for broadcast range and 2 for full range.
639    write_uint(
640        &mut color,
641        ebml::RANGE,
642        1 + u64::from(vpcc.video_full_range),
643    );
644    Ok(color)
645}
646
647fn to_ticks(pts: u64, timescale: u32) -> Result<u64> {
648    let ticks = (u128::from(pts) * u128::from(TICKS_PER_SECOND) + u128::from(timescale) / 2)
649        / u128::from(timescale);
650    u64::try_from(ticks)
651        .map_err(|_| Error::new(ErrorKind::ResourceLimit, "WebM timestamp overflow"))
652}
653
654fn ebml_header() -> Vec<u8> {
655    let mut payload = Vec::new();
656    write_uint(&mut payload, ebml::EBML_VERSION, 1);
657    write_uint(&mut payload, ebml::EBML_READ_VERSION, 1);
658    write_uint(&mut payload, ebml::EBML_MAX_ID_LENGTH, 4);
659    write_uint(&mut payload, ebml::EBML_MAX_SIZE_LENGTH, 8);
660    write_string(&mut payload, ebml::DOC_TYPE, "webm");
661    write_uint(&mut payload, ebml::DOC_TYPE_VERSION, 4);
662    // SimpleBlock needs a version 2 reader.
663    write_uint(&mut payload, ebml::DOC_TYPE_READ_VERSION, 2);
664    let mut output = Vec::new();
665    write_element(&mut output, ebml::EBML, &payload);
666    output
667}
668
669struct HeaderRegion {
670    bytes: Vec<u8>,
671    info_offset: usize,
672}
673
674/// The `SeekHead` and `Info` at the start of the Segment. Every value `finish`
675/// learns late is written at a fixed width, so the region is the same length
676/// as the placeholder written first and is rewritten in place. Without cues,
677/// the Cues entry becomes a Void of the same length.
678fn header_region(positions: [u64; 3], duration: f64, with_cues: bool) -> HeaderRegion {
679    let mut seek_head = Vec::new();
680    for (target, position) in SEEK_TARGETS.into_iter().zip(positions) {
681        let mut seek = Vec::new();
682        let mut id = Vec::new();
683        write_id(&mut id, target);
684        write_element(&mut seek, ebml::SEEK_ID, &id);
685        ebml::write_uint_fixed(&mut seek, ebml::SEEK_POSITION, position);
686        let mut entry = Vec::new();
687        write_element(&mut entry, ebml::SEEK, &seek);
688        if target == ebml::CUES && !with_cues {
689            let filler = entry.len() - 2;
690            entry.clear();
691            write_element(&mut entry, ebml::VOID, &vec![0; filler]);
692        }
693        seek_head.extend_from_slice(&entry);
694    }
695    let mut info = Vec::new();
696    write_uint(&mut info, ebml::TIMESTAMP_SCALE, TIMESTAMP_SCALE_NS);
697    write_float(&mut info, ebml::DURATION, duration);
698    write_string(&mut info, ebml::MUXING_APP, MUXING_APP);
699    write_string(&mut info, ebml::WRITING_APP, MUXING_APP);
700
701    let mut bytes = Vec::new();
702    write_element(&mut bytes, ebml::SEEK_HEAD, &seek_head);
703    let info_offset = bytes.len();
704    write_element(&mut bytes, ebml::INFO, &info);
705    HeaderRegion { bytes, info_offset }
706}
707
708fn tracks_element(configs: &[Mp4TrackConfig]) -> Result<Vec<u8>> {
709    let mut tracks = Vec::new();
710    for (index, config) in configs.iter().enumerate() {
711        let number = index as u64 + 1;
712        let (codec_id, private) = codec_private(config)?;
713        let mut entry = Vec::new();
714        write_uint(&mut entry, ebml::TRACK_NUMBER, number);
715        write_uint(&mut entry, ebml::TRACK_UID, number);
716        match config.format {
717            Mp4TrackFormat::Video(dimensions) => {
718                write_uint(&mut entry, ebml::TRACK_TYPE, ebml::TRACK_TYPE_VIDEO);
719                write_uint(&mut entry, ebml::FLAG_LACING, 0);
720                write_string(&mut entry, ebml::CODEC_ID, codec_id);
721                if !private.is_empty() {
722                    write_element(&mut entry, ebml::CODEC_PRIVATE, &private);
723                }
724                let mut video = Vec::new();
725                write_uint(&mut video, ebml::PIXEL_WIDTH, u64::from(dimensions.width));
726                write_uint(&mut video, ebml::PIXEL_HEIGHT, u64::from(dimensions.height));
727                if config.encoder.codec == Codec::Vp9 {
728                    write_element(&mut video, ebml::COLOR, &vp9_color(config)?);
729                }
730                write_element(&mut entry, ebml::VIDEO, &video);
731            }
732            Mp4TrackFormat::Audio { channels } => {
733                write_uint(&mut entry, ebml::TRACK_TYPE, ebml::TRACK_TYPE_AUDIO);
734                write_uint(&mut entry, ebml::FLAG_LACING, 0);
735                write_string(&mut entry, ebml::CODEC_ID, codec_id);
736                write_element(&mut entry, ebml::CODEC_PRIVATE, &private);
737                let delay = codec_delay(config)?;
738                if config.encoder.codec == Codec::Opus {
739                    // Opus's delay and preroll are on its 48 kHz clock.
740                    let delay_ns = u64::from(delay) * NANOSECONDS_PER_SECOND
741                        / u64::from(crate::OPUS_SAMPLE_RATE);
742                    write_uint(&mut entry, ebml::CODEC_DELAY, delay_ns);
743                    write_uint(&mut entry, ebml::SEEK_PRE_ROLL, OPUS_SEEK_PRE_ROLL_NS);
744                }
745                let mut audio = Vec::new();
746                write_float(
747                    &mut audio,
748                    ebml::SAMPLING_FREQUENCY,
749                    f64::from(config.encoder.timescale),
750                );
751                write_uint(&mut audio, ebml::CHANNELS, u64::from(channels));
752                write_element(&mut entry, ebml::AUDIO, &audio);
753            }
754        }
755        write_element(&mut tracks, ebml::TRACK_ENTRY, &entry);
756    }
757    let mut output = Vec::new();
758    write_element(&mut output, ebml::TRACKS, &tracks);
759    Ok(output)
760}
761
762fn cues_element(cues: &[CueRecord]) -> Vec<u8> {
763    let mut points = Vec::new();
764    for cue in cues {
765        let mut positions = Vec::new();
766        write_uint(&mut positions, ebml::CUE_TRACK, cue.track);
767        write_uint(
768            &mut positions,
769            ebml::CUE_CLUSTER_POSITION,
770            cue.cluster_position,
771        );
772        write_uint(
773            &mut positions,
774            ebml::CUE_RELATIVE_POSITION,
775            cue.relative_position,
776        );
777        let mut point = Vec::new();
778        write_uint(&mut point, ebml::CUE_TIME, cue.time);
779        write_element(&mut point, ebml::CUE_TRACK_POSITIONS, &positions);
780        write_element(&mut points, ebml::CUE_POINT, &point);
781    }
782    let mut output = Vec::new();
783    write_element(&mut output, ebml::CUES, &points);
784    output
785}
786
787fn invalid(message: &str) -> Error {
788    Error::new(ErrorKind::InvalidInput, message)
789}
790
791fn invalid_state(message: &str) -> Error {
792    Error::new(ErrorKind::InvalidState, message)
793}
794
795fn internal(message: &str) -> Error {
796    Error::new(ErrorKind::Internal, message)
797}