1use 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
33const TIMESTAMP_SCALE_NS: u64 = 1_000_000;
36const TICKS_PER_SECOND: u64 = 1_000;
37const MAX_TRACKS: usize = 126;
39const 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;
43const OPUS_SEEK_PRE_ROLL_NS: u64 = 80_000_000;
46const 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 codec_delay: u32,
61 padding: u32,
62 held: Option<HeldBlock>,
64 wrote_block: bool,
66 complete: bool,
69}
70
71struct HeldBlock {
72 timestamp: u64,
73 data: Vec<u8>,
74 end: u64,
76}
77
78struct OpenCluster {
79 start: u64,
81 data_start: u64,
83 timestamp: u64,
84 blocks: usize,
85}
86
87struct CueRecord {
88 time: u64,
89 track: u64,
90 cluster_position: u64,
92 relative_position: u64,
94}
95
96pub 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 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 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 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 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 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 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 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 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
544fn 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
555fn 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 Ok(("V_AV1", whole[8..].to_vec()))
573 }
574 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 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 Codec::Opus => Ok((
601 "A_OPUS",
602 OpusHead::from_dops(&config.encoder.decoder_config)?.to_identification_header(),
603 )),
604 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
614fn 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 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 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
674fn 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 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}