2020-03-02 01:53:41 -05:00
|
|
|
// This file is part of Moonfire NVR, a security camera network video recorder.
|
2020-03-20 00:35:42 -04:00
|
|
|
// Copyright (C) 2016-2020 The Moonfire NVR Authors
|
2016-11-25 17:34:00 -05:00
|
|
|
//
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
|
|
// (at your option) any later version.
|
|
|
|
//
|
|
|
|
// In addition, as a special exception, the copyright holders give
|
|
|
|
// permission to link the code of portions of this program with the
|
|
|
|
// OpenSSL library under certain conditions as described in each
|
|
|
|
// individual source file, and distribute linked combinations including
|
|
|
|
// the two.
|
|
|
|
//
|
|
|
|
// You must obey the GNU General Public License in all respects for all
|
|
|
|
// of the code used other than OpenSSL. If you modify file(s) with this
|
|
|
|
// exception, you may extend this exception to your version of the
|
|
|
|
// file(s), but you are not obligated to do so. If you do not wish to do
|
|
|
|
// so, delete this exception statement from your version. If you delete
|
|
|
|
// this exception statement from all source files in the program, then
|
|
|
|
// also delete it here.
|
|
|
|
//
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU General Public License for more details.
|
|
|
|
//
|
|
|
|
// You should have received a copy of the GNU General Public License
|
|
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
//! `.mp4` virtual file serving.
|
|
|
|
//!
|
|
|
|
//! The `mp4` module builds virtual files representing ISO/IEC 14496-12 (ISO base media format /
|
|
|
|
//! MPEG-4 / `.mp4`) video. These can be constructed from one or more recordings and are suitable
|
2016-12-02 23:40:55 -05:00
|
|
|
//! for HTTP range serving or download. The generated `.mp4` file has the `moov` box before the
|
|
|
|
//! `mdat` box for fast start. More specifically, boxes are arranged in the order suggested by
|
|
|
|
//! ISO/IEC 14496-12 section 6.2.3 (Table 1):
|
|
|
|
//!
|
2021-01-27 14:47:52 -05:00
|
|
|
//! ```text
|
2016-12-02 23:40:55 -05:00
|
|
|
//! * ftyp (file type and compatibility)
|
|
|
|
//! * moov (container for all the metadata)
|
|
|
|
//! ** mvhd (movie header, overall declarations)
|
|
|
|
//!
|
|
|
|
//! ** trak (video: container for an individual track or stream)
|
|
|
|
//! *** tkhd (track header, overall information about the track)
|
|
|
|
//! *** (optional) edts (edit list container)
|
|
|
|
//! **** elst (an edit list)
|
|
|
|
//! *** mdia (container for the media information in a track)
|
|
|
|
//! **** mdhd (media header, overall information about the media)
|
|
|
|
//! *** minf (media information container)
|
|
|
|
//! **** vmhd (video media header, overall information (video track only))
|
|
|
|
//! **** dinf (data information box, container)
|
|
|
|
//! ***** dref (data reference box, declares source(s) of media data in track)
|
|
|
|
//! **** stbl (sample table box, container for the time/space map)
|
|
|
|
//! ***** stsd (sample descriptions (codec types, initilization etc.)
|
|
|
|
//! ***** stts ((decoding) time-to-sample)
|
|
|
|
//! ***** stsc (sample-to-chunk, partial data-offset information)
|
|
|
|
//! ***** stsz (samples sizes (framing))
|
|
|
|
//! ***** co64 (64-bit chunk offset)
|
|
|
|
//! ***** stss (sync sample table)
|
|
|
|
//!
|
|
|
|
//! ** (optional) trak (subtitle: container for an individual track or stream)
|
|
|
|
//! *** tkhd (track header, overall information about the track)
|
|
|
|
//! *** mdia (container for the media information in a track)
|
|
|
|
//! **** mdhd (media header, overall information about the media)
|
|
|
|
//! *** minf (media information container)
|
|
|
|
//! **** nmhd (null media header, overall information)
|
|
|
|
//! **** dinf (data information box, container)
|
|
|
|
//! ***** dref (data reference box, declares source(s) of media data in track)
|
|
|
|
//! **** stbl (sample table box, container for the time/space map)
|
|
|
|
//! ***** stsd (sample descriptions (codec types, initilization etc.)
|
|
|
|
//! ***** stts ((decoding) time-to-sample)
|
|
|
|
//! ***** stsc (sample-to-chunk, partial data-offset information)
|
|
|
|
//! ***** stsz (samples sizes (framing))
|
|
|
|
//! ***** co64 (64-bit chunk offset)
|
|
|
|
//!
|
|
|
|
//! * mdat (media data container)
|
|
|
|
//! ```
|
2016-11-25 17:34:00 -05:00
|
|
|
|
2020-03-20 23:52:30 -04:00
|
|
|
use base::{Error, ErrorKind, ResultExt, bail_t, format_err_t};
|
2021-01-27 14:47:52 -05:00
|
|
|
use bytes::BytesMut;
|
2016-11-25 17:34:00 -05:00
|
|
|
use byteorder::{BigEndian, ByteOrder, WriteBytesExt};
|
2018-12-28 13:21:49 -05:00
|
|
|
use crate::body::{Chunk, BoxedError, wrap_error};
|
2018-12-28 22:53:29 -05:00
|
|
|
use db::dir;
|
2020-08-07 13:16:06 -04:00
|
|
|
use db::recording::{self, TIME_UNITS_PER_SEC, rescale};
|
2018-08-30 01:26:19 -04:00
|
|
|
use futures::Stream;
|
2017-03-02 22:29:28 -05:00
|
|
|
use futures::stream;
|
2018-04-06 18:54:52 -04:00
|
|
|
use http;
|
2018-08-30 01:26:19 -04:00
|
|
|
use http::header::HeaderValue;
|
2018-01-23 14:08:21 -05:00
|
|
|
use http_serve;
|
2021-01-27 14:47:52 -05:00
|
|
|
use hyper::body::Buf;
|
2018-12-28 22:53:29 -05:00
|
|
|
use log::{debug, error, trace, warn};
|
2017-03-02 22:29:28 -05:00
|
|
|
use memmap;
|
2019-05-31 18:08:49 -04:00
|
|
|
use parking_lot::Once;
|
2020-01-09 02:04:36 -05:00
|
|
|
use reffers::ARefss;
|
2018-12-28 13:21:49 -05:00
|
|
|
use crate::slices::{self, Slices};
|
2016-11-25 17:34:00 -05:00
|
|
|
use smallvec::SmallVec;
|
2017-03-02 22:29:28 -05:00
|
|
|
use std::cell::UnsafeCell;
|
2020-02-17 02:16:19 -05:00
|
|
|
use std::convert::TryFrom;
|
2016-11-25 17:34:00 -05:00
|
|
|
use std::cmp;
|
2017-10-09 09:32:43 -04:00
|
|
|
use std::fmt;
|
2016-11-25 17:34:00 -05:00
|
|
|
use std::io;
|
|
|
|
use std::ops::Range;
|
|
|
|
use std::mem;
|
2017-02-25 00:33:26 -05:00
|
|
|
use std::sync::Arc;
|
2018-08-30 01:26:19 -04:00
|
|
|
use std::time::SystemTime;
|
2016-11-25 17:34:00 -05:00
|
|
|
|
|
|
|
/// This value should be incremented any time a change is made to this file that causes different
|
2020-02-17 02:16:19 -05:00
|
|
|
/// bytes to be output for a particular set of `FileBuilder` options. Incrementing this value will
|
2016-11-25 17:34:00 -05:00
|
|
|
/// cause the etag to change as well.
|
2020-03-20 00:35:42 -04:00
|
|
|
const FORMAT_VERSION: [u8; 1] = [0x07];
|
2016-11-25 17:34:00 -05:00
|
|
|
|
|
|
|
/// An `ftyp` (ISO/IEC 14496-12 section 4.3 `FileType`) box.
|
2017-10-01 18:29:22 -04:00
|
|
|
const NORMAL_FTYP_BOX: &'static [u8] = &[
|
|
|
|
0x00, 0x00, 0x00, 0x20, // length = 32, sizeof(NORMAL_FTYP_BOX)
|
2016-11-25 17:34:00 -05:00
|
|
|
b'f', b't', b'y', b'p', // type
|
|
|
|
b'i', b's', b'o', b'm', // major_brand
|
|
|
|
0x00, 0x00, 0x02, 0x00, // minor_version
|
|
|
|
b'i', b's', b'o', b'm', // compatible_brands[0]
|
|
|
|
b'i', b's', b'o', b'2', // compatible_brands[1]
|
|
|
|
b'a', b'v', b'c', b'1', // compatible_brands[2]
|
|
|
|
b'm', b'p', b'4', b'1', // compatible_brands[3]
|
|
|
|
];
|
|
|
|
|
2017-10-01 18:29:22 -04:00
|
|
|
/// An `ftyp` (ISO/IEC 14496-12 section 4.3 `FileType`) box for an initialization segment.
|
|
|
|
/// More restrictive brands because of the default-base-is-moof flag.
|
|
|
|
const INIT_SEGMENT_FTYP_BOX: &'static [u8] = &[
|
|
|
|
0x00, 0x00, 0x00, 0x10, // length = 16, sizeof(INIT_SEGMENT_FTYP_BOX)
|
|
|
|
b'f', b't', b'y', b'p', // type
|
|
|
|
b'i', b's', b'o', b'5', // major_brand
|
|
|
|
0x00, 0x00, 0x02, 0x00, // minor_version
|
|
|
|
];
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// An `hdlr` (ISO/IEC 14496-12 section 8.4.3 `HandlerBox`) box suitable for video.
|
2016-11-25 17:34:00 -05:00
|
|
|
const VIDEO_HDLR_BOX: &'static [u8] = &[
|
|
|
|
0x00, 0x00, 0x00, 0x21, // length == sizeof(kHdlrBox)
|
|
|
|
b'h', b'd', b'l', b'r', // type == hdlr, ISO/IEC 14496-12 section 8.4.3.
|
|
|
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
|
|
|
0x00, 0x00, 0x00, 0x00, // pre_defined
|
|
|
|
b'v', b'i', b'd', b'e', // handler = vide
|
|
|
|
0x00, 0x00, 0x00, 0x00, // reserved[0]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // reserved[1]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // reserved[2]
|
|
|
|
0x00, // name, zero-terminated (empty)
|
|
|
|
];
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// An `hdlr` (ISO/IEC 14496-12 section 8.4.3 `HandlerBox`) box suitable for subtitles.
|
2016-11-25 17:34:00 -05:00
|
|
|
const SUBTITLE_HDLR_BOX: &'static [u8] = &[
|
|
|
|
0x00, 0x00, 0x00, 0x21, // length == sizeof(kHdlrBox)
|
|
|
|
b'h', b'd', b'l', b'r', // type == hdlr, ISO/IEC 14496-12 section 8.4.3.
|
|
|
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
|
|
|
0x00, 0x00, 0x00, 0x00, // pre_defined
|
|
|
|
b's', b'b', b't', b'l', // handler = sbtl
|
|
|
|
0x00, 0x00, 0x00, 0x00, // reserved[0]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // reserved[1]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // reserved[2]
|
|
|
|
0x00, // name, zero-terminated (empty)
|
|
|
|
];
|
|
|
|
|
|
|
|
/// Part of an `mvhd` (`MovieHeaderBox` version 0, ISO/IEC 14496-12 section 8.2.2), used from
|
|
|
|
/// `append_mvhd`.
|
|
|
|
const MVHD_JUNK: &'static [u8] = &[
|
|
|
|
0x00, 0x01, 0x00, 0x00, // rate
|
|
|
|
0x01, 0x00, // volume
|
|
|
|
0x00, 0x00, // reserved
|
|
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
|
|
0x00, 0x01, 0x00, 0x00, // matrix[0]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // matrix[1]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // matrix[2]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // matrix[3]
|
|
|
|
0x00, 0x01, 0x00, 0x00, // matrix[4]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // matrix[5]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // matrix[6]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // matrix[7]
|
|
|
|
0x40, 0x00, 0x00, 0x00, // matrix[8]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // pre_defined[0]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // pre_defined[1]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // pre_defined[2]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // pre_defined[3]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // pre_defined[4]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // pre_defined[5]
|
|
|
|
];
|
|
|
|
|
|
|
|
/// Part of a `tkhd` (`TrackHeaderBox` version 0, ISO/IEC 14496-12 section 8.3.2), used from
|
|
|
|
/// `append_video_tkhd` and `append_subtitle_tkhd`.
|
|
|
|
const TKHD_JUNK: &'static [u8] = &[
|
|
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
|
|
0x00, 0x00, 0x00, 0x00, // layer + alternate_group
|
|
|
|
0x00, 0x00, 0x00, 0x00, // volume + reserved
|
|
|
|
0x00, 0x01, 0x00, 0x00, // matrix[0]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // matrix[1]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // matrix[2]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // matrix[3]
|
|
|
|
0x00, 0x01, 0x00, 0x00, // matrix[4]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // matrix[5]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // matrix[6]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // matrix[7]
|
|
|
|
0x40, 0x00, 0x00, 0x00, // matrix[8]
|
|
|
|
];
|
|
|
|
|
|
|
|
/// Part of a `minf` (`MediaInformationBox`, ISO/IEC 14496-12 section 8.4.4), used from
|
|
|
|
/// `append_video_minf`.
|
|
|
|
const VIDEO_MINF_JUNK: &'static [u8] = &[
|
|
|
|
b'm', b'i', b'n', b'f', // type = minf, ISO/IEC 14496-12 section 8.4.4.
|
|
|
|
// A vmhd box; the "graphicsmode" and "opcolor" values don't have any
|
|
|
|
// meaningful use.
|
|
|
|
0x00, 0x00, 0x00, 0x14, // length == sizeof(kVmhdBox)
|
|
|
|
b'v', b'm', b'h', b'd', // type = vmhd, ISO/IEC 14496-12 section 12.1.2.
|
|
|
|
0x00, 0x00, 0x00, 0x01, // version + flags(1)
|
|
|
|
0x00, 0x00, 0x00, 0x00, // graphicsmode (copy), opcolor[0]
|
|
|
|
0x00, 0x00, 0x00, 0x00, // opcolor[1], opcolor[2]
|
|
|
|
|
|
|
|
// A dinf box suitable for a "self-contained" .mp4 file (no URL/URN
|
|
|
|
// references to external data).
|
|
|
|
0x00, 0x00, 0x00, 0x24, // length == sizeof(kDinfBox)
|
|
|
|
b'd', b'i', b'n', b'f', // type = dinf, ISO/IEC 14496-12 section 8.7.1.
|
|
|
|
0x00, 0x00, 0x00, 0x1c, // length
|
|
|
|
b'd', b'r', b'e', b'f', // type = dref, ISO/IEC 14496-12 section 8.7.2.
|
|
|
|
0x00, 0x00, 0x00, 0x00, // version and flags
|
|
|
|
0x00, 0x00, 0x00, 0x01, // entry_count
|
|
|
|
0x00, 0x00, 0x00, 0x0c, // length
|
|
|
|
b'u', b'r', b'l', b' ', // type = url, ISO/IEC 14496-12 section 8.7.2.
|
|
|
|
0x00, 0x00, 0x00, 0x01, // version=0, flags=self-contained
|
|
|
|
];
|
|
|
|
|
|
|
|
/// Part of a `minf` (`MediaInformationBox`, ISO/IEC 14496-12 section 8.4.4), used from
|
|
|
|
/// `append_subtitle_minf`.
|
|
|
|
const SUBTITLE_MINF_JUNK: &'static [u8] = &[
|
|
|
|
b'm', b'i', b'n', b'f', // type = minf, ISO/IEC 14496-12 section 8.4.4.
|
|
|
|
// A nmhd box.
|
|
|
|
0x00, 0x00, 0x00, 0x0c, // length == sizeof(kNmhdBox)
|
|
|
|
b'n', b'm', b'h', b'd', // type = nmhd, ISO/IEC 14496-12 section 12.1.2.
|
|
|
|
0x00, 0x00, 0x00, 0x01, // version + flags(1)
|
|
|
|
|
|
|
|
// A dinf box suitable for a "self-contained" .mp4 file (no URL/URN
|
|
|
|
// references to external data).
|
|
|
|
0x00, 0x00, 0x00, 0x24, // length == sizeof(kDinfBox)
|
|
|
|
b'd', b'i', b'n', b'f', // type = dinf, ISO/IEC 14496-12 section 8.7.1.
|
|
|
|
0x00, 0x00, 0x00, 0x1c, // length
|
|
|
|
b'd', b'r', b'e', b'f', // type = dref, ISO/IEC 14496-12 section 8.7.2.
|
|
|
|
0x00, 0x00, 0x00, 0x00, // version and flags
|
|
|
|
0x00, 0x00, 0x00, 0x01, // entry_count
|
|
|
|
0x00, 0x00, 0x00, 0x0c, // length
|
|
|
|
b'u', b'r', b'l', b' ', // type = url, ISO/IEC 14496-12 section 8.7.2.
|
|
|
|
0x00, 0x00, 0x00, 0x01, // version=0, flags=self-contained
|
|
|
|
];
|
|
|
|
|
|
|
|
/// Part of a `stbl` (`SampleTableBox`, ISO/IEC 14496 section 8.5.1) used from
|
|
|
|
/// `append_subtitle_stbl`.
|
|
|
|
const SUBTITLE_STBL_JUNK: &'static [u8] = &[
|
|
|
|
b's', b't', b'b', b'l', // type = stbl, ISO/IEC 14496-12 section 8.5.1.
|
|
|
|
|
|
|
|
// A stsd box.
|
|
|
|
0x00, 0x00, 0x00, 0x54, // length
|
|
|
|
b's', b't', b's', b'd', // type == stsd, ISO/IEC 14496-12 section 8.5.2.
|
|
|
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
|
|
|
0x00, 0x00, 0x00, 0x01, // entry_count == 1
|
|
|
|
|
|
|
|
// SampleEntry, ISO/IEC 14496-12 section 8.5.2.2.
|
|
|
|
0x00, 0x00, 0x00, 0x44, // length
|
|
|
|
b't', b'x', b'3', b'g', // type == tx3g, 3GPP TS 26.245 section 5.16.
|
|
|
|
0x00, 0x00, 0x00, 0x00, // reserved
|
|
|
|
0x00, 0x00, 0x00, 0x01, // reserved, data_reference_index == 1
|
|
|
|
|
|
|
|
// TextSampleEntry
|
|
|
|
0x00, 0x00, 0x00, 0x00, // displayFlags == none
|
|
|
|
0x00, // horizontal-justification == left
|
|
|
|
0x00, // vertical-justification == top
|
|
|
|
0x00, 0x00, 0x00, 0x00, // background-color-rgba == transparent
|
|
|
|
|
|
|
|
// TextSampleEntry.BoxRecord
|
|
|
|
0x00, 0x00, // top
|
|
|
|
0x00, 0x00, // left
|
|
|
|
0x00, 0x00, // bottom
|
|
|
|
0x00, 0x00, // right
|
|
|
|
|
|
|
|
// TextSampleEntry.StyleRecord
|
|
|
|
0x00, 0x00, // startChar
|
|
|
|
0x00, 0x00, // endChar
|
|
|
|
0x00, 0x01, // font-ID
|
|
|
|
0x00, // face-style-flags
|
|
|
|
0x12, // font-size == 18 px
|
|
|
|
0xff, 0xff, 0xff, 0xff, // text-color-rgba == opaque white
|
|
|
|
|
|
|
|
// TextSampleEntry.FontTableBox
|
|
|
|
0x00, 0x00, 0x00, 0x16, // length
|
|
|
|
b'f', b't', b'a', b'b', // type == ftab, section 5.16
|
|
|
|
0x00, 0x01, // entry-count == 1
|
|
|
|
0x00, 0x01, // font-ID == 1
|
|
|
|
0x09, // font-name-length == 9
|
|
|
|
b'M', b'o', b'n', b'o', b's', b'p', b'a', b'c', b'e',
|
|
|
|
];
|
|
|
|
|
|
|
|
/// Pointers to each static bytestrings.
|
|
|
|
/// The order here must match the `StaticBytestring` enum.
|
2017-10-01 18:29:22 -04:00
|
|
|
const STATIC_BYTESTRINGS: [&'static [u8]; 9] = [
|
|
|
|
NORMAL_FTYP_BOX,
|
|
|
|
INIT_SEGMENT_FTYP_BOX,
|
2016-11-25 17:34:00 -05:00
|
|
|
VIDEO_HDLR_BOX,
|
|
|
|
SUBTITLE_HDLR_BOX,
|
|
|
|
MVHD_JUNK,
|
|
|
|
TKHD_JUNK,
|
|
|
|
VIDEO_MINF_JUNK,
|
|
|
|
SUBTITLE_MINF_JUNK,
|
|
|
|
SUBTITLE_STBL_JUNK,
|
|
|
|
];
|
|
|
|
|
|
|
|
/// Enumeration of the static bytestrings. The order here must match the `STATIC_BYTESTRINGS`
|
|
|
|
/// array. The advantage of this enum over direct pointers to the relevant strings is that it
|
2017-02-21 22:37:36 -05:00
|
|
|
/// fits into `Slice`'s 20-bit `p`.
|
2016-11-25 17:34:00 -05:00
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
|
|
enum StaticBytestring {
|
2017-10-01 18:29:22 -04:00
|
|
|
NormalFtypBox,
|
|
|
|
InitSegmentFtypBox,
|
2016-11-25 17:34:00 -05:00
|
|
|
VideoHdlrBox,
|
|
|
|
SubtitleHdlrBox,
|
|
|
|
MvhdJunk,
|
|
|
|
TkhdJunk,
|
|
|
|
VideoMinfJunk,
|
|
|
|
SubtitleMinfJunk,
|
|
|
|
SubtitleStblJunk,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// The template fed into strtime for a timestamp subtitle. This must produce fixed-length output
|
|
|
|
/// (see `SUBTITLE_LENGTH`) to allow quick calculation of the total size of the subtitles for
|
|
|
|
/// a given time range.
|
|
|
|
const SUBTITLE_TEMPLATE: &'static str = "%Y-%m-%d %H:%M:%S %z";
|
|
|
|
|
|
|
|
/// The length of the output of `SUBTITLE_TEMPLATE`.
|
|
|
|
const SUBTITLE_LENGTH: usize = 25; // "2015-07-02 17:10:00 -0700".len();
|
|
|
|
|
2017-02-26 03:02:49 -05:00
|
|
|
/// The lengths of the indexes associated with a `Segment`; for use within `Segment` only.
|
|
|
|
struct SegmentLengths {
|
|
|
|
stts: usize,
|
|
|
|
stsz: usize,
|
|
|
|
stss: usize,
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// A wrapper around `recording::Segment` that keeps some additional `.mp4`-specific state.
|
2017-02-25 00:33:26 -05:00
|
|
|
struct Segment {
|
2020-08-05 00:44:01 -04:00
|
|
|
/// The underlying segment (a portion of a recording).
|
2016-11-25 17:34:00 -05:00
|
|
|
s: recording::Segment,
|
|
|
|
|
2020-08-05 00:44:01 -04:00
|
|
|
/// The absolute timestamp of the recording's start time.
|
|
|
|
recording_start: recording::Time,
|
|
|
|
|
|
|
|
recording_wall_duration_90k: i32,
|
|
|
|
recording_media_duration_90k: i32,
|
|
|
|
|
2020-08-07 13:16:06 -04:00
|
|
|
/// The _desired_, _relative_, _media_ time range covered by this recording.
|
2020-08-05 00:44:01 -04:00
|
|
|
/// * _desired_: as noted in `recording::Segment`, the _actual_ time range may be somewhat
|
|
|
|
/// more if there's no key frame at the desired start.
|
|
|
|
/// * _relative_: relative to `recording_start` rather than absolute timestamps.
|
2020-08-07 13:16:06 -04:00
|
|
|
/// * _media_ time: as described in design/glossary.md and design/time.md.
|
|
|
|
rel_media_range_90k: Range<i32>,
|
2020-08-05 00:44:01 -04:00
|
|
|
|
2017-10-01 18:29:22 -04:00
|
|
|
/// If generated, the `.mp4`-format sample indexes, accessed only through `get_index`:
|
2017-02-26 03:02:49 -05:00
|
|
|
/// 1. stts: `slice[.. stsz_start]`
|
|
|
|
/// 2. stsz: `slice[stsz_start .. stss_start]`
|
|
|
|
/// 3. stss: `slice[stss_start ..]`
|
2017-03-02 22:29:28 -05:00
|
|
|
index: UnsafeCell<Result<Box<[u8]>, ()>>,
|
2020-11-28 00:56:15 -05:00
|
|
|
index_once: Once,
|
2016-11-25 17:34:00 -05:00
|
|
|
|
2017-02-25 00:33:26 -05:00
|
|
|
/// The 1-indexed frame number in the `File` of the first frame in this segment.
|
2016-11-25 17:34:00 -05:00
|
|
|
first_frame_num: u32,
|
2017-03-02 22:29:28 -05:00
|
|
|
num_subtitle_samples: u16,
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2017-10-09 09:32:43 -04:00
|
|
|
// Manually implement Debug because `index` and `index_once` are not Debug.
|
|
|
|
impl fmt::Debug for Segment {
|
|
|
|
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
fmt.debug_struct("mp4::Segment")
|
|
|
|
.field("s", &self.s)
|
2020-11-28 00:56:15 -05:00
|
|
|
.field("recording_start", &self.recording_start)
|
|
|
|
.field("recording_wall_duration_90k", &self.recording_wall_duration_90k)
|
|
|
|
.field("recording_media_duration_90k", &self.recording_media_duration_90k)
|
|
|
|
.field("rel_media_range_90k", &self.rel_media_range_90k)
|
2017-10-09 09:32:43 -04:00
|
|
|
.field("first_frame_num", &self.first_frame_num)
|
|
|
|
.field("num_subtitle_samples", &self.num_subtitle_samples)
|
|
|
|
.finish()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-02 22:29:28 -05:00
|
|
|
unsafe impl Sync for Segment {}
|
|
|
|
|
2017-02-25 00:33:26 -05:00
|
|
|
impl Segment {
|
2020-08-07 13:16:06 -04:00
|
|
|
fn new(db: &db::LockedDatabase, row: &db::ListRecordingsRow, rel_media_range_90k: Range<i32>,
|
2020-08-07 18:30:22 -04:00
|
|
|
first_frame_num: u32, start_at_key: bool) -> Result<Self, Error> {
|
2020-08-05 00:44:01 -04:00
|
|
|
Ok(Segment {
|
2020-08-07 18:30:22 -04:00
|
|
|
s: recording::Segment::new(db, row, rel_media_range_90k.clone(), start_at_key)
|
2020-08-07 13:16:06 -04:00
|
|
|
.err_kind(ErrorKind::Unknown)?,
|
2020-08-05 00:44:01 -04:00
|
|
|
recording_start: row.start,
|
|
|
|
recording_wall_duration_90k: row.wall_duration_90k,
|
|
|
|
recording_media_duration_90k: row.media_duration_90k,
|
2020-08-07 13:16:06 -04:00
|
|
|
rel_media_range_90k,
|
2017-03-02 22:29:28 -05:00
|
|
|
index: UnsafeCell::new(Err(())),
|
2019-05-31 18:08:49 -04:00
|
|
|
index_once: Once::new(),
|
2017-10-09 09:32:43 -04:00
|
|
|
first_frame_num,
|
2017-02-26 23:10:02 -05:00
|
|
|
num_subtitle_samples: 0,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2020-08-07 13:16:06 -04:00
|
|
|
fn wall(&self, rel_media_90k: i32) -> i32 {
|
|
|
|
rescale(rel_media_90k, self.recording_media_duration_90k, self.recording_wall_duration_90k)
|
|
|
|
}
|
|
|
|
|
2020-08-05 00:44:01 -04:00
|
|
|
fn media(&self, rel_wall_90k: i32) -> i32 {
|
2020-08-07 13:16:06 -04:00
|
|
|
rescale(rel_wall_90k, self.recording_wall_duration_90k, self.recording_media_duration_90k)
|
2020-08-05 00:44:01 -04:00
|
|
|
}
|
|
|
|
|
2017-03-02 22:29:28 -05:00
|
|
|
fn get_index<'a, F>(&'a self, db: &db::Database, f: F) -> Result<&'a [u8], Error>
|
2017-02-26 03:02:49 -05:00
|
|
|
where F: FnOnce(&[u8], SegmentLengths) -> &[u8] {
|
2017-03-02 22:29:28 -05:00
|
|
|
self.index_once.call_once(|| {
|
|
|
|
let index = unsafe { &mut *self.index.get() };
|
|
|
|
*index = db.lock()
|
2018-08-24 01:34:40 -04:00
|
|
|
.with_recording_playback(self.s.id, &mut |playback| self.build_index(playback))
|
|
|
|
.map_err(|e| { error!("Unable to build index for segment: {:?}", e); });
|
2017-02-26 03:02:49 -05:00
|
|
|
});
|
2017-03-02 22:29:28 -05:00
|
|
|
let index: &'a _ = unsafe { &*self.index.get() };
|
|
|
|
match *index {
|
|
|
|
Ok(ref b) => return Ok(f(&b[..], self.lens())),
|
2018-12-28 18:30:33 -05:00
|
|
|
Err(()) => bail_t!(Unknown, "Unable to build index; see previous error."),
|
2017-03-02 22:29:28 -05:00
|
|
|
}
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2017-02-26 23:10:02 -05:00
|
|
|
fn lens(&self) -> SegmentLengths {
|
|
|
|
SegmentLengths {
|
|
|
|
stts: mem::size_of::<u32>() * 2 * (self.s.frames as usize),
|
|
|
|
stsz: mem::size_of::<u32>() * self.s.frames as usize,
|
|
|
|
stss: mem::size_of::<u32>() * self.s.key_frames as usize,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-26 03:02:49 -05:00
|
|
|
fn stts(buf: &[u8], lens: SegmentLengths) -> &[u8] { &buf[.. lens.stts] }
|
|
|
|
fn stsz(buf: &[u8], lens: SegmentLengths) -> &[u8] { &buf[lens.stts .. lens.stts + lens.stsz] }
|
|
|
|
fn stss(buf: &[u8], lens: SegmentLengths) -> &[u8] { &buf[lens.stts + lens.stsz ..] }
|
|
|
|
|
2018-12-28 18:30:33 -05:00
|
|
|
fn build_index(&self, playback: &db::RecordingPlayback) -> Result<Box<[u8]>, failure::Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
let s = &self.s;
|
2017-02-26 23:10:02 -05:00
|
|
|
let lens = self.lens();
|
2017-02-26 03:02:49 -05:00
|
|
|
let len = lens.stts + lens.stsz + lens.stss;
|
|
|
|
let mut buf = {
|
2016-12-08 00:05:49 -05:00
|
|
|
let mut v = Vec::with_capacity(len);
|
2017-02-26 03:02:49 -05:00
|
|
|
unsafe { v.set_len(len) };
|
2016-12-08 00:05:49 -05:00
|
|
|
v.into_boxed_slice()
|
|
|
|
};
|
2017-10-01 18:29:22 -04:00
|
|
|
|
2016-11-25 17:34:00 -05:00
|
|
|
{
|
2017-09-22 00:51:58 -04:00
|
|
|
let (stts, rest) = buf.split_at_mut(lens.stts);
|
2017-02-26 03:02:49 -05:00
|
|
|
let (stsz, stss) = rest.split_at_mut(lens.stsz);
|
2016-11-25 17:34:00 -05:00
|
|
|
let mut frame = 0;
|
|
|
|
let mut key_frame = 0;
|
|
|
|
let mut last_start_and_dur = None;
|
2017-03-01 02:28:25 -05:00
|
|
|
s.foreach(playback, |it| {
|
2016-11-25 17:34:00 -05:00
|
|
|
last_start_and_dur = Some((it.start_90k, it.duration_90k));
|
|
|
|
BigEndian::write_u32(&mut stts[8*frame .. 8*frame+4], 1);
|
|
|
|
BigEndian::write_u32(&mut stts[8*frame+4 .. 8*frame+8], it.duration_90k as u32);
|
|
|
|
BigEndian::write_u32(&mut stsz[4*frame .. 4*frame+4], it.bytes as u32);
|
2017-02-28 00:14:06 -05:00
|
|
|
if it.is_key() {
|
2016-11-25 17:34:00 -05:00
|
|
|
BigEndian::write_u32(&mut stss[4*key_frame .. 4*key_frame+4],
|
|
|
|
self.first_frame_num + (frame as u32));
|
|
|
|
key_frame += 1;
|
|
|
|
}
|
|
|
|
frame += 1;
|
|
|
|
Ok(())
|
|
|
|
})?;
|
|
|
|
|
|
|
|
// Fix up the final frame's duration.
|
|
|
|
// Doing this after the fact is more efficient than having a condition on every
|
|
|
|
// iteration.
|
|
|
|
if let Some((last_start, dur)) = last_start_and_dur {
|
2020-08-07 13:16:06 -04:00
|
|
|
let min = cmp::min(self.rel_media_range_90k.end - last_start, dur);
|
2020-08-05 00:44:01 -04:00
|
|
|
BigEndian::write_u32(&mut stts[8*frame-4 ..], u32::try_from(min).unwrap());
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
}
|
2017-10-01 18:29:22 -04:00
|
|
|
|
2017-02-26 03:02:49 -05:00
|
|
|
Ok(buf)
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
2017-10-01 18:29:22 -04:00
|
|
|
|
|
|
|
fn truns_len(&self) -> usize {
|
2020-08-07 18:30:22 -04:00
|
|
|
self.s.key_frames as usize * (mem::size_of::<u32>() * 6) +
|
|
|
|
self.s.frames as usize * (mem::size_of::<u32>() * 2) +
|
|
|
|
if self.s.starts_with_nonkey() { mem::size_of::<u32>() * 5 } else { 0 }
|
2017-10-01 18:29:22 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// TrackRunBox / trun (8.8.8).
|
|
|
|
fn truns(&self, playback: &db::RecordingPlayback, initial_pos: u64, len: usize)
|
2018-12-28 18:30:33 -05:00
|
|
|
-> Result<Vec<u8>, failure::Error> {
|
2017-10-01 18:29:22 -04:00
|
|
|
let mut v = Vec::with_capacity(len);
|
|
|
|
|
|
|
|
struct RunInfo {
|
|
|
|
box_len_pos: usize,
|
|
|
|
sample_count_pos: usize,
|
|
|
|
count: u32,
|
|
|
|
last_start: i32,
|
|
|
|
last_dur: i32,
|
|
|
|
}
|
|
|
|
let mut run_info: Option<RunInfo> = None;
|
|
|
|
let mut data_pos = initial_pos;
|
|
|
|
self.s.foreach(playback, |it| {
|
2020-08-07 18:30:22 -04:00
|
|
|
let is_key = it.is_key();
|
|
|
|
if is_key {
|
2017-10-01 18:29:22 -04:00
|
|
|
if let Some(r) = run_info.take() {
|
|
|
|
// Finish a non-terminal run.
|
|
|
|
let p = v.len();
|
|
|
|
BigEndian::write_u32(&mut v[r.box_len_pos .. r.box_len_pos + 4],
|
|
|
|
(p - r.box_len_pos) as u32);
|
|
|
|
BigEndian::write_u32(&mut v[r.sample_count_pos .. r.sample_count_pos + 4],
|
|
|
|
r.count);
|
|
|
|
}
|
|
|
|
}
|
2020-08-07 18:30:22 -04:00
|
|
|
let mut r = match run_info.take() {
|
|
|
|
None => {
|
|
|
|
let box_len_pos = v.len();
|
|
|
|
v.extend_from_slice(&[
|
|
|
|
0x00, 0x00, 0x00, 0x00, // placeholder for size
|
|
|
|
b't', b'r', b'u', b'n',
|
|
|
|
|
|
|
|
// version 0, tr_flags:
|
|
|
|
// 0x000001 data-offset-present
|
|
|
|
// 0x000004 first-sample-flags-present
|
|
|
|
// 0x000100 sample-duration-present
|
|
|
|
// 0x000200 sample-size-present
|
|
|
|
0x00, 0x00, 0x03, 0x01 | if is_key { 0x04 } else { 0 },
|
|
|
|
]);
|
|
|
|
let sample_count_pos = v.len();
|
|
|
|
v.write_u32::<BigEndian>(0)?; // placeholder for sample count
|
|
|
|
v.write_u32::<BigEndian>(data_pos as u32)?;
|
|
|
|
|
|
|
|
if is_key {
|
|
|
|
// first_sample_flags. See trex (8.8.3.1).
|
|
|
|
v.write_u32::<BigEndian>(
|
|
|
|
// As defined by the Independent and Disposable Samples Box
|
|
|
|
// (sdp, 8.6.4).
|
|
|
|
(2 << 26) | // is_leading: this sample is not a leading sample
|
|
|
|
(2 << 24) | // sample_depends_on: this sample does not depend on others
|
|
|
|
(1 << 22) | // sample_is_depend_on: others may depend on this one
|
|
|
|
(2 << 20) | // sample_has_redundancy: no redundant coding
|
|
|
|
// As defined by the sample padding bits (padb, 8.7.6).
|
|
|
|
(0 << 17) | // no padding
|
|
|
|
(0 << 16) | // sample_is_non_sync_sample=0
|
|
|
|
0)?; // TODO: sample_degradation_priority
|
|
|
|
}
|
|
|
|
RunInfo {
|
|
|
|
box_len_pos,
|
|
|
|
sample_count_pos,
|
|
|
|
count: 0,
|
|
|
|
last_start: 0,
|
|
|
|
last_dur: 0,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
Some(r) => r,
|
|
|
|
};
|
|
|
|
r.count += 1;
|
|
|
|
r.last_start = it.start_90k;
|
|
|
|
r.last_dur = it.duration_90k;
|
2017-10-01 18:29:22 -04:00
|
|
|
v.write_u32::<BigEndian>(it.duration_90k as u32)?;
|
|
|
|
v.write_u32::<BigEndian>(it.bytes as u32)?;
|
|
|
|
data_pos += it.bytes as u64;
|
2020-08-07 18:30:22 -04:00
|
|
|
run_info = Some(r);
|
2017-10-01 18:29:22 -04:00
|
|
|
Ok(())
|
2018-12-28 18:30:33 -05:00
|
|
|
}).err_kind(ErrorKind::Internal)?;
|
2017-10-01 18:29:22 -04:00
|
|
|
if let Some(r) = run_info.take() {
|
|
|
|
// Finish the run as in the non-terminal case above.
|
|
|
|
let p = v.len();
|
|
|
|
BigEndian::write_u32(&mut v[r.box_len_pos .. r.box_len_pos + 4],
|
|
|
|
(p - r.box_len_pos) as u32);
|
|
|
|
BigEndian::write_u32(&mut v[r.sample_count_pos .. r.sample_count_pos + 4], r.count);
|
|
|
|
|
|
|
|
// One more thing to do in the terminal case: fix up the final frame's duration.
|
|
|
|
// Doing this after the fact is more efficient than having a condition on every
|
|
|
|
// iteration.
|
2020-08-07 13:16:06 -04:00
|
|
|
BigEndian::write_u32(
|
|
|
|
&mut v[p-8 .. p-4],
|
|
|
|
u32::try_from(cmp::min(self.rel_media_range_90k.end - r.last_start, r.last_dur))
|
|
|
|
.unwrap());
|
2017-10-01 18:29:22 -04:00
|
|
|
|
|
|
|
}
|
2020-08-07 18:30:22 -04:00
|
|
|
if len != v.len() {
|
|
|
|
bail_t!(Internal, "truns on {:?} expected len {} got len {}", self, len, v.len());
|
|
|
|
}
|
2017-10-01 18:29:22 -04:00
|
|
|
Ok(v)
|
|
|
|
}
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2017-02-25 00:33:26 -05:00
|
|
|
pub struct FileBuilder {
|
2016-11-25 17:34:00 -05:00
|
|
|
/// Segments of video: one per "recording" table entry as they should
|
|
|
|
/// appear in the video.
|
2017-02-25 00:33:26 -05:00
|
|
|
segments: Vec<Segment>,
|
2016-12-02 23:40:55 -05:00
|
|
|
video_sample_entries: SmallVec<[Arc<db::VideoSampleEntry>; 1]>,
|
2016-11-25 17:34:00 -05:00
|
|
|
next_frame_num: u32,
|
2020-08-05 00:44:01 -04:00
|
|
|
|
|
|
|
/// The total media time, after applying edit lists (if applicable) to skip unwanted portions.
|
|
|
|
media_duration_90k: u64,
|
2016-11-25 17:34:00 -05:00
|
|
|
num_subtitle_samples: u32,
|
|
|
|
subtitle_co64_pos: Option<usize>,
|
|
|
|
body: BodyState,
|
2017-10-01 18:29:22 -04:00
|
|
|
type_: Type,
|
2020-08-05 00:44:01 -04:00
|
|
|
prev_media_duration_and_cur_runs: Option<(recording::Duration, i32)>,
|
2016-11-25 17:34:00 -05:00
|
|
|
include_timestamp_subtitle_track: bool,
|
2020-02-17 02:16:19 -05:00
|
|
|
content_disposition: Option<HeaderValue>,
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2017-02-25 00:33:26 -05:00
|
|
|
/// The portion of `FileBuilder` which is mutated while building the body of the file.
|
2016-11-25 17:34:00 -05:00
|
|
|
/// This is separated out from the rest so that it can be borrowed in a loop over
|
2017-02-25 00:33:26 -05:00
|
|
|
/// `FileBuilder::segments`; otherwise this would cause a double-self-borrow.
|
2016-11-25 17:34:00 -05:00
|
|
|
struct BodyState {
|
2017-02-25 21:54:52 -05:00
|
|
|
slices: Slices<Slice>,
|
2016-11-25 17:34:00 -05:00
|
|
|
|
|
|
|
/// `self.buf[unflushed_buf_pos .. self.buf.len()]` holds bytes that should be
|
|
|
|
/// appended to `slices` before any other slice. See `flush_buf()`.
|
|
|
|
unflushed_buf_pos: usize,
|
|
|
|
buf: Vec<u8>,
|
|
|
|
}
|
|
|
|
|
2017-02-25 00:33:26 -05:00
|
|
|
/// A single slice of a `File`, for use with a `Slices` object. Each slice is responsible for
|
2016-12-02 23:40:55 -05:00
|
|
|
/// some portion of the generated `.mp4` file. The box headers and such are generally in `Static`
|
|
|
|
/// or `Buf` slices; the others generally represent a single segment's contribution to the
|
|
|
|
/// like-named box.
|
2017-02-21 22:37:36 -05:00
|
|
|
///
|
|
|
|
/// This is stored in a packed representation to be more cache-efficient:
|
|
|
|
///
|
|
|
|
/// * low 40 bits: end() (maximum 1 TiB).
|
|
|
|
/// * next 4 bits: t(), the SliceType.
|
|
|
|
/// * top 20 bits: p(), a parameter specified by the SliceType (maximum 1 Mi).
|
|
|
|
struct Slice(u64);
|
|
|
|
|
|
|
|
/// The type of a `Slice`.
|
|
|
|
#[derive(Copy, Clone, Debug)]
|
|
|
|
#[repr(u8)]
|
|
|
|
enum SliceType {
|
|
|
|
Static = 0, // param is index into STATIC_BYTESTRINGS
|
|
|
|
Buf = 1, // param is index into m.buf
|
|
|
|
VideoSampleEntry = 2, // param is index into m.video_sample_entries
|
|
|
|
Stts = 3, // param is index into m.segments
|
|
|
|
Stsz = 4, // param is index into m.segments
|
2017-03-02 22:29:28 -05:00
|
|
|
Stss = 5, // param is index into m.segments
|
|
|
|
Co64 = 6, // param is unused
|
2017-02-21 22:37:36 -05:00
|
|
|
VideoSampleData = 7, // param is index into m.segments
|
|
|
|
SubtitleSampleData = 8, // param is index into m.segments
|
2017-10-01 18:29:22 -04:00
|
|
|
Truns = 9, // param is index into m.segments
|
2017-02-21 22:37:36 -05:00
|
|
|
|
|
|
|
// There must be no value > 15, as this is packed into 4 bits in Slice.
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2017-02-21 22:37:36 -05:00
|
|
|
impl Slice {
|
|
|
|
fn new(end: u64, t: SliceType, p: usize) -> Result<Self, Error> {
|
|
|
|
if end >= (1<<40) || p >= (1<<20) {
|
2018-12-28 18:30:33 -05:00
|
|
|
bail_t!(InvalidArgument, "end={} p={} too large for {:?} Slice", end, p, t);
|
2017-02-21 22:37:36 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(Slice(end | ((t as u64) << 40) | ((p as u64) << 44)))
|
|
|
|
}
|
|
|
|
|
|
|
|
fn t(&self) -> SliceType {
|
|
|
|
// This value is guaranteed to be a valid SliceType because it was copied from a SliceType
|
|
|
|
// in Slice::new.
|
|
|
|
unsafe { ::std::mem::transmute(((self.0 >> 40) & 0xF) as u8) }
|
|
|
|
}
|
|
|
|
fn p(&self) -> usize { (self.0 >> 44) as usize }
|
2017-03-02 22:29:28 -05:00
|
|
|
|
2020-08-07 18:30:22 -04:00
|
|
|
fn wrap_index<F>(&self, mp4: &File, r: Range<u64>, len: u64, f: &F) -> Result<Chunk, Error>
|
2017-03-02 22:29:28 -05:00
|
|
|
where F: Fn(&[u8], SegmentLengths) -> &[u8] {
|
2020-01-09 02:04:36 -05:00
|
|
|
let mp4 = ARefss::new(mp4.0.clone());
|
2017-03-02 22:29:28 -05:00
|
|
|
let r = r.start as usize .. r.end as usize;
|
|
|
|
let p = self.p();
|
2020-08-07 18:30:22 -04:00
|
|
|
Ok(mp4.try_map(|mp4| {
|
|
|
|
let i = mp4.segments[p].get_index(&mp4.db, f)?;
|
|
|
|
if u64::try_from(i.len()).unwrap() != len {
|
|
|
|
bail_t!(Internal, "expected len {} got {}", len, i.len());
|
|
|
|
}
|
|
|
|
Ok::<_, Error>(&i[r])
|
|
|
|
})?.into())
|
2017-03-02 22:29:28 -05:00
|
|
|
}
|
2017-10-01 18:29:22 -04:00
|
|
|
|
|
|
|
fn wrap_truns(&self, mp4: &File, r: Range<u64>, len: usize) -> Result<Chunk, Error> {
|
|
|
|
let s = &mp4.0.segments[self.p()];
|
|
|
|
let mut pos = mp4.0.initial_sample_byte_pos;
|
|
|
|
for ps in &mp4.0.segments[0 .. self.p()] {
|
|
|
|
let r = ps.s.sample_file_range();
|
|
|
|
pos += r.end - r.start;
|
|
|
|
}
|
|
|
|
let truns =
|
|
|
|
mp4.0.db.lock()
|
2018-12-28 18:30:33 -05:00
|
|
|
.with_recording_playback(s.s.id, &mut |playback| s.truns(playback, pos, len))
|
|
|
|
.err_kind(ErrorKind::Unknown)?;
|
2020-01-09 02:04:36 -05:00
|
|
|
let truns = ARefss::new(truns);
|
2018-08-30 01:26:19 -04:00
|
|
|
Ok(truns.map(|t| &t[r.start as usize .. r.end as usize]).into())
|
2017-10-01 18:29:22 -04:00
|
|
|
}
|
2020-08-07 18:30:22 -04:00
|
|
|
|
|
|
|
fn wrap_video_sample_entry(&self, f: &File, r: Range<u64>, len: u64) -> Result<Chunk, Error> {
|
|
|
|
let mp4 = ARefss::new(f.0.clone());
|
|
|
|
Ok(mp4.try_map(|mp4| {
|
|
|
|
let data = &mp4.video_sample_entries[self.p()].data;
|
|
|
|
if u64::try_from(data.len()).unwrap() != len {
|
|
|
|
bail_t!(Internal, "expected len {} got len {}", len, data.len());
|
|
|
|
}
|
|
|
|
Ok::<_, Error>(&data[r.start as usize .. r.end as usize])
|
|
|
|
})?.into())
|
|
|
|
}
|
2017-02-21 22:37:36 -05:00
|
|
|
}
|
|
|
|
|
2017-02-25 21:54:52 -05:00
|
|
|
impl slices::Slice for Slice {
|
|
|
|
type Ctx = File;
|
2018-08-30 01:26:19 -04:00
|
|
|
type Chunk = Chunk;
|
2017-02-25 21:54:52 -05:00
|
|
|
|
2017-02-21 22:37:36 -05:00
|
|
|
fn end(&self) -> u64 { return self.0 & 0xFF_FF_FF_FF_FF }
|
2018-08-30 01:26:19 -04:00
|
|
|
fn get_range(&self, f: &File, range: Range<u64>, len: u64)
|
2020-01-09 02:04:36 -05:00
|
|
|
-> Box<dyn Stream<Item = Result<Self::Chunk, BoxedError>> + Send + Sync> {
|
2017-03-02 22:29:28 -05:00
|
|
|
trace!("getting mp4 slice {:?}'s range {:?} / {}", self, range, len);
|
2017-02-21 22:37:36 -05:00
|
|
|
let p = self.p();
|
2017-03-02 22:29:28 -05:00
|
|
|
let res = match self.t() {
|
2017-02-21 22:37:36 -05:00
|
|
|
SliceType::Static => {
|
|
|
|
let s = STATIC_BYTESTRINGS[p];
|
2020-08-07 18:30:22 -04:00
|
|
|
if u64::try_from(s.len()).unwrap() != len {
|
|
|
|
Err(format_err_t!(Internal, "expected len {} got len {}", len, s.len()))
|
|
|
|
} else {
|
|
|
|
let part = &s[range.start as usize .. range.end as usize];
|
|
|
|
Ok(part.into())
|
|
|
|
}
|
2016-11-25 17:34:00 -05:00
|
|
|
},
|
2017-02-21 22:37:36 -05:00
|
|
|
SliceType::Buf => {
|
2020-01-09 02:04:36 -05:00
|
|
|
let r = ARefss::new(f.0.clone());
|
2018-08-30 01:26:19 -04:00
|
|
|
Ok(r.map(|f| &f.buf[p+range.start as usize .. p+range.end as usize]).into())
|
2016-11-25 17:34:00 -05:00
|
|
|
},
|
2020-08-07 18:30:22 -04:00
|
|
|
SliceType::VideoSampleEntry => self.wrap_video_sample_entry(f, range.clone(), len),
|
|
|
|
SliceType::Stts => self.wrap_index(f, range.clone(), len, &Segment::stts),
|
|
|
|
SliceType::Stsz => self.wrap_index(f, range.clone(), len, &Segment::stsz),
|
|
|
|
SliceType::Stss => self.wrap_index(f, range.clone(), len, &Segment::stss),
|
2017-03-02 22:29:28 -05:00
|
|
|
SliceType::Co64 => f.0.get_co64(range.clone(), len),
|
|
|
|
SliceType::VideoSampleData => f.0.get_video_sample_data(p, range.clone()),
|
|
|
|
SliceType::SubtitleSampleData => f.0.get_subtitle_sample_data(p, range.clone(), len),
|
2017-10-01 18:29:22 -04:00
|
|
|
SliceType::Truns => self.wrap_truns(f, range.clone(), len as usize),
|
2017-03-02 22:29:28 -05:00
|
|
|
};
|
2020-01-09 02:04:36 -05:00
|
|
|
Box::new(stream::once(futures::future::ready(res
|
2018-08-30 01:26:19 -04:00
|
|
|
.map_err(|e| wrap_error(e))
|
2017-03-02 22:29:28 -05:00
|
|
|
.and_then(move |c| {
|
2018-08-30 01:26:19 -04:00
|
|
|
if c.remaining() != (range.end - range.start) as usize {
|
2018-12-28 18:30:33 -05:00
|
|
|
return Err(wrap_error(format_err_t!(
|
|
|
|
Internal,
|
2018-08-30 01:26:19 -04:00
|
|
|
"Error producing {:?}: range {:?} produced incorrect len {}.",
|
|
|
|
self, range, c.remaining())));
|
2017-03-02 22:29:28 -05:00
|
|
|
}
|
|
|
|
Ok(c)
|
2020-01-09 02:04:36 -05:00
|
|
|
}))))
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
2017-03-02 22:29:28 -05:00
|
|
|
|
|
|
|
fn get_slices(ctx: &File) -> &Slices<Self> { &ctx.0.slices }
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2018-12-29 14:06:44 -05:00
|
|
|
impl fmt::Debug for Slice {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
2017-02-21 22:37:36 -05:00
|
|
|
// Write an unpacked representation. Omit end(); Slices writes that part.
|
|
|
|
write!(f, "{:?} {}", self.t(), self.p())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-10-01 18:29:22 -04:00
|
|
|
/// Converts from seconds since Unix epoch (1970-01-01 00:00:00 UTC) to seconds since
|
2016-12-02 23:40:55 -05:00
|
|
|
/// ISO-14496 epoch (1904-01-01 00:00:00 UTC).
|
2017-10-01 18:29:22 -04:00
|
|
|
fn to_iso14496_timestamp(unix_secs: i64) -> u32 { unix_secs as u32 + 24107 * 86400 }
|
2016-11-25 17:34:00 -05:00
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Writes a box length for everything appended in the supplied scope.
|
2017-02-25 00:33:26 -05:00
|
|
|
/// Used only within FileBuilder::build (and methods it calls internally).
|
2016-11-25 17:34:00 -05:00
|
|
|
macro_rules! write_length {
|
|
|
|
($_self:ident, $b:block) => {{
|
|
|
|
let len_pos = $_self.body.buf.len();
|
|
|
|
let len_start = $_self.body.slices.len() + $_self.body.buf.len() as u64 -
|
|
|
|
$_self.body.unflushed_buf_pos as u64;
|
|
|
|
$_self.body.append_u32(0); // placeholder.
|
|
|
|
{ $b; }
|
|
|
|
let len_end = $_self.body.slices.len() + $_self.body.buf.len() as u64 -
|
|
|
|
$_self.body.unflushed_buf_pos as u64;
|
|
|
|
BigEndian::write_u32(&mut $_self.body.buf[len_pos .. len_pos + 4],
|
|
|
|
(len_end - len_start) as u32);
|
2017-02-21 22:37:36 -05:00
|
|
|
Ok::<_, Error>(())
|
2016-11-25 17:34:00 -05:00
|
|
|
}}
|
|
|
|
}
|
|
|
|
|
2020-02-17 02:16:19 -05:00
|
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
2017-10-01 18:29:22 -04:00
|
|
|
pub enum Type {
|
|
|
|
Normal,
|
|
|
|
InitSegment,
|
|
|
|
MediaSegment,
|
|
|
|
}
|
|
|
|
|
2017-02-25 00:33:26 -05:00
|
|
|
impl FileBuilder {
|
2017-10-01 18:29:22 -04:00
|
|
|
pub fn new(type_: Type) -> Self {
|
2017-03-02 22:29:28 -05:00
|
|
|
FileBuilder {
|
2016-11-25 17:34:00 -05:00
|
|
|
segments: Vec::new(),
|
|
|
|
video_sample_entries: SmallVec::new(),
|
|
|
|
next_frame_num: 1,
|
2020-08-05 00:44:01 -04:00
|
|
|
media_duration_90k: 0,
|
2016-11-25 17:34:00 -05:00
|
|
|
num_subtitle_samples: 0,
|
|
|
|
subtitle_co64_pos: None,
|
|
|
|
body: BodyState{
|
|
|
|
slices: Slices::new(),
|
|
|
|
buf: Vec::new(),
|
|
|
|
unflushed_buf_pos: 0,
|
|
|
|
},
|
2017-10-01 18:29:22 -04:00
|
|
|
type_: type_,
|
2016-11-25 17:34:00 -05:00
|
|
|
include_timestamp_subtitle_track: false,
|
2020-02-17 02:16:19 -05:00
|
|
|
content_disposition: None,
|
2020-08-05 00:44:01 -04:00
|
|
|
prev_media_duration_and_cur_runs: None,
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Sets if the generated `.mp4` should include a subtitle track with second-level timestamps.
|
|
|
|
/// Default is false.
|
2020-08-05 00:44:01 -04:00
|
|
|
pub fn include_timestamp_subtitle_track(&mut self, b: bool) -> Result<(), Error> {
|
|
|
|
if b && self.type_ == Type::MediaSegment {
|
|
|
|
// There's no support today for timestamp truns or for timestamps without edit lists.
|
|
|
|
// The latter would invalidate the code's assumption that desired timespan == actual
|
|
|
|
// timespan in the timestamp track.
|
|
|
|
bail_t!(InvalidArgument, "timestamp subtitles aren't supported on media segments");
|
|
|
|
}
|
2016-11-25 17:34:00 -05:00
|
|
|
self.include_timestamp_subtitle_track = b;
|
2020-08-05 00:44:01 -04:00
|
|
|
Ok(())
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Reserves space for the given number of additional segments.
|
2016-11-25 17:34:00 -05:00
|
|
|
pub fn reserve(&mut self, additional: usize) {
|
|
|
|
self.segments.reserve(additional);
|
|
|
|
}
|
|
|
|
|
2017-10-01 18:29:22 -04:00
|
|
|
pub fn append_video_sample_entry(&mut self, ent: Arc<db::VideoSampleEntry>) {
|
|
|
|
self.video_sample_entries.push(ent);
|
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a segment for (a subset of) the given recording.
|
2020-08-07 13:16:06 -04:00
|
|
|
/// `rel_media_range_90k` is the media time range within the recording.
|
|
|
|
/// Eg `0 .. row.media_duration_90k` means the full recording.
|
2017-02-25 00:33:26 -05:00
|
|
|
pub fn append(&mut self, db: &db::LockedDatabase, row: db::ListRecordingsRow,
|
2020-08-07 18:30:22 -04:00
|
|
|
rel_media_range_90k: Range<i32>, start_at_key: bool) -> Result<(), Error> {
|
2016-12-21 01:08:18 -05:00
|
|
|
if let Some(prev) = self.segments.last() {
|
2017-02-28 00:14:06 -05:00
|
|
|
if prev.s.have_trailing_zero() {
|
2018-12-28 18:30:33 -05:00
|
|
|
bail_t!(InvalidArgument,
|
|
|
|
"unable to append recording {} after recording {} with trailing zero",
|
|
|
|
row.id, prev.s.id);
|
2016-12-21 01:08:18 -05:00
|
|
|
}
|
2020-06-10 01:06:03 -04:00
|
|
|
} else {
|
|
|
|
// Include the current run in this count here, as we're not propagating the
|
|
|
|
// run_offset_id further.
|
2020-08-05 00:44:01 -04:00
|
|
|
self.prev_media_duration_and_cur_runs = row.prev_media_duration_and_runs
|
2020-06-10 01:06:03 -04:00
|
|
|
.map(|(d, r)| (d, r + if row.open_id == 0 { 1 } else { 0 }));
|
2016-12-21 01:08:18 -05:00
|
|
|
}
|
2020-08-07 18:30:22 -04:00
|
|
|
let s = Segment::new(db, &row, rel_media_range_90k, self.next_frame_num, start_at_key)?;
|
2017-10-09 09:32:43 -04:00
|
|
|
|
|
|
|
self.next_frame_num += s.s.frames as u32;
|
|
|
|
self.segments.push(s);
|
2018-03-01 23:59:05 -05:00
|
|
|
if !self.video_sample_entries.iter().any(|e| e.id == row.video_sample_entry_id) {
|
|
|
|
let vse = db.video_sample_entries_by_id().get(&row.video_sample_entry_id).unwrap();
|
|
|
|
self.video_sample_entries.push(vse.clone());
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
2016-12-02 23:40:55 -05:00
|
|
|
Ok(())
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2020-02-17 02:16:19 -05:00
|
|
|
pub fn set_filename(&mut self, filename: &str) -> Result<(), Error> {
|
|
|
|
self.content_disposition =
|
|
|
|
Some(HeaderValue::try_from(format!("attachment; filename=\"{}\"", filename))
|
|
|
|
.err_kind(ErrorKind::InvalidArgument)?);
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2017-02-25 00:33:26 -05:00
|
|
|
/// Builds the `File`, consuming the builder.
|
2018-02-12 01:45:51 -05:00
|
|
|
pub fn build(mut self, db: Arc<db::Database>,
|
|
|
|
dirs_by_stream_id: Arc<::fnv::FnvHashMap<i32, Arc<dir::SampleFileDir>>>)
|
2017-02-25 00:33:26 -05:00
|
|
|
-> Result<File, Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
let mut max_end = None;
|
2020-03-20 23:52:30 -04:00
|
|
|
let mut etag = blake3::Hasher::new();
|
|
|
|
etag.update(&FORMAT_VERSION[..]);
|
2016-11-25 17:34:00 -05:00
|
|
|
if self.include_timestamp_subtitle_track {
|
2020-03-20 23:52:30 -04:00
|
|
|
etag.update(b":ts:");
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
2020-02-17 02:16:19 -05:00
|
|
|
if let Some(cd) = self.content_disposition.as_ref() {
|
2020-03-20 23:52:30 -04:00
|
|
|
etag.update(b":cd:");
|
|
|
|
etag.update(cd.as_bytes());
|
2020-02-17 02:16:19 -05:00
|
|
|
}
|
2017-10-01 18:29:22 -04:00
|
|
|
match self.type_ {
|
|
|
|
Type::Normal => {},
|
2020-03-20 23:52:30 -04:00
|
|
|
Type::InitSegment => { etag.update(b":init:"); },
|
|
|
|
Type::MediaSegment => { etag.update(b":media:"); },
|
2017-10-01 18:29:22 -04:00
|
|
|
};
|
2016-11-25 17:34:00 -05:00
|
|
|
for s in &mut self.segments {
|
2020-08-07 13:16:06 -04:00
|
|
|
let md = &s.rel_media_range_90k;
|
2020-08-05 00:44:01 -04:00
|
|
|
|
|
|
|
// Add the media time for this segment. If edit lists are supported (not media
|
|
|
|
// segments), this shouldn't include the portion they skip.
|
|
|
|
let start = match self.type_ {
|
|
|
|
Type::MediaSegment => s.s.actual_start_90k(),
|
|
|
|
_ => md.start,
|
|
|
|
};
|
|
|
|
self.media_duration_90k += u64::try_from(md.end - start).unwrap();
|
|
|
|
let wall =
|
2020-08-07 13:16:06 -04:00
|
|
|
s.recording_start + recording::Duration(i64::from(s.wall(md.start))) ..
|
|
|
|
s.recording_start + recording::Duration(i64::from(s.wall(md.end)));
|
2016-11-25 17:34:00 -05:00
|
|
|
max_end = match max_end {
|
2020-08-05 00:44:01 -04:00
|
|
|
None => Some(wall.end),
|
|
|
|
Some(v) => Some(cmp::max(v, wall.end)),
|
2016-11-25 17:34:00 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
if self.include_timestamp_subtitle_track {
|
|
|
|
// Calculate the number of subtitle samples: starting to ending time (rounding up).
|
2020-08-05 00:44:01 -04:00
|
|
|
let start_sec = wall.start.unix_seconds();
|
|
|
|
let end_sec =
|
|
|
|
(wall.end + recording::Duration(TIME_UNITS_PER_SEC - 1)).unix_seconds();
|
2017-03-02 22:29:28 -05:00
|
|
|
s.num_subtitle_samples = (end_sec - start_sec) as u16;
|
|
|
|
self.num_subtitle_samples += s.num_subtitle_samples as u32;
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Update the etag to reflect this segment.
|
2018-03-02 14:38:11 -05:00
|
|
|
let mut data = [0_u8; 28];
|
2016-11-25 17:34:00 -05:00
|
|
|
let mut cursor = io::Cursor::new(&mut data[..]);
|
2018-12-28 18:30:33 -05:00
|
|
|
cursor.write_i64::<BigEndian>(s.s.id.0).err_kind(ErrorKind::Internal)?;
|
2020-08-05 00:44:01 -04:00
|
|
|
cursor.write_i64::<BigEndian>(s.recording_start.0).err_kind(ErrorKind::Internal)?;
|
2018-12-28 18:30:33 -05:00
|
|
|
cursor.write_u32::<BigEndian>(s.s.open_id).err_kind(ErrorKind::Internal)?;
|
2020-08-07 13:16:06 -04:00
|
|
|
cursor.write_i32::<BigEndian>(md.start).err_kind(ErrorKind::Internal)?;
|
|
|
|
cursor.write_i32::<BigEndian>(md.end).err_kind(ErrorKind::Internal)?;
|
2020-03-20 23:52:30 -04:00
|
|
|
etag.update(cursor.into_inner());
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
let max_end = match max_end {
|
2017-10-01 18:29:22 -04:00
|
|
|
None => 0,
|
|
|
|
Some(v) => v.unix_seconds(),
|
2016-11-25 17:34:00 -05:00
|
|
|
};
|
|
|
|
let creation_ts = to_iso14496_timestamp(max_end);
|
|
|
|
let mut est_slices = 16 + self.video_sample_entries.len() + 4 * self.segments.len();
|
|
|
|
if self.include_timestamp_subtitle_track {
|
|
|
|
est_slices += 16 + self.segments.len();
|
|
|
|
}
|
|
|
|
self.body.slices.reserve(est_slices);
|
|
|
|
const EST_BUF_LEN: usize = 2048;
|
|
|
|
self.body.buf.reserve(EST_BUF_LEN);
|
2017-10-01 18:29:22 -04:00
|
|
|
let initial_sample_byte_pos = match self.type_ {
|
|
|
|
Type::MediaSegment => {
|
|
|
|
self.append_moof()?;
|
|
|
|
let p = self.append_mdat()?;
|
|
|
|
|
|
|
|
// If the segment is > 4 GiB, the 32-bit trun data offsets are untrustworthy.
|
|
|
|
// We'd need multiple moof+mdat sequences to support large media segments properly.
|
|
|
|
if self.body.slices.len() > u32::max_value() as u64 {
|
2018-12-28 18:30:33 -05:00
|
|
|
bail_t!(InvalidArgument,
|
|
|
|
"media segment has length {}, greater than allowed 4 GiB",
|
|
|
|
self.body.slices.len());
|
2017-10-01 18:29:22 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
p
|
|
|
|
},
|
|
|
|
Type::InitSegment => {
|
|
|
|
self.body.append_static(StaticBytestring::InitSegmentFtypBox)?;
|
|
|
|
self.append_moov(creation_ts)?;
|
|
|
|
self.body.flush_buf()?;
|
|
|
|
0
|
|
|
|
},
|
|
|
|
Type::Normal => {
|
|
|
|
self.body.append_static(StaticBytestring::NormalFtypBox)?;
|
|
|
|
self.append_moov(creation_ts)?;
|
|
|
|
self.append_mdat()?
|
|
|
|
},
|
|
|
|
};
|
|
|
|
|
|
|
|
if est_slices < self.body.slices.num() {
|
|
|
|
warn!("Estimated {} slices; actually were {} slices", est_slices,
|
|
|
|
self.body.slices.num());
|
|
|
|
} else {
|
|
|
|
debug!("Estimated {} slices; actually were {} slices", est_slices,
|
|
|
|
self.body.slices.num());
|
|
|
|
}
|
|
|
|
if EST_BUF_LEN < self.body.buf.len() {
|
|
|
|
warn!("Estimated {} buf bytes; actually were {}", EST_BUF_LEN, self.body.buf.len());
|
|
|
|
} else {
|
|
|
|
debug!("Estimated {} buf bytes; actually were {}", EST_BUF_LEN, self.body.buf.len());
|
|
|
|
}
|
2017-10-09 09:32:43 -04:00
|
|
|
debug!("segments: {:#?}", self.segments);
|
2017-10-01 18:29:22 -04:00
|
|
|
debug!("slices: {:?}", self.body.slices);
|
2018-08-30 01:26:19 -04:00
|
|
|
let last_modified = ::std::time::UNIX_EPOCH +
|
|
|
|
::std::time::Duration::from_secs(max_end as u64);
|
2020-03-20 23:52:30 -04:00
|
|
|
let etag = etag.finalize();
|
2017-10-01 18:29:22 -04:00
|
|
|
Ok(File(Arc::new(FileInner {
|
|
|
|
db,
|
2018-02-12 01:45:51 -05:00
|
|
|
dirs_by_stream_id,
|
2017-10-01 18:29:22 -04:00
|
|
|
segments: self.segments,
|
|
|
|
slices: self.body.slices,
|
|
|
|
buf: self.body.buf,
|
|
|
|
video_sample_entries: self.video_sample_entries,
|
|
|
|
initial_sample_byte_pos,
|
2018-08-30 01:26:19 -04:00
|
|
|
last_modified,
|
2020-03-20 23:52:30 -04:00
|
|
|
etag: HeaderValue::try_from(format!("\"{}\"", etag.to_hex().as_str()))
|
2018-08-30 01:26:19 -04:00
|
|
|
.expect("hex string should be valid UTF-8"),
|
2020-02-17 02:16:19 -05:00
|
|
|
content_disposition: self.content_disposition,
|
2020-08-05 00:44:01 -04:00
|
|
|
prev_media_duration_and_cur_runs: self.prev_media_duration_and_cur_runs,
|
2020-06-10 01:06:03 -04:00
|
|
|
type_: self.type_,
|
2017-10-01 18:29:22 -04:00
|
|
|
})))
|
|
|
|
}
|
2016-11-25 17:34:00 -05:00
|
|
|
|
2017-10-01 18:29:22 -04:00
|
|
|
fn append_mdat(&mut self) -> Result<u64, Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
// Write the mdat header. Use the large format to support files over 2^32-1 bytes long.
|
|
|
|
// Write zeroes for the length as a placeholder; fill it in after it's known.
|
|
|
|
// It'd be nice to use the until-EOF form, but QuickTime Player doesn't support it.
|
|
|
|
self.body.buf.extend_from_slice(b"\x00\x00\x00\x01mdat\x00\x00\x00\x00\x00\x00\x00\x00");
|
|
|
|
let mdat_len_pos = self.body.buf.len() - 8;
|
2017-02-21 22:37:36 -05:00
|
|
|
self.body.flush_buf()?;
|
2016-11-25 17:34:00 -05:00
|
|
|
let initial_sample_byte_pos = self.body.slices.len();
|
|
|
|
for (i, s) in self.segments.iter().enumerate() {
|
|
|
|
let r = s.s.sample_file_range();
|
2017-02-21 22:37:36 -05:00
|
|
|
self.body.append_slice(r.end - r.start, SliceType::VideoSampleData, i)?;
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
if let Some(p) = self.subtitle_co64_pos {
|
|
|
|
BigEndian::write_u64(&mut self.body.buf[p .. p + 8], self.body.slices.len());
|
|
|
|
for (i, s) in self.segments.iter().enumerate() {
|
2017-02-21 22:37:36 -05:00
|
|
|
self.body.append_slice(
|
2016-11-25 17:34:00 -05:00
|
|
|
s.num_subtitle_samples as u64 *
|
|
|
|
(mem::size_of::<u16>() + SUBTITLE_LENGTH) as u64,
|
2017-02-21 22:37:36 -05:00
|
|
|
SliceType::SubtitleSampleData, i)?;
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
// Fill in the length left as a placeholder above. Note the 16 here is the length
|
|
|
|
// of the mdat header.
|
|
|
|
BigEndian::write_u64(&mut self.body.buf[mdat_len_pos .. mdat_len_pos + 8],
|
|
|
|
16 + self.body.slices.len() - initial_sample_byte_pos);
|
2017-10-01 18:29:22 -04:00
|
|
|
Ok(initial_sample_byte_pos)
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `MovieBox` (ISO/IEC 14496-12 section 8.2.1).
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_moov(&mut self, creation_ts: u32) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"moov");
|
2017-02-21 22:37:36 -05:00
|
|
|
self.append_mvhd(creation_ts)?;
|
2016-11-25 17:34:00 -05:00
|
|
|
self.append_video_trak(creation_ts)?;
|
|
|
|
if self.include_timestamp_subtitle_track {
|
2017-02-21 22:37:36 -05:00
|
|
|
self.append_subtitle_trak(creation_ts)?;
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
2017-10-01 18:29:22 -04:00
|
|
|
if self.type_ == Type::InitSegment {
|
|
|
|
self.append_mvex()?;
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Appends a `MovieExtendsBox` (ISO/IEC 14496-12 section 8.8.1).
|
|
|
|
fn append_mvex(&mut self) -> Result<(), Error> {
|
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"mvex");
|
|
|
|
|
2020-08-07 18:30:22 -04:00
|
|
|
// Appends a `TrackExtendsBox`, `trex` (ISO/IEC 14496-12 section 8.8.3) for the video
|
|
|
|
// track.
|
2017-10-01 18:29:22 -04:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(&[
|
|
|
|
b't', b'r', b'e', b'x',
|
|
|
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
|
|
|
0x00, 0x00, 0x00, 0x01, // track_id
|
|
|
|
0x00, 0x00, 0x00, 0x01, // default_sample_description_index
|
|
|
|
0x00, 0x00, 0x00, 0x00, // default_sample_duration
|
|
|
|
0x00, 0x00, 0x00, 0x00, // default_sample_size
|
|
|
|
0x09, 0x21, 0x00, 0x00, // default_sample_flags (non sync):
|
|
|
|
// is_leading: not a leading sample
|
|
|
|
// sample_depends_on: does depend on others
|
|
|
|
// sample_is_depend_on: unknown
|
|
|
|
// sample_has_redundancy: no
|
|
|
|
// no padding
|
|
|
|
// sample_is_non_sync_sample: 1
|
|
|
|
// sample_degradation_priority: 0
|
|
|
|
]);
|
|
|
|
})?;
|
2017-02-21 22:37:36 -05:00
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2017-10-01 18:29:22 -04:00
|
|
|
/// Appends a `MovieFragmentBox` (ISO/IEC 14496-12 section 8.8.4).
|
|
|
|
fn append_moof(&mut self) -> Result<(), Error> {
|
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"moof");
|
|
|
|
|
|
|
|
// MovieFragmentHeaderBox (ISO/IEC 14496-12 section 8.8.5).
|
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"mfhd\x00\x00\x00\x00");
|
|
|
|
self.body.append_u32(1); // sequence_number
|
|
|
|
})?;
|
|
|
|
|
|
|
|
// TrackFragmentBox (ISO/IEC 14496-12 section 8.8.6).
|
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"traf");
|
|
|
|
|
2020-08-07 18:30:22 -04:00
|
|
|
// TrackFragmentHeaderBox, tfhd (ISO/IEC 14496-12 section 8.8.7).
|
2017-10-01 18:29:22 -04:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(&[
|
|
|
|
b't', b'f', b'h', b'd',
|
|
|
|
0x00, 0x02, 0x00, 0x00, // version + flags (default-base-is-moof)
|
|
|
|
0x00, 0x00, 0x00, 0x01, // track_id = 1
|
|
|
|
]);
|
|
|
|
})?;
|
|
|
|
self.append_truns()?;
|
|
|
|
|
|
|
|
// `TrackFragmentBaseMediaDecodeTimeBox` (ISO/IEC 14496-12 section 8.8.12).
|
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(&[
|
|
|
|
b't', b'f', b'd', b't',
|
|
|
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
|
|
|
0x00, 0x00, 0x00, 0x00, // TODO: baseMediaDecodeTime
|
|
|
|
]);
|
|
|
|
})?;
|
|
|
|
})?;
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
fn append_truns(&mut self) -> Result<(), Error> {
|
|
|
|
self.body.flush_buf()?;
|
|
|
|
for (i, s) in self.segments.iter().enumerate() {
|
|
|
|
self.body.append_slice(s.truns_len() as u64, SliceType::Truns, i)?;
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2019-01-07 03:59:32 -05:00
|
|
|
/// Appends a `MovieHeaderBox` version 1 (ISO/IEC 14496-12 section 8.2.2).
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_mvhd(&mut self, creation_ts: u32) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
2019-01-07 03:59:32 -05:00
|
|
|
self.body.buf.extend_from_slice(b"mvhd\x01\x00\x00\x00");
|
|
|
|
self.body.append_u64(creation_ts as u64);
|
|
|
|
self.body.append_u64(creation_ts as u64);
|
2016-11-25 17:34:00 -05:00
|
|
|
self.body.append_u32(TIME_UNITS_PER_SEC as u32);
|
2020-08-05 00:44:01 -04:00
|
|
|
let d = self.media_duration_90k;
|
2019-01-07 03:59:32 -05:00
|
|
|
self.body.append_u64(d);
|
2017-02-21 22:37:36 -05:00
|
|
|
self.body.append_static(StaticBytestring::MvhdJunk)?;
|
2016-12-02 23:40:55 -05:00
|
|
|
let next_track_id = if self.include_timestamp_subtitle_track { 3 } else { 2 };
|
|
|
|
self.body.append_u32(next_track_id);
|
2017-02-21 22:37:36 -05:00
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `TrackBox` (ISO/IEC 14496-12 section 8.3.1) suitable for video.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_video_trak(&mut self, creation_ts: u32) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"trak");
|
2017-02-21 22:37:36 -05:00
|
|
|
self.append_video_tkhd(creation_ts)?;
|
2016-11-25 17:34:00 -05:00
|
|
|
self.maybe_append_video_edts()?;
|
2017-02-21 22:37:36 -05:00
|
|
|
self.append_video_mdia(creation_ts)?;
|
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `TrackBox` (ISO/IEC 14496-12 section 8.3.1) suitable for subtitles.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_subtitle_trak(&mut self, creation_ts: u32) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"trak");
|
2017-02-21 22:37:36 -05:00
|
|
|
self.append_subtitle_tkhd(creation_ts)?;
|
|
|
|
self.append_subtitle_mdia(creation_ts)?;
|
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `TrackHeaderBox` (ISO/IEC 14496-12 section 8.3.2) suitable for video.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_video_tkhd(&mut self, creation_ts: u32) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
// flags 7: track_enabled | track_in_movie | track_in_preview
|
|
|
|
self.body.buf.extend_from_slice(b"tkhd\x00\x00\x00\x07");
|
|
|
|
self.body.append_u32(creation_ts);
|
|
|
|
self.body.append_u32(creation_ts);
|
|
|
|
self.body.append_u32(1); // track_id
|
|
|
|
self.body.append_u32(0); // reserved
|
2020-08-05 00:44:01 -04:00
|
|
|
self.body.append_u32(self.media_duration_90k as u32);
|
2017-02-21 22:37:36 -05:00
|
|
|
self.body.append_static(StaticBytestring::TkhdJunk)?;
|
2018-12-28 10:01:47 -05:00
|
|
|
|
|
|
|
let (width, height) = self.video_sample_entries.iter().fold(None, |m, e| {
|
|
|
|
match m {
|
|
|
|
None => Some((e.width, e.height)),
|
|
|
|
Some((w, h)) => Some((cmp::max(w, e.width), cmp::max(h, e.height))),
|
|
|
|
}
|
2018-12-28 18:30:33 -05:00
|
|
|
}).ok_or_else(|| format_err_t!(InvalidArgument, "no video_sample_entries"))?;
|
2016-11-25 17:34:00 -05:00
|
|
|
self.body.append_u32((width as u32) << 16);
|
|
|
|
self.body.append_u32((height as u32) << 16);
|
2017-02-21 22:37:36 -05:00
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `TrackHeaderBox` (ISO/IEC 14496-12 section 8.3.2) suitable for subtitles.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_subtitle_tkhd(&mut self, creation_ts: u32) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
// flags 7: track_enabled | track_in_movie | track_in_preview
|
2019-01-07 03:59:32 -05:00
|
|
|
self.body.buf.extend_from_slice(b"tkhd\x01\x00\x00\x07");
|
|
|
|
self.body.append_u64(creation_ts as u64);
|
|
|
|
self.body.append_u64(creation_ts as u64);
|
2016-11-25 17:34:00 -05:00
|
|
|
self.body.append_u32(2); // track_id
|
|
|
|
self.body.append_u32(0); // reserved
|
2020-08-05 00:44:01 -04:00
|
|
|
self.body.append_u64(self.media_duration_90k);
|
2017-02-21 22:37:36 -05:00
|
|
|
self.body.append_static(StaticBytestring::TkhdJunk)?;
|
2016-11-25 17:34:00 -05:00
|
|
|
self.body.append_u32(0); // width, unused.
|
|
|
|
self.body.append_u32(0); // height, unused.
|
2017-02-21 22:37:36 -05:00
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends an `EditBox` (ISO/IEC 14496-12 section 8.6.5) suitable for video, if necessary.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn maybe_append_video_edts(&mut self) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
#[derive(Debug, Default)]
|
|
|
|
struct Entry {
|
|
|
|
segment_duration: u64,
|
|
|
|
media_time: u64,
|
|
|
|
};
|
|
|
|
let mut flushed: Vec<Entry> = Vec::new();
|
|
|
|
let mut unflushed: Entry = Default::default();
|
|
|
|
let mut cur_media_time: u64 = 0;
|
|
|
|
for s in &self.segments {
|
|
|
|
// The actual range may start before the desired range because it can only start on a
|
|
|
|
// key frame. This relationship should hold true:
|
2017-10-17 09:14:47 -04:00
|
|
|
// actual start <= desired start <= desired end
|
2017-03-27 23:55:58 -04:00
|
|
|
let actual_start_90k = s.s.actual_start_90k();
|
2020-08-07 13:16:06 -04:00
|
|
|
let md = &s.rel_media_range_90k;
|
2020-08-05 00:44:01 -04:00
|
|
|
let skip = md.start - actual_start_90k;
|
|
|
|
let keep = md.end - md.start;
|
2017-10-17 09:14:47 -04:00
|
|
|
if skip < 0 || keep < 0 {
|
2018-12-28 18:30:33 -05:00
|
|
|
bail_t!(Internal, "skip={} keep={} on segment {:#?}", skip, keep, s);
|
2017-10-17 09:14:47 -04:00
|
|
|
}
|
2016-11-25 17:34:00 -05:00
|
|
|
cur_media_time += skip as u64;
|
|
|
|
if unflushed.segment_duration + unflushed.media_time == cur_media_time {
|
|
|
|
unflushed.segment_duration += keep as u64;
|
|
|
|
} else {
|
|
|
|
if unflushed.segment_duration > 0 {
|
|
|
|
flushed.push(unflushed);
|
|
|
|
}
|
2017-10-09 09:32:43 -04:00
|
|
|
unflushed = Entry {
|
2016-11-25 17:34:00 -05:00
|
|
|
segment_duration: keep as u64,
|
|
|
|
media_time: cur_media_time,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
cur_media_time += keep as u64;
|
|
|
|
}
|
|
|
|
|
|
|
|
if flushed.is_empty() && unflushed.media_time == 0 {
|
|
|
|
return Ok(()); // use implicit one-to-one mapping.
|
|
|
|
}
|
|
|
|
|
|
|
|
flushed.push(unflushed);
|
|
|
|
|
|
|
|
debug!("Using edit list: {:?}", flushed);
|
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"edts");
|
|
|
|
write_length!(self, {
|
|
|
|
// Use version 1 for 64-bit times.
|
|
|
|
self.body.buf.extend_from_slice(b"elst\x01\x00\x00\x00");
|
|
|
|
self.body.append_u32(flushed.len() as u32);
|
|
|
|
for e in &flushed {
|
|
|
|
self.body.append_u64(e.segment_duration);
|
|
|
|
self.body.append_u64(e.media_time);
|
|
|
|
|
2017-10-04 03:03:33 -04:00
|
|
|
// media_rate_integer + media_rate_fraction: fixed at 1.0
|
|
|
|
self.body.buf.extend_from_slice(b"\x00\x01\x00\x00");
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
2017-02-21 22:37:36 -05:00
|
|
|
})?;
|
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `MediaBox` (ISO/IEC 14496-12 section 8.4.1) suitable for video.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_video_mdia(&mut self, creation_ts: u32) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"mdia");
|
2017-02-21 22:37:36 -05:00
|
|
|
self.append_mdhd(creation_ts)?;
|
|
|
|
self.body.append_static(StaticBytestring::VideoHdlrBox)?;
|
|
|
|
self.append_video_minf()?;
|
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `MediaBox` (ISO/IEC 14496-12 section 8.4.1) suitable for subtitles.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_subtitle_mdia(&mut self, creation_ts: u32) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"mdia");
|
2017-02-21 22:37:36 -05:00
|
|
|
self.append_mdhd(creation_ts)?;
|
|
|
|
self.body.append_static(StaticBytestring::SubtitleHdlrBox)?;
|
|
|
|
self.append_subtitle_minf()?;
|
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `MediaHeaderBox` (ISO/IEC 14496-12 section 8.4.2.) suitable for either the video
|
|
|
|
/// or subtitle track.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_mdhd(&mut self, creation_ts: u32) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
2019-01-07 03:59:32 -05:00
|
|
|
self.body.buf.extend_from_slice(b"mdhd\x01\x00\x00\x00");
|
|
|
|
self.body.append_u64(creation_ts as u64);
|
|
|
|
self.body.append_u64(creation_ts as u64);
|
2016-11-25 17:34:00 -05:00
|
|
|
self.body.append_u32(TIME_UNITS_PER_SEC as u32);
|
2020-08-05 00:44:01 -04:00
|
|
|
self.body.append_u64(self.media_duration_90k);
|
2016-11-25 17:34:00 -05:00
|
|
|
self.body.append_u32(0x55c40000); // language=und + pre_defined
|
2017-02-21 22:37:36 -05:00
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `MediaInformationBox` (ISO/IEC 14496-12 section 8.4.4) suitable for video.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_video_minf(&mut self) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
2017-02-21 22:37:36 -05:00
|
|
|
self.body.append_static(StaticBytestring::VideoMinfJunk)?;
|
|
|
|
self.append_video_stbl()?;
|
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `MediaInformationBox` (ISO/IEC 14496-12 section 8.4.4) suitable for subtitles.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_subtitle_minf(&mut self) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
2017-02-21 22:37:36 -05:00
|
|
|
self.body.append_static(StaticBytestring::SubtitleMinfJunk)?;
|
|
|
|
self.append_subtitle_stbl()?;
|
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `SampleTableBox` (ISO/IEC 14496-12 section 8.5.1) suitable for video.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_video_stbl(&mut self) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"stbl");
|
2017-02-21 22:37:36 -05:00
|
|
|
self.append_video_stsd()?;
|
|
|
|
self.append_video_stts()?;
|
|
|
|
self.append_video_stsc()?;
|
|
|
|
self.append_video_stsz()?;
|
|
|
|
self.append_video_co64()?;
|
|
|
|
self.append_video_stss()?;
|
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `SampleTableBox` (ISO/IEC 14496-12 section 8.5.1) suitable for subtitles.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_subtitle_stbl(&mut self) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
2017-02-21 22:37:36 -05:00
|
|
|
self.body.append_static(StaticBytestring::SubtitleStblJunk)?;
|
|
|
|
self.append_subtitle_stts()?;
|
|
|
|
self.append_subtitle_stsc()?;
|
|
|
|
self.append_subtitle_stsz()?;
|
|
|
|
self.append_subtitle_co64()?;
|
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `SampleDescriptionBox` (ISO/IEC 14496-12 section 8.5.2) suitable for video.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_video_stsd(&mut self) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"stsd\x00\x00\x00\x00");
|
|
|
|
let n_entries = self.video_sample_entries.len() as u32;
|
|
|
|
self.body.append_u32(n_entries);
|
2017-02-21 22:37:36 -05:00
|
|
|
self.body.flush_buf()?;
|
2016-11-25 17:34:00 -05:00
|
|
|
for (i, e) in self.video_sample_entries.iter().enumerate() {
|
2017-02-21 22:37:36 -05:00
|
|
|
self.body.append_slice(e.data.len() as u64, SliceType::VideoSampleEntry, i)?;
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
2017-02-21 22:37:36 -05:00
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2020-08-05 00:44:01 -04:00
|
|
|
/// Appends an `stts` / `TimeToSampleBox` (ISO/IEC 14496-12 section 8.6.1) for video.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_video_stts(&mut self) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"stts\x00\x00\x00\x00");
|
|
|
|
let mut entry_count = 0;
|
|
|
|
for s in &self.segments {
|
|
|
|
entry_count += s.s.frames as u32;
|
|
|
|
}
|
|
|
|
self.body.append_u32(entry_count);
|
2017-10-01 18:29:22 -04:00
|
|
|
if !self.segments.is_empty() {
|
|
|
|
self.body.flush_buf()?;
|
|
|
|
for (i, s) in self.segments.iter().enumerate() {
|
|
|
|
self.body.append_slice(
|
|
|
|
2 * (mem::size_of::<u32>() as u64) * (s.s.frames as u64),
|
|
|
|
SliceType::Stts, i)?;
|
|
|
|
}
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
2017-02-21 22:37:36 -05:00
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2020-08-05 00:44:01 -04:00
|
|
|
/// Appends an `stts` / `TimeToSampleBox` (ISO/IEC 14496-12 section 8.6.1) for subtitles.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_subtitle_stts(&mut self) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"stts\x00\x00\x00\x00");
|
|
|
|
|
|
|
|
let entry_count_pos = self.body.buf.len();
|
|
|
|
self.body.append_u32(0); // placeholder for entry_count
|
|
|
|
|
|
|
|
let mut entry_count = 0;
|
|
|
|
for s in &self.segments {
|
2020-08-05 00:44:01 -04:00
|
|
|
// Note desired media range = actual media range for the subtitle track.
|
|
|
|
// We still need to consider media time vs wall time.
|
2020-08-07 13:16:06 -04:00
|
|
|
let mr = &s.rel_media_range_90k;
|
|
|
|
let start = s.recording_start + recording::Duration(i64::from(s.wall(mr.start)));
|
|
|
|
let end = s.recording_start + recording::Duration(i64::from(s.wall(mr.end)));
|
2016-11-25 17:34:00 -05:00
|
|
|
let start_next_sec = recording::Time(
|
|
|
|
start.0 + TIME_UNITS_PER_SEC - (start.0 % TIME_UNITS_PER_SEC));
|
2020-08-05 00:44:01 -04:00
|
|
|
|
2016-11-25 17:34:00 -05:00
|
|
|
if end <= start_next_sec {
|
2020-08-05 00:44:01 -04:00
|
|
|
// Segment doesn't last past the next second. Just write one entry.
|
2016-11-25 17:34:00 -05:00
|
|
|
entry_count += 1;
|
2020-08-05 00:44:01 -04:00
|
|
|
self.body.append_u32(1);
|
|
|
|
self.body.append_u32(u32::try_from(mr.end - mr.start).unwrap());
|
2016-11-25 17:34:00 -05:00
|
|
|
} else {
|
2020-08-05 00:44:01 -04:00
|
|
|
// The first subtitle lasts until the next second.
|
2020-11-28 00:56:15 -05:00
|
|
|
// media_off is relative to the start of the desired range.
|
|
|
|
let mut media_off =
|
2020-08-05 00:44:01 -04:00
|
|
|
s.media(i32::try_from((start_next_sec - start).0).unwrap());
|
2016-11-25 17:34:00 -05:00
|
|
|
entry_count += 1;
|
2020-08-05 00:44:01 -04:00
|
|
|
self.body.append_u32(1);
|
2020-11-28 00:56:15 -05:00
|
|
|
self.body.append_u32(u32::try_from(media_off).unwrap());
|
2020-08-05 00:44:01 -04:00
|
|
|
|
|
|
|
// Then there are zero or more "interior" subtitles, one second each. That's
|
2020-08-07 13:16:06 -04:00
|
|
|
// one second converted from wall to media duration. rescale rounds down,
|
2020-08-05 00:44:01 -04:00
|
|
|
// and these errors accumulate, so the final subtitle can be too early by as
|
|
|
|
// much as (MAX_RECORDING_WALL_DURATION/TIME_UNITS_PER_SEC) time units, or
|
|
|
|
// roughly 3 ms. We could avoid that by writing a separate entry for each
|
|
|
|
// second but it's not worth bloating the moov over 3 ms.
|
2016-11-25 17:34:00 -05:00
|
|
|
let end_prev_sec = recording::Time(end.0 - (end.0 % TIME_UNITS_PER_SEC));
|
|
|
|
if start_next_sec < end_prev_sec {
|
2020-08-05 00:44:01 -04:00
|
|
|
let onesec_media_dur =
|
|
|
|
s.media(i32::try_from(TIME_UNITS_PER_SEC).unwrap());
|
2016-11-25 17:34:00 -05:00
|
|
|
let interior = (end_prev_sec - start_next_sec).0 / TIME_UNITS_PER_SEC;
|
2020-08-05 00:44:01 -04:00
|
|
|
entry_count += 1;
|
2016-11-25 17:34:00 -05:00
|
|
|
self.body.append_u32(interior as u32); // count
|
2020-08-05 00:44:01 -04:00
|
|
|
self.body.append_u32(u32::try_from(onesec_media_dur).unwrap());
|
2020-11-28 00:56:15 -05:00
|
|
|
media_off += onesec_media_dur * i32::try_from(interior).unwrap();
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Then there's a final subtitle for the remaining fraction of a second.
|
|
|
|
entry_count += 1;
|
2020-08-05 00:44:01 -04:00
|
|
|
self.body.append_u32(1);
|
2020-11-28 00:56:15 -05:00
|
|
|
self.body.append_u32(u32::try_from(mr.end - mr.start - media_off).unwrap());
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
BigEndian::write_u32(&mut self.body.buf[entry_count_pos .. entry_count_pos + 4],
|
|
|
|
entry_count);
|
2017-02-21 22:37:36 -05:00
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `SampleToChunkBox` (ISO/IEC 14496-12 section 8.7.4) suitable for video.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_video_stsc(&mut self) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"stsc\x00\x00\x00\x00");
|
|
|
|
self.body.append_u32(self.segments.len() as u32);
|
|
|
|
for (i, s) in self.segments.iter().enumerate() {
|
|
|
|
self.body.append_u32((i + 1) as u32);
|
|
|
|
self.body.append_u32(s.s.frames as u32);
|
|
|
|
|
|
|
|
// Write sample_description_index.
|
|
|
|
let i = self.video_sample_entries.iter().position(
|
2017-02-28 00:14:06 -05:00
|
|
|
|e| e.id == s.s.video_sample_entry_id()).unwrap();
|
2016-11-25 17:34:00 -05:00
|
|
|
self.body.append_u32((i + 1) as u32);
|
|
|
|
}
|
2017-02-21 22:37:36 -05:00
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `SampleToChunkBox` (ISO/IEC 14496-12 section 8.7.4) suitable for subtitles.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_subtitle_stsc(&mut self) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(
|
|
|
|
b"stsc\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01");
|
2017-03-02 22:29:28 -05:00
|
|
|
self.body.append_u32(self.num_subtitle_samples as u32);
|
2016-11-25 17:34:00 -05:00
|
|
|
self.body.append_u32(1);
|
2017-02-21 22:37:36 -05:00
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `SampleSizeBox` (ISO/IEC 14496-12 section 8.7.3) suitable for video.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_video_stsz(&mut self) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"stsz\x00\x00\x00\x00\x00\x00\x00\x00");
|
|
|
|
let mut entry_count = 0;
|
|
|
|
for s in &self.segments {
|
|
|
|
entry_count += s.s.frames as u32;
|
|
|
|
}
|
|
|
|
self.body.append_u32(entry_count);
|
2017-10-01 18:29:22 -04:00
|
|
|
if !self.segments.is_empty() {
|
|
|
|
self.body.flush_buf()?;
|
|
|
|
for (i, s) in self.segments.iter().enumerate() {
|
|
|
|
self.body.append_slice(
|
|
|
|
(mem::size_of::<u32>()) as u64 * (s.s.frames as u64), SliceType::Stsz, i)?;
|
|
|
|
}
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
2017-02-21 22:37:36 -05:00
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `SampleSizeBox` (ISO/IEC 14496-12 section 8.7.3) suitable for subtitles.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_subtitle_stsz(&mut self) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"stsz\x00\x00\x00\x00");
|
|
|
|
self.body.append_u32((mem::size_of::<u16>() + SUBTITLE_LENGTH) as u32);
|
2017-03-02 22:29:28 -05:00
|
|
|
self.body.append_u32(self.num_subtitle_samples as u32);
|
2017-02-21 22:37:36 -05:00
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `ChunkLargeOffsetBox` (ISO/IEC 14496-12 section 8.7.5) suitable for video.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_video_co64(&mut self) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"co64\x00\x00\x00\x00");
|
|
|
|
self.body.append_u32(self.segments.len() as u32);
|
2017-10-01 18:29:22 -04:00
|
|
|
if !self.segments.is_empty() {
|
|
|
|
self.body.flush_buf()?;
|
|
|
|
self.body.append_slice(
|
|
|
|
(mem::size_of::<u64>()) as u64 * (self.segments.len() as u64),
|
|
|
|
SliceType::Co64, 0)?;
|
|
|
|
}
|
2017-02-21 22:37:36 -05:00
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `ChunkLargeOffsetBox` (ISO/IEC 14496-12 section 8.7.5) suitable for subtitles.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_subtitle_co64(&mut self) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
// Write a placeholder; the actual value will be filled in later.
|
|
|
|
self.body.buf.extend_from_slice(
|
|
|
|
b"co64\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00");
|
|
|
|
self.subtitle_co64_pos = Some(self.body.buf.len() - 8);
|
2017-02-21 22:37:36 -05:00
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a `SyncSampleBox` (ISO/IEC 14496-12 section 8.6.2) suitable for video.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_video_stss(&mut self) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
write_length!(self, {
|
|
|
|
self.body.buf.extend_from_slice(b"stss\x00\x00\x00\x00");
|
|
|
|
let mut entry_count = 0;
|
|
|
|
for s in &self.segments {
|
|
|
|
entry_count += s.s.key_frames as u32;
|
|
|
|
}
|
|
|
|
self.body.append_u32(entry_count);
|
2017-10-01 18:29:22 -04:00
|
|
|
if !self.segments.is_empty() {
|
|
|
|
self.body.flush_buf()?;
|
|
|
|
for (i, s) in self.segments.iter().enumerate() {
|
|
|
|
self.body.append_slice(
|
|
|
|
(mem::size_of::<u32>() as u64) * (s.s.key_frames as u64),
|
|
|
|
SliceType::Stss, i)?;
|
|
|
|
}
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
2017-02-21 22:37:36 -05:00
|
|
|
})
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl BodyState {
|
|
|
|
fn append_u32(&mut self, v: u32) {
|
|
|
|
self.buf.write_u32::<BigEndian>(v).expect("Vec write shouldn't fail");
|
|
|
|
}
|
|
|
|
|
|
|
|
fn append_u64(&mut self, v: u64) {
|
|
|
|
self.buf.write_u64::<BigEndian>(v).expect("Vec write shouldn't fail");
|
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Flushes the buffer: appends a slice for everything written into the buffer so far,
|
|
|
|
/// noting the position which has been flushed. Call this method prior to adding any non-buffer
|
|
|
|
/// slice.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn flush_buf(&mut self) -> Result<(), Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
let len = self.buf.len();
|
|
|
|
if self.unflushed_buf_pos < len {
|
2017-02-21 22:37:36 -05:00
|
|
|
let p = self.unflushed_buf_pos;
|
|
|
|
self.append_slice((len - p) as u64, SliceType::Buf, p)?;
|
2016-11-25 17:34:00 -05:00
|
|
|
self.unflushed_buf_pos = len;
|
|
|
|
}
|
2017-02-21 22:37:36 -05:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn append_slice(&mut self, len: u64, t: SliceType, p: usize) -> Result<(), Error> {
|
|
|
|
let l = self.slices.len();
|
2018-12-28 18:30:33 -05:00
|
|
|
self.slices.append(Slice::new(l + len, t, p)?).err_kind(ErrorKind::Internal)
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Appends a static bytestring, flushing the buffer if necessary.
|
2017-02-21 22:37:36 -05:00
|
|
|
fn append_static(&mut self, which: StaticBytestring) -> Result<(), Error> {
|
|
|
|
self.flush_buf()?;
|
2016-11-25 17:34:00 -05:00
|
|
|
let s = STATIC_BYTESTRINGS[which as usize];
|
2017-02-21 22:37:36 -05:00
|
|
|
self.append_slice(s.len() as u64, SliceType::Static, which as usize)
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-02 22:29:28 -05:00
|
|
|
struct FileInner {
|
2016-12-02 23:40:55 -05:00
|
|
|
db: Arc<db::Database>,
|
2018-02-12 01:45:51 -05:00
|
|
|
dirs_by_stream_id: Arc<::fnv::FnvHashMap<i32, Arc<dir::SampleFileDir>>>,
|
2017-02-25 00:33:26 -05:00
|
|
|
segments: Vec<Segment>,
|
2017-02-25 21:54:52 -05:00
|
|
|
slices: Slices<Slice>,
|
2016-11-25 17:34:00 -05:00
|
|
|
buf: Vec<u8>,
|
2016-12-02 23:40:55 -05:00
|
|
|
video_sample_entries: SmallVec<[Arc<db::VideoSampleEntry>; 1]>,
|
2016-11-25 17:34:00 -05:00
|
|
|
initial_sample_byte_pos: u64,
|
2018-08-30 01:26:19 -04:00
|
|
|
last_modified: SystemTime,
|
|
|
|
etag: HeaderValue,
|
2020-02-17 02:16:19 -05:00
|
|
|
content_disposition: Option<HeaderValue>,
|
2020-08-05 00:44:01 -04:00
|
|
|
prev_media_duration_and_cur_runs: Option<(recording::Duration, i32)>,
|
2020-06-10 01:06:03 -04:00
|
|
|
type_: Type,
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2017-03-02 22:29:28 -05:00
|
|
|
impl FileInner {
|
|
|
|
fn get_co64(&self, r: Range<u64>, l: u64) -> Result<Chunk, Error> {
|
|
|
|
let mut v = Vec::with_capacity(l as usize);
|
|
|
|
let mut pos = self.initial_sample_byte_pos;
|
|
|
|
for s in &self.segments {
|
2018-12-28 18:30:33 -05:00
|
|
|
v.write_u64::<BigEndian>(pos).err_kind(ErrorKind::Internal)?;
|
2017-03-02 22:29:28 -05:00
|
|
|
let r = s.s.sample_file_range();
|
|
|
|
pos += r.end - r.start;
|
|
|
|
}
|
2020-01-09 02:04:36 -05:00
|
|
|
Ok(ARefss::new(v).map(|v| &v[r.start as usize .. r.end as usize]).into())
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2017-03-02 22:29:28 -05:00
|
|
|
/// Gets a `Chunk` of video sample data from disk.
|
|
|
|
/// This works by `mmap()`ing in the data. There are a couple caveats:
|
|
|
|
///
|
|
|
|
/// * The thread which reads the resulting slice is likely to experience major page faults.
|
|
|
|
/// Eventually this will likely be rewritten to `mmap()` the memory in another thread, and
|
|
|
|
/// `mlock()` and send chunks of it to be read and `munlock()`ed to avoid this problem.
|
|
|
|
///
|
|
|
|
/// * If the backing file is truncated, the program will crash with `SIGBUS`. This shouldn't
|
|
|
|
/// happen because nothing should be touching Moonfire NVR's files but itself.
|
|
|
|
fn get_video_sample_data(&self, i: usize, r: Range<u64>) -> Result<Chunk, Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
let s = &self.segments[i];
|
2018-02-12 01:45:51 -05:00
|
|
|
let f = self.dirs_by_stream_id
|
2018-02-20 13:11:10 -05:00
|
|
|
.get(&s.s.id.stream())
|
2018-12-28 18:30:33 -05:00
|
|
|
.ok_or_else(|| format_err_t!(NotFound, "{}: stream not found", s.s.id))?
|
|
|
|
.open_file(s.s.id).err_kind(ErrorKind::Unknown)?;
|
2017-10-05 01:51:16 -04:00
|
|
|
let start = s.s.sample_file_range().start + r.start;
|
2017-11-17 02:01:09 -05:00
|
|
|
let mmap = Box::new(unsafe {
|
|
|
|
memmap::MmapOptions::new()
|
2018-11-20 12:32:55 -05:00
|
|
|
.offset(start)
|
2017-11-17 02:01:09 -05:00
|
|
|
.len((r.end - r.start) as usize)
|
2018-12-28 18:30:33 -05:00
|
|
|
.map(&f).err_kind(ErrorKind::Internal)?
|
2017-11-17 02:01:09 -05:00
|
|
|
});
|
|
|
|
use core::ops::Deref;
|
2020-01-09 02:04:36 -05:00
|
|
|
Ok(ARefss::new(mmap).map(|m| m.deref()).into())
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2017-03-02 22:29:28 -05:00
|
|
|
fn get_subtitle_sample_data(&self, i: usize, r: Range<u64>, l: u64) -> Result<Chunk, Error> {
|
2016-11-25 17:34:00 -05:00
|
|
|
let s = &self.segments[i];
|
2020-08-07 13:16:06 -04:00
|
|
|
let md = &s.rel_media_range_90k;
|
|
|
|
let wd = s.wall(md.start) .. s.wall(md.end);
|
2020-08-05 00:44:01 -04:00
|
|
|
let start_sec =
|
2020-08-07 13:16:06 -04:00
|
|
|
(s.recording_start + recording::Duration(i64::from(wd.start))).unix_seconds();
|
2020-08-05 00:44:01 -04:00
|
|
|
let end_sec =
|
2020-08-07 13:16:06 -04:00
|
|
|
(s.recording_start + recording::Duration(i64::from(wd.end) + TIME_UNITS_PER_SEC - 1))
|
2020-08-05 00:44:01 -04:00
|
|
|
.unix_seconds();
|
|
|
|
let l = usize::try_from(l).unwrap();
|
|
|
|
let mut v = Vec::with_capacity(l);
|
|
|
|
// TODO(slamb): is this right?!? might have an off-by-one here.
|
2017-03-02 22:29:28 -05:00
|
|
|
for ts in start_sec .. end_sec {
|
2018-12-28 18:30:33 -05:00
|
|
|
v.write_u16::<BigEndian>(SUBTITLE_LENGTH as u16).expect("Vec write shouldn't fail");
|
2017-03-02 22:29:28 -05:00
|
|
|
let tm = time::at(time::Timespec{sec: ts, nsec: 0});
|
|
|
|
use std::io::Write;
|
2018-12-28 18:30:33 -05:00
|
|
|
write!(v, "{}", tm.strftime(SUBTITLE_TEMPLATE).err_kind(ErrorKind::Internal)?)
|
|
|
|
.expect("Vec write shouldn't fail");
|
2017-03-02 22:29:28 -05:00
|
|
|
}
|
2020-08-05 00:44:01 -04:00
|
|
|
assert_eq!(l, v.len());
|
2020-01-09 02:04:36 -05:00
|
|
|
Ok(ARefss::new(v).map(|v| &v[r.start as usize .. r.end as usize]).into())
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-02 22:29:28 -05:00
|
|
|
#[derive(Clone)]
|
|
|
|
pub struct File(Arc<FileInner>);
|
|
|
|
|
2020-02-29 00:41:31 -05:00
|
|
|
impl File {
|
|
|
|
pub async fn append_into_vec(self, v: &mut Vec<u8>) -> Result<(), Error> {
|
|
|
|
use http_serve::Entity;
|
|
|
|
v.reserve(usize::try_from(self.len())
|
|
|
|
.map_err(|_| format_err_t!(InvalidArgument, "{}-byte mp4 is too big to send over WebSockets!",
|
|
|
|
self.len()))?);
|
|
|
|
let mut b = std::pin::Pin::from(self.get_range(0 .. self.len()));
|
|
|
|
loop {
|
|
|
|
use futures::stream::StreamExt;
|
|
|
|
match b.next().await {
|
|
|
|
Some(r) => {
|
2021-01-27 14:47:52 -05:00
|
|
|
let mut chunk = r
|
2020-02-29 00:41:31 -05:00
|
|
|
.map_err(failure::Error::from_boxed_compat)
|
|
|
|
.err_kind(ErrorKind::Unknown)?;
|
2021-01-27 14:47:52 -05:00
|
|
|
while chunk.has_remaining() {
|
|
|
|
let c = chunk.chunk();
|
|
|
|
v.extend_from_slice(c);
|
|
|
|
let len = c.len();
|
|
|
|
chunk.advance(len);
|
|
|
|
}
|
2020-02-29 00:41:31 -05:00
|
|
|
},
|
|
|
|
None => return Ok(()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-23 14:08:21 -05:00
|
|
|
impl http_serve::Entity for File {
|
2018-08-30 01:26:19 -04:00
|
|
|
type Data = Chunk;
|
|
|
|
type Error = BoxedError;
|
2017-09-22 00:51:58 -04:00
|
|
|
|
2018-04-06 18:54:52 -04:00
|
|
|
fn add_headers(&self, hdrs: &mut http::header::HeaderMap) {
|
|
|
|
let mut mime = BytesMut::with_capacity(64);
|
|
|
|
mime.extend_from_slice(b"video/mp4; codecs=\"");
|
2017-10-04 02:25:58 -04:00
|
|
|
let mut first = true;
|
|
|
|
for e in &self.0.video_sample_entries {
|
|
|
|
if first {
|
|
|
|
first = false
|
|
|
|
} else {
|
2018-04-06 18:54:52 -04:00
|
|
|
mime.extend_from_slice(b", ");
|
2017-10-04 02:25:58 -04:00
|
|
|
}
|
2018-04-06 18:54:52 -04:00
|
|
|
mime.extend_from_slice(e.rfc6381_codec.as_bytes());
|
2017-10-04 02:25:58 -04:00
|
|
|
}
|
2018-04-06 18:54:52 -04:00
|
|
|
mime.extend_from_slice(b"\"");
|
|
|
|
hdrs.insert(http::header::CONTENT_TYPE,
|
2020-01-09 02:04:36 -05:00
|
|
|
http::header::HeaderValue::from_maybe_shared(mime.freeze()).unwrap());
|
2020-02-17 02:16:19 -05:00
|
|
|
|
|
|
|
if let Some(cd) = self.0.content_disposition.as_ref() {
|
|
|
|
hdrs.insert(http::header::CONTENT_DISPOSITION, cd.clone());
|
|
|
|
}
|
2020-06-10 01:06:03 -04:00
|
|
|
if self.0.type_ == Type::MediaSegment {
|
2020-08-05 00:44:01 -04:00
|
|
|
if let Some((d, r)) = self.0.prev_media_duration_and_cur_runs {
|
2020-06-10 01:06:03 -04:00
|
|
|
hdrs.insert(
|
2020-08-05 00:44:01 -04:00
|
|
|
"X-Prev-Media-Duration",
|
2020-06-10 01:06:03 -04:00
|
|
|
HeaderValue::try_from(d.0.to_string()).expect("ints are valid headers"));
|
|
|
|
hdrs.insert(
|
|
|
|
"X-Runs",
|
|
|
|
HeaderValue::try_from(r.to_string()).expect("ints are valid headers"));
|
|
|
|
}
|
|
|
|
if let Some(s) = self.0.segments.first() {
|
2020-08-07 13:16:06 -04:00
|
|
|
let skip = s.rel_media_range_90k.start - s.s.actual_start_90k();
|
2020-06-10 01:06:03 -04:00
|
|
|
if skip > 0 {
|
|
|
|
hdrs.insert(
|
2020-08-05 00:44:01 -04:00
|
|
|
"X-Leading-Media-Duration",
|
2020-06-10 01:06:03 -04:00
|
|
|
HeaderValue::try_from(skip.to_string()).expect("ints are valid headers"));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-01-28 23:10:21 -05:00
|
|
|
}
|
2018-08-30 01:26:19 -04:00
|
|
|
fn last_modified(&self) -> Option<SystemTime> { Some(self.0.last_modified) }
|
|
|
|
fn etag(&self) -> Option<HeaderValue> { Some(self.0.etag.clone()) }
|
2017-03-02 22:29:28 -05:00
|
|
|
fn len(&self) -> u64 { self.0.slices.len() }
|
2018-08-30 01:26:19 -04:00
|
|
|
fn get_range(&self, range: Range<u64>)
|
2020-01-09 02:04:36 -05:00
|
|
|
-> Box<dyn Stream<Item = Result<Self::Data, Self::Error>> + Send + Sync> {
|
2018-08-30 01:26:19 -04:00
|
|
|
self.0.slices.get_range(self, range)
|
|
|
|
}
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2018-12-29 14:06:44 -05:00
|
|
|
impl fmt::Debug for File {
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
|
|
f.debug_struct("mp4::File")
|
|
|
|
.field("last_modified", &self.0.last_modified)
|
|
|
|
.field("etag", &self.0.etag)
|
|
|
|
.field("slices", &self.0.slices)
|
2018-12-29 14:15:01 -05:00
|
|
|
.field("segments", &self.0.segments)
|
2018-12-29 14:06:44 -05:00
|
|
|
.finish()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Tests. There are two general strategies used to validate the resulting files:
|
|
|
|
///
|
|
|
|
/// * basic tests that ffmpeg can read the generated mp4s. This ensures compatibility with
|
|
|
|
/// popular software, though it's hard to test specifics. ffmpeg provides an abstraction layer
|
|
|
|
/// over the encapsulation format, so mp4-specific details are hard to see. Also, ffmpeg might
|
|
|
|
/// paper over errors in the mp4 or have its own bugs.
|
|
|
|
///
|
|
|
|
/// * tests using the `BoxCursor` type to inspect the generated mp4s more closely. These don't
|
|
|
|
/// detect misunderstandings of the specification or incompatibilities, but they can be used
|
|
|
|
/// to verify the output is byte-for-byte as expected.
|
2016-11-25 17:34:00 -05:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
2020-03-20 23:52:30 -04:00
|
|
|
use base::clock::RealClocks;
|
2016-12-10 01:04:35 -05:00
|
|
|
use byteorder::{BigEndian, ByteOrder};
|
2021-01-27 14:47:52 -05:00
|
|
|
use hyper::body::Buf;
|
2018-12-28 22:53:29 -05:00
|
|
|
use crate::stream::{self, Opener, Stream};
|
|
|
|
use db::recording::{self, TIME_UNITS_PER_SEC};
|
|
|
|
use db::testutil::{self, TestDb, TEST_STREAM_ID};
|
|
|
|
use db::writer;
|
2020-01-09 02:04:36 -05:00
|
|
|
use futures::stream::TryStreamExt;
|
2018-12-28 22:53:29 -05:00
|
|
|
use log::info;
|
2018-01-23 14:08:21 -05:00
|
|
|
use http_serve::{self, Entity};
|
2016-11-25 17:34:00 -05:00
|
|
|
use std::fs;
|
2016-12-10 01:04:35 -05:00
|
|
|
use std::ops::Range;
|
2016-11-25 17:34:00 -05:00
|
|
|
use std::path::Path;
|
2020-01-09 02:04:36 -05:00
|
|
|
use std::pin::Pin;
|
2016-12-02 23:40:55 -05:00
|
|
|
use std::str;
|
2016-11-25 17:34:00 -05:00
|
|
|
use super::*;
|
|
|
|
|
2020-01-09 02:04:36 -05:00
|
|
|
async fn fill_slice<E: http_serve::Entity>(slice: &mut [u8], e: &E, start: u64)
|
2018-08-30 01:26:19 -04:00
|
|
|
where E::Error : ::std::fmt::Debug {
|
2017-03-02 22:29:28 -05:00
|
|
|
let mut p = 0;
|
2020-01-09 02:04:36 -05:00
|
|
|
Pin::from(e.get_range(start .. start + slice.len() as u64))
|
2021-01-27 14:47:52 -05:00
|
|
|
.try_for_each(|mut chunk| {
|
|
|
|
let len = chunk.remaining();
|
|
|
|
chunk.copy_to_slice(&mut slice[p .. p + len]);
|
|
|
|
p += len;
|
2020-01-09 02:04:36 -05:00
|
|
|
futures::future::ok::<_, E::Error>(())
|
2017-03-02 22:29:28 -05:00
|
|
|
})
|
2020-01-09 02:04:36 -05:00
|
|
|
.await
|
2017-03-02 22:29:28 -05:00
|
|
|
.unwrap();
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2020-03-20 23:52:30 -04:00
|
|
|
/// Returns the Blake3 digest of the given `Entity`.
|
|
|
|
async fn digest<E: http_serve::Entity>(e: &E) -> blake3::Hash
|
2018-08-30 01:26:19 -04:00
|
|
|
where E::Error : ::std::fmt::Debug {
|
2020-01-09 02:04:36 -05:00
|
|
|
Pin::from(e.get_range(0 .. e.len()))
|
2021-01-27 14:47:52 -05:00
|
|
|
.try_fold(blake3::Hasher::new(), |mut hasher, mut chunk| {
|
|
|
|
while chunk.has_remaining() {
|
|
|
|
let c = chunk.chunk();
|
|
|
|
hasher.update(c);
|
|
|
|
let len = c.len();
|
|
|
|
chunk.advance(len);
|
|
|
|
}
|
2020-03-20 23:52:30 -04:00
|
|
|
futures::future::ok::<_, E::Error>(hasher)
|
2017-03-02 22:29:28 -05:00
|
|
|
})
|
2020-01-09 02:04:36 -05:00
|
|
|
.await
|
2017-03-02 22:29:28 -05:00
|
|
|
.unwrap()
|
2020-03-20 23:52:30 -04:00
|
|
|
.finalize()
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Information used within `BoxCursor` to describe a box on the stack.
|
|
|
|
#[derive(Clone)]
|
|
|
|
struct Mp4Box {
|
|
|
|
interior: Range<u64>,
|
|
|
|
boxtype: [u8; 4],
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// A cursor over the boxes in a `.mp4` file. Supports moving forward and up/down the box
|
|
|
|
/// stack, not backward. Panics on error.
|
|
|
|
#[derive(Clone)]
|
2017-03-02 22:29:28 -05:00
|
|
|
struct BoxCursor {
|
|
|
|
mp4: File,
|
2016-12-02 23:40:55 -05:00
|
|
|
stack: Vec<Mp4Box>,
|
|
|
|
}
|
2016-11-25 17:34:00 -05:00
|
|
|
|
2017-03-02 22:29:28 -05:00
|
|
|
impl BoxCursor {
|
|
|
|
pub fn new(mp4: File) -> BoxCursor {
|
2016-12-02 23:40:55 -05:00
|
|
|
BoxCursor{
|
|
|
|
mp4: mp4,
|
|
|
|
stack: Vec::new(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Pushes the box at the given position onto the stack (returning true), or returns
|
|
|
|
/// false if pos == max.
|
2020-01-09 02:04:36 -05:00
|
|
|
async fn internal_push(&mut self, pos: u64, max: u64) -> bool {
|
2016-12-02 23:40:55 -05:00
|
|
|
if pos == max { return false; }
|
|
|
|
let mut hdr = [0u8; 16];
|
2020-01-09 02:04:36 -05:00
|
|
|
fill_slice(&mut hdr[..8], &self.mp4, pos).await;
|
2016-12-02 23:40:55 -05:00
|
|
|
let (len, hdr_len, boxtype_slice) = match BigEndian::read_u32(&hdr[..4]) {
|
|
|
|
0 => (self.mp4.len() - pos, 8, &hdr[4..8]),
|
|
|
|
1 => {
|
2020-01-09 02:04:36 -05:00
|
|
|
fill_slice(&mut hdr[8..], &self.mp4, pos + 8).await;
|
2017-10-01 18:29:22 -04:00
|
|
|
(BigEndian::read_u64(&hdr[8..16]), 16, &hdr[4..8])
|
2016-12-02 23:40:55 -05:00
|
|
|
},
|
|
|
|
l => (l as u64, 8, &hdr[4..8]),
|
|
|
|
};
|
|
|
|
let mut boxtype = [0u8; 4];
|
|
|
|
assert!(pos + (hdr_len as u64) <= max);
|
2017-10-01 18:29:22 -04:00
|
|
|
assert!(pos + len <= max, "path={} pos={} len={} max={}", self.path(), pos, len, max);
|
2016-12-02 23:40:55 -05:00
|
|
|
boxtype[..].copy_from_slice(boxtype_slice);
|
|
|
|
self.stack.push(Mp4Box{
|
|
|
|
interior: pos + hdr_len as u64 .. pos + len,
|
|
|
|
boxtype: boxtype,
|
|
|
|
});
|
|
|
|
trace!("positioned at {}", self.path());
|
|
|
|
true
|
|
|
|
}
|
|
|
|
|
2017-10-01 18:29:22 -04:00
|
|
|
fn interior(&self) -> Range<u64> {
|
|
|
|
self.stack.last().expect("at root").interior.clone()
|
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
fn path(&self) -> String {
|
|
|
|
let mut s = String::with_capacity(5 * self.stack.len());
|
|
|
|
for b in &self.stack {
|
|
|
|
s.push('/');
|
|
|
|
s.push_str(str::from_utf8(&b.boxtype[..]).unwrap());
|
|
|
|
}
|
|
|
|
s
|
|
|
|
}
|
|
|
|
|
2017-10-01 18:29:22 -04:00
|
|
|
fn name(&self) -> &str {
|
|
|
|
str::from_utf8(&self.stack.last().expect("at root").boxtype[..]).unwrap()
|
|
|
|
}
|
|
|
|
|
2020-08-07 18:30:22 -04:00
|
|
|
pub fn depth(&self) -> usize { self.stack.len() }
|
|
|
|
|
2017-10-01 18:29:22 -04:00
|
|
|
/// Gets the specified byte range within the current box (excluding length and type).
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Must not be at EOF.
|
2020-01-09 02:04:36 -05:00
|
|
|
pub async fn get(&self, start: u64, buf: &mut [u8]) {
|
2016-12-02 23:40:55 -05:00
|
|
|
let interior = &self.stack.last().expect("at root").interior;
|
2017-10-01 18:29:22 -04:00
|
|
|
assert!(start + (buf.len() as u64) <= interior.end - interior.start,
|
|
|
|
"path={} start={} buf.len={} interior={:?}",
|
|
|
|
self.path(), start, buf.len(), interior);
|
2020-01-09 02:04:36 -05:00
|
|
|
fill_slice(buf, &self.mp4, start+interior.start).await;
|
2016-12-02 23:40:55 -05:00
|
|
|
}
|
|
|
|
|
2020-01-09 02:04:36 -05:00
|
|
|
pub async fn get_all(&self) -> Vec<u8> {
|
2016-12-02 23:40:55 -05:00
|
|
|
let interior = self.stack.last().expect("at root").interior.clone();
|
|
|
|
let len = (interior.end - interior.start) as usize;
|
2017-03-02 22:29:28 -05:00
|
|
|
trace!("get_all: start={}, len={}", interior.start, len);
|
2016-12-02 23:40:55 -05:00
|
|
|
let mut out = Vec::with_capacity(len);
|
2017-03-02 22:29:28 -05:00
|
|
|
unsafe { out.set_len(len) };
|
2020-01-09 02:04:36 -05:00
|
|
|
fill_slice(&mut out[..], &self.mp4, interior.start).await;
|
2016-12-02 23:40:55 -05:00
|
|
|
out
|
|
|
|
}
|
|
|
|
|
2017-10-01 18:29:22 -04:00
|
|
|
/// Gets the specified u32 within the current box (excluding length and type).
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Must not be at EOF.
|
2020-01-09 02:04:36 -05:00
|
|
|
pub async fn get_u32(&self, p: u64) -> u32 {
|
2016-12-02 23:40:55 -05:00
|
|
|
let mut buf = [0u8; 4];
|
2020-01-09 02:04:36 -05:00
|
|
|
self.get(p, &mut buf).await;
|
2016-12-02 23:40:55 -05:00
|
|
|
BigEndian::read_u32(&buf[..])
|
|
|
|
}
|
|
|
|
|
2020-01-09 02:04:36 -05:00
|
|
|
pub async fn get_u64(&self, p: u64) -> u64 {
|
2017-10-17 09:14:47 -04:00
|
|
|
let mut buf = [0u8; 8];
|
2020-01-09 02:04:36 -05:00
|
|
|
self.get(p, &mut buf).await;
|
2017-10-17 09:14:47 -04:00
|
|
|
BigEndian::read_u64(&buf[..])
|
|
|
|
}
|
|
|
|
|
2016-12-02 23:40:55 -05:00
|
|
|
/// Navigates to the next box after the current one, or up if the current one is last.
|
2020-01-09 02:04:36 -05:00
|
|
|
pub async fn next(&mut self) -> bool {
|
2016-12-02 23:40:55 -05:00
|
|
|
let old = self.stack.pop().expect("positioned at root; there is no next");
|
|
|
|
let max = self.stack.last().map(|b| b.interior.end).unwrap_or_else(|| self.mp4.len());
|
2020-01-09 02:04:36 -05:00
|
|
|
self.internal_push(old.interior.end, max).await
|
2016-12-02 23:40:55 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Finds the next box of the given type after the current one, or navigates up if absent.
|
2020-01-09 02:04:36 -05:00
|
|
|
pub async fn find(&mut self, boxtype: &[u8]) -> bool {
|
2016-12-02 23:40:55 -05:00
|
|
|
trace!("looking for {}", str::from_utf8(boxtype).unwrap());
|
|
|
|
loop {
|
|
|
|
if &self.stack.last().unwrap().boxtype[..] == boxtype {
|
|
|
|
return true;
|
|
|
|
}
|
2020-01-09 02:04:36 -05:00
|
|
|
if !self.next().await {
|
2016-12-02 23:40:55 -05:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Moves up the stack. Must not be at root.
|
|
|
|
pub fn up(&mut self) { self.stack.pop(); }
|
|
|
|
|
|
|
|
/// Moves down the stack. Must be positioned on a box with children.
|
2020-01-09 02:04:36 -05:00
|
|
|
pub async fn down(&mut self) {
|
2016-12-02 23:40:55 -05:00
|
|
|
let range = self.stack.last().map(|b| b.interior.clone())
|
|
|
|
.unwrap_or_else(|| 0 .. self.mp4.len());
|
2020-01-09 02:04:36 -05:00
|
|
|
assert!(self.internal_push(range.start, range.end).await,
|
|
|
|
"no children in {}", self.path());
|
2016-12-02 23:40:55 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Information returned by `find_track`.
|
2017-03-02 22:29:28 -05:00
|
|
|
struct Track {
|
|
|
|
edts_cursor: Option<BoxCursor>,
|
|
|
|
stbl_cursor: BoxCursor,
|
2016-12-02 23:40:55 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Finds the `moov/trak` that has a `tkhd` associated with the given `track_id`, which must
|
|
|
|
/// exist.
|
2020-01-09 02:04:36 -05:00
|
|
|
async fn find_track(mp4: File, track_id: u32) -> Track {
|
2016-12-02 23:40:55 -05:00
|
|
|
let mut cursor = BoxCursor::new(mp4);
|
2020-01-09 02:04:36 -05:00
|
|
|
cursor.down().await;
|
|
|
|
assert!(cursor.find(b"moov").await);
|
|
|
|
cursor.down().await;
|
2016-12-02 23:40:55 -05:00
|
|
|
loop {
|
2020-01-09 02:04:36 -05:00
|
|
|
assert!(cursor.find(b"trak").await);
|
|
|
|
cursor.down().await;
|
|
|
|
assert!(cursor.find(b"tkhd").await);
|
2016-12-02 23:40:55 -05:00
|
|
|
let mut version = [0u8; 1];
|
2020-01-09 02:04:36 -05:00
|
|
|
cursor.get(0, &mut version).await;
|
2016-12-02 23:40:55 -05:00
|
|
|
|
|
|
|
// Let id_pos be the offset after the FullBox section of the track_id.
|
|
|
|
let id_pos = match version[0] {
|
|
|
|
0 => 8, // track_id follows 32-bit creation_time and modification_time
|
|
|
|
1 => 16, // ...64-bit times...
|
|
|
|
v => panic!("unexpected tkhd version {}", v),
|
|
|
|
};
|
2020-01-09 02:04:36 -05:00
|
|
|
let cur_track_id = cursor.get_u32(4 + id_pos).await;
|
2016-12-02 23:40:55 -05:00
|
|
|
trace!("found moov/trak/tkhd with id {}; want {}", cur_track_id, track_id);
|
|
|
|
if cur_track_id == track_id {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
cursor.up();
|
2020-01-09 02:04:36 -05:00
|
|
|
assert!(cursor.next().await);
|
2016-12-02 23:40:55 -05:00
|
|
|
}
|
|
|
|
let edts_cursor;
|
2020-01-09 02:04:36 -05:00
|
|
|
if cursor.find(b"edts").await {
|
2016-12-02 23:40:55 -05:00
|
|
|
edts_cursor = Some(cursor.clone());
|
|
|
|
cursor.up();
|
|
|
|
} else {
|
|
|
|
edts_cursor = None;
|
|
|
|
};
|
2020-01-09 02:04:36 -05:00
|
|
|
cursor.down().await;
|
|
|
|
assert!(cursor.find(b"mdia").await);
|
|
|
|
cursor.down().await;
|
|
|
|
assert!(cursor.find(b"minf").await);
|
|
|
|
cursor.down().await;
|
|
|
|
assert!(cursor.find(b"stbl").await);
|
2016-12-02 23:40:55 -05:00
|
|
|
Track{
|
|
|
|
edts_cursor: edts_cursor,
|
|
|
|
stbl_cursor: cursor,
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-07 18:30:22 -04:00
|
|
|
/// Traverses the box structure in `mp4` depth-first, validating the box positions.
|
|
|
|
async fn traverse(mp4: File) {
|
|
|
|
let mut cursor = BoxCursor::new(mp4);
|
|
|
|
cursor.down().await;
|
|
|
|
while cursor.depth() > 0 {
|
|
|
|
cursor.next().await;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-23 16:31:23 -04:00
|
|
|
fn copy_mp4_to_db(db: &TestDb<RealClocks>) {
|
2016-12-06 21:41:44 -05:00
|
|
|
let mut input =
|
|
|
|
stream::FFMPEG.open(stream::Source::File("src/testdata/clip.mp4")).unwrap();
|
2016-11-25 17:34:00 -05:00
|
|
|
|
|
|
|
// 2015-04-26 00:00:00 UTC.
|
|
|
|
const START_TIME: recording::Time = recording::Time(1430006400i64 * TIME_UNITS_PER_SEC);
|
|
|
|
let extra_data = input.get_extra_data().unwrap();
|
2020-03-20 00:35:42 -04:00
|
|
|
let video_sample_entry_id =
|
|
|
|
db.db.lock().insert_video_sample_entry(extra_data.entry).unwrap();
|
2018-02-12 01:45:51 -05:00
|
|
|
let dir = db.dirs_by_stream_id.get(&TEST_STREAM_ID).unwrap();
|
2018-03-23 16:31:23 -04:00
|
|
|
let mut output = writer::Writer::new(dir, &db.db, &db.syncer_channel, TEST_STREAM_ID,
|
2018-03-04 15:24:24 -05:00
|
|
|
video_sample_entry_id);
|
2016-12-06 21:41:44 -05:00
|
|
|
|
|
|
|
// end_pts is the pts of the end of the most recent frame (start + duration).
|
|
|
|
// It's needed because dir::Writer calculates a packet's duration from its pts and the
|
|
|
|
// next packet's pts. That's more accurate for RTSP than ffmpeg's estimate of duration.
|
|
|
|
// To write the final packet of this sample .mp4 with a full duration, we need to fake a
|
|
|
|
// next packet's pts from the ffmpeg-supplied duration.
|
|
|
|
let mut end_pts = None;
|
2016-12-28 23:56:08 -05:00
|
|
|
|
|
|
|
let mut frame_time = START_TIME;
|
|
|
|
|
2016-11-25 17:34:00 -05:00
|
|
|
loop {
|
|
|
|
let pkt = match input.get_next() {
|
|
|
|
Ok(p) => p,
|
2017-09-21 00:06:06 -04:00
|
|
|
Err(e) if e.is_eof() => { break; },
|
2016-11-25 17:34:00 -05:00
|
|
|
Err(e) => { panic!("unexpected input error: {}", e); },
|
|
|
|
};
|
2016-12-28 23:56:08 -05:00
|
|
|
let pts = pkt.pts().unwrap();
|
2017-09-21 00:06:06 -04:00
|
|
|
frame_time += recording::Duration(pkt.duration() as i64);
|
2016-12-28 23:56:08 -05:00
|
|
|
output.write(pkt.data().expect("packet without data"), frame_time, pts,
|
2016-11-25 17:34:00 -05:00
|
|
|
pkt.is_key()).unwrap();
|
2017-09-21 00:06:06 -04:00
|
|
|
end_pts = Some(pts + pkt.duration() as i64);
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
2019-02-14 01:34:19 -05:00
|
|
|
output.close(end_pts).unwrap();
|
2016-11-25 17:34:00 -05:00
|
|
|
db.syncer_channel.flush();
|
|
|
|
}
|
|
|
|
|
2020-08-07 18:30:22 -04:00
|
|
|
pub fn create_mp4_from_db(tdb: &TestDb<RealClocks>, skip_90k: i32, shorten_90k: i32,
|
|
|
|
include_subtitles: bool) -> File {
|
2017-10-01 18:29:22 -04:00
|
|
|
let mut builder = FileBuilder::new(Type::Normal);
|
2020-08-05 00:44:01 -04:00
|
|
|
builder.include_timestamp_subtitle_track(include_subtitles).unwrap();
|
2016-11-25 17:34:00 -05:00
|
|
|
let all_time = recording::Time(i64::min_value()) .. recording::Time(i64::max_value());
|
2016-12-02 23:40:55 -05:00
|
|
|
{
|
2018-02-12 01:45:51 -05:00
|
|
|
let db = tdb.db.lock();
|
2018-02-23 12:19:42 -05:00
|
|
|
db.list_recordings_by_time(TEST_STREAM_ID, all_time, &mut |r| {
|
2020-08-05 00:44:01 -04:00
|
|
|
let d = r.media_duration_90k;
|
track cumulative duration and runs
This is useful for a combo scrub bar-based UI (#32) + live view UI (#59)
in a non-obvious way. When constructing a HTML Media Source Extensions
API SourceBuffer, the caller can specify a "mode" of either "segments"
or "sequence":
In "sequence" mode, playback assumes segments are added sequentially.
This is good enough for a live view-only UI (#59) but not for a scrub
bar UI in which you may want to seek backward to a segment you've never
seen before. You will then need to insert a segment out-of-sequence.
Imagine what happens when the user goes forward again until the end of
the segment inserted immediately before it. The user should see the
chronologically next segment or a pause for loading if it's unavailable.
The best approximation of this is to track the mapping of timestamps to
segments and insert a VTTCue with an enter/exit handler that seeks to
the right position. But seeking isn't instantaneous; the user will
likely briefly see first the segment they seeked to before. That's
janky. Additionally, the "canplaythrough" event will behave strangely.
In "segments" mode, playback respects the timestamps we set:
* The obvious choice is to use wall clock timestamps. This is fine if
they're known to be fixed and correct. They're not. The
currently-recording segment may be "unanchored", meaning its start
timestamp is not yet fixed. Older timestamps may overlap if the system
clock was stepped between runs. The latter isn't /too/ bad from a user
perspective, though it's confusing as a developer. We probably will
only end up showing the more recent recording for a given
timestamp anyway. But the former is quite annoying. It means we have
to throw away part of the SourceBuffer that we may want to seek back
(causing UI pauses when that happens) or keep our own spare copy of it
(memory bloat). I'd like to avoid the whole mess.
* Another approach is to use timestamps that are guaranteed to be in
the correct order but that may have gaps. In particular, a timestamp
of (recording_id * max_recording_duration) + time_within_recording.
But again seeking isn't instantaneous. In my experiments, there's a
visible pause between segments that drives me nuts.
* Finally, the approach that led me to this schema change. Use
timestamps that place each segment after the one before, possibly with
an intentional gap between runs (to force a wait where we have an
actual gap). This should make the browser's natural playback behavior
work properly: it never goes to an incorrect place, and it only waits
when/if we want it to. We have to maintain a mapping between its
timestamps and segment ids but that's doable.
This commit is only the schema change; the new data aren't exposed in
the API yet, much less used by a UI.
Note that stream.next_recording_id became stream.cum_recordings. I made
a slight definition change in the process: recording ids for new streams
start at 0 rather than 1. Various tests changed accordingly.
The upgrade process makes a best effort to backfill these new fields,
but of course it doesn't know the total duration or number of runs of
previously deleted rows. That's good enough.
2020-06-09 19:17:32 -04:00
|
|
|
assert!(skip_90k + shorten_90k < d, "skip_90k={} shorten_90k={} r={:?}",
|
|
|
|
skip_90k, shorten_90k, r);
|
2020-08-07 18:30:22 -04:00
|
|
|
builder.append(&*db, r, skip_90k .. d - shorten_90k, true).unwrap();
|
2016-12-02 23:40:55 -05:00
|
|
|
Ok(())
|
|
|
|
}).unwrap();
|
|
|
|
}
|
2018-02-12 01:45:51 -05:00
|
|
|
builder.build(tdb.db.clone(), tdb.dirs_by_stream_id.clone()).unwrap()
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2020-01-09 02:04:36 -05:00
|
|
|
async fn write_mp4(mp4: &File, dir: &Path) -> String {
|
2016-11-25 17:34:00 -05:00
|
|
|
let mut filename = dir.to_path_buf();
|
|
|
|
filename.push("clip.new.mp4");
|
|
|
|
let mut out = fs::OpenOptions::new().write(true).create_new(true).open(&filename).unwrap();
|
2017-03-02 22:29:28 -05:00
|
|
|
use ::std::io::Write;
|
2020-01-09 02:04:36 -05:00
|
|
|
Pin::from(mp4.get_range(0 .. mp4.len()))
|
2021-01-27 14:47:52 -05:00
|
|
|
.try_for_each(|mut chunk| {
|
|
|
|
while chunk.has_remaining() {
|
|
|
|
let c = chunk.chunk();
|
|
|
|
let len = match out.write(c) {
|
|
|
|
Err(e) => return futures::future::err(BoxedError::from(e)),
|
|
|
|
Ok(l) => l,
|
|
|
|
};
|
|
|
|
chunk.advance(len);
|
|
|
|
}
|
|
|
|
futures::future::ok(())
|
2017-03-02 22:29:28 -05:00
|
|
|
})
|
2020-01-09 02:04:36 -05:00
|
|
|
.await
|
2017-03-02 22:29:28 -05:00
|
|
|
.unwrap();
|
2017-10-04 03:03:33 -04:00
|
|
|
info!("wrote {:?}", filename);
|
2016-11-25 17:34:00 -05:00
|
|
|
filename.to_str().unwrap().to_string()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn compare_mp4s(new_filename: &str, pts_offset: i64, shorten: i64) {
|
2016-12-06 21:41:44 -05:00
|
|
|
let mut orig = stream::FFMPEG.open(stream::Source::File("src/testdata/clip.mp4")).unwrap();
|
|
|
|
let mut new = stream::FFMPEG.open(stream::Source::File(new_filename)).unwrap();
|
2016-11-25 17:34:00 -05:00
|
|
|
assert_eq!(orig.get_extra_data().unwrap(), new.get_extra_data().unwrap());
|
|
|
|
let mut final_durations = None;
|
|
|
|
loop {
|
|
|
|
let orig_pkt = match orig.get_next() {
|
|
|
|
Ok(p) => Some(p),
|
2017-09-21 00:06:06 -04:00
|
|
|
Err(e) if e.is_eof() => None,
|
2016-11-25 17:34:00 -05:00
|
|
|
Err(e) => { panic!("unexpected input error: {}", e); },
|
|
|
|
};
|
|
|
|
let new_pkt = match new.get_next() {
|
|
|
|
Ok(p) => Some(p),
|
2017-09-21 00:06:06 -04:00
|
|
|
Err(e) if e.is_eof() => { break; },
|
2016-11-25 17:34:00 -05:00
|
|
|
Err(e) => { panic!("unexpected input error: {}", e); },
|
|
|
|
};
|
|
|
|
let (orig_pkt, new_pkt) = match (orig_pkt, new_pkt) {
|
|
|
|
(Some(o), Some(n)) => (o, n),
|
|
|
|
(None, None) => break,
|
|
|
|
(o, n) => panic!("orig: {} new: {}", o.is_some(), n.is_some()),
|
|
|
|
};
|
|
|
|
assert_eq!(orig_pkt.pts().unwrap(), new_pkt.pts().unwrap() + pts_offset);
|
|
|
|
assert_eq!(orig_pkt.dts(), new_pkt.dts() + pts_offset);
|
|
|
|
assert_eq!(orig_pkt.data(), new_pkt.data());
|
|
|
|
assert_eq!(orig_pkt.is_key(), new_pkt.is_key());
|
2017-09-21 00:06:06 -04:00
|
|
|
final_durations = Some((orig_pkt.duration() as i64, new_pkt.duration() as i64));
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if let Some((orig_dur, new_dur)) = final_durations {
|
|
|
|
// One would normally expect the duration to be exactly the same, but when using an
|
2019-07-10 20:02:45 -04:00
|
|
|
// edit list, ffmpeg 3.x appears to extend the last packet's duration by the amount
|
|
|
|
// skipped at the beginning. ffmpeg 4.x behaves properly. Allow either behavior.
|
|
|
|
// See <https://github.com/scottlamb/moonfire-nvr/issues/10>.
|
|
|
|
assert!(orig_dur - shorten + pts_offset == new_dur ||
|
|
|
|
orig_dur - shorten == new_dur,
|
2016-11-25 17:34:00 -05:00
|
|
|
"orig_dur={} new_dur={} shorten={} pts_offset={}",
|
|
|
|
orig_dur, new_dur, shorten, pts_offset);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-02-21 22:37:36 -05:00
|
|
|
/// Makes a `.mp4` file which is only good for exercising the `Slice` logic for producing
|
|
|
|
/// sample tables that match the supplied encoder.
|
2018-03-23 16:31:23 -04:00
|
|
|
fn make_mp4_from_encoders(type_: Type, db: &TestDb<RealClocks>,
|
2018-03-02 18:40:32 -05:00
|
|
|
mut recordings: Vec<db::RecordingToInsert>,
|
2020-08-07 18:30:22 -04:00
|
|
|
desired_range_90k: Range<i32>,
|
|
|
|
start_at_key: bool) -> Result<File, Error> {
|
2017-10-01 18:29:22 -04:00
|
|
|
let mut builder = FileBuilder::new(type_);
|
2017-10-09 09:32:43 -04:00
|
|
|
let mut duration_so_far = 0;
|
2018-03-02 18:40:32 -05:00
|
|
|
for r in recordings.drain(..) {
|
|
|
|
let row = db.insert_recording_from_encoder(r);
|
2017-10-09 09:32:43 -04:00
|
|
|
let d_start = if desired_range_90k.start < duration_so_far { 0 }
|
|
|
|
else { desired_range_90k.start - duration_so_far };
|
2020-08-05 00:44:01 -04:00
|
|
|
let d_end = if desired_range_90k.end > duration_so_far + row.media_duration_90k {
|
|
|
|
row.media_duration_90k
|
|
|
|
} else {
|
|
|
|
desired_range_90k.end - duration_so_far
|
|
|
|
};
|
|
|
|
duration_so_far += row.media_duration_90k;
|
2020-08-07 18:30:22 -04:00
|
|
|
builder.append(&db.db.lock(), row, d_start .. d_end, start_at_key).unwrap();
|
2017-10-09 09:32:43 -04:00
|
|
|
}
|
2018-12-28 10:01:47 -05:00
|
|
|
builder.build(db.db.clone(), db.dirs_by_stream_id.clone())
|
2016-12-02 23:40:55 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Tests sample table for a simple video index of all sync frames.
|
2020-01-09 02:04:36 -05:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_all_sync_frames() {
|
2016-12-02 23:40:55 -05:00
|
|
|
testutil::init();
|
2018-03-23 16:31:23 -04:00
|
|
|
let db = TestDb::new(RealClocks {});
|
2018-03-02 18:40:32 -05:00
|
|
|
let mut r = db::RecordingToInsert::default();
|
2016-12-02 23:40:55 -05:00
|
|
|
let mut encoder = recording::SampleIndexEncoder::new();
|
|
|
|
for i in 1..6 {
|
|
|
|
let duration_90k = 2 * i;
|
|
|
|
let bytes = 3 * i;
|
2020-08-06 08:16:38 -04:00
|
|
|
encoder.add_sample(duration_90k, bytes, true, &mut r);
|
2016-12-02 23:40:55 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Time range [2, 2+4+6+8) means the 2nd, 3rd, and 4th samples should be included.
|
2020-08-07 18:30:22 -04:00
|
|
|
let mp4 = make_mp4_from_encoders(Type::Normal, &db, vec![r], 2 .. 2+4+6+8, true).unwrap();
|
|
|
|
traverse(mp4.clone()).await;
|
2020-01-09 02:04:36 -05:00
|
|
|
let track = find_track(mp4, 1).await;
|
2016-12-02 23:40:55 -05:00
|
|
|
assert!(track.edts_cursor.is_none());
|
|
|
|
let mut cursor = track.stbl_cursor;
|
2020-01-09 02:04:36 -05:00
|
|
|
cursor.down().await;
|
|
|
|
cursor.find(b"stts").await;
|
|
|
|
assert_eq!(cursor.get_all().await, &[
|
2016-12-02 23:40:55 -05:00
|
|
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
|
|
|
0x00, 0x00, 0x00, 0x03, // entry_count
|
|
|
|
|
|
|
|
// entries
|
|
|
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x04, // run length / timestamps.
|
|
|
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06,
|
|
|
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08,
|
|
|
|
]);
|
|
|
|
|
2020-01-09 02:04:36 -05:00
|
|
|
cursor.find(b"stsz").await;
|
|
|
|
assert_eq!(cursor.get_all().await, &[
|
2016-12-02 23:40:55 -05:00
|
|
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
|
|
|
0x00, 0x00, 0x00, 0x00, // sample_size
|
|
|
|
0x00, 0x00, 0x00, 0x03, // sample_count
|
|
|
|
|
|
|
|
// entries
|
|
|
|
0x00, 0x00, 0x00, 0x06, // size
|
|
|
|
0x00, 0x00, 0x00, 0x09,
|
|
|
|
0x00, 0x00, 0x00, 0x0c,
|
|
|
|
]);
|
|
|
|
|
2020-01-09 02:04:36 -05:00
|
|
|
cursor.find(b"stss").await;
|
|
|
|
assert_eq!(cursor.get_all().await, &[
|
2016-12-02 23:40:55 -05:00
|
|
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
|
|
|
0x00, 0x00, 0x00, 0x03, // entry_count
|
|
|
|
|
|
|
|
// entries
|
|
|
|
0x00, 0x00, 0x00, 0x01, // sample_number
|
|
|
|
0x00, 0x00, 0x00, 0x02,
|
|
|
|
0x00, 0x00, 0x00, 0x03,
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Tests sample table and edit list for a video index with half sync frames.
|
2020-01-09 02:04:36 -05:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_half_sync_frames() {
|
2016-12-02 23:40:55 -05:00
|
|
|
testutil::init();
|
2018-03-23 16:31:23 -04:00
|
|
|
let db = TestDb::new(RealClocks {});
|
2018-03-02 18:40:32 -05:00
|
|
|
let mut r = db::RecordingToInsert::default();
|
2016-12-02 23:40:55 -05:00
|
|
|
let mut encoder = recording::SampleIndexEncoder::new();
|
|
|
|
for i in 1..6 {
|
|
|
|
let duration_90k = 2 * i;
|
|
|
|
let bytes = 3 * i;
|
2020-08-06 08:16:38 -04:00
|
|
|
encoder.add_sample(duration_90k, bytes, (i % 2) == 1, &mut r);
|
2016-12-02 23:40:55 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Time range [2+4+6, 2+4+6+8) means the 4th sample should be included.
|
|
|
|
// The 3rd gets pulled in also because it's a sync frame and the 4th isn't.
|
2020-08-07 18:30:22 -04:00
|
|
|
let mp4 = make_mp4_from_encoders(Type::Normal, &db, vec![r], 2+4+6 .. 2+4+6+8, true)
|
|
|
|
.unwrap();
|
|
|
|
traverse(mp4.clone()).await;
|
2020-01-09 02:04:36 -05:00
|
|
|
let track = find_track(mp4, 1).await;
|
2016-12-02 23:40:55 -05:00
|
|
|
|
|
|
|
// Examine edts. It should skip the 3rd frame.
|
|
|
|
let mut cursor = track.edts_cursor.unwrap();
|
2020-01-09 02:04:36 -05:00
|
|
|
cursor.down().await;
|
|
|
|
cursor.find(b"elst").await;
|
|
|
|
assert_eq!(cursor.get_all().await, &[
|
2016-12-02 23:40:55 -05:00
|
|
|
0x01, 0x00, 0x00, 0x00, // version + flags
|
|
|
|
0x00, 0x00, 0x00, 0x01, // length
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, // segment_duration
|
|
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, // media_time
|
2017-10-04 03:03:33 -04:00
|
|
|
0x00, 0x01, 0x00, 0x00, // media_rate_{integer,fraction}
|
2016-12-02 23:40:55 -05:00
|
|
|
]);
|
|
|
|
|
|
|
|
// Examine stbl.
|
|
|
|
let mut cursor = track.stbl_cursor;
|
2020-01-09 02:04:36 -05:00
|
|
|
cursor.down().await;
|
|
|
|
cursor.find(b"stts").await;
|
|
|
|
assert_eq!(cursor.get_all().await, &[
|
2016-12-02 23:40:55 -05:00
|
|
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
|
|
|
0x00, 0x00, 0x00, 0x02, // entry_count
|
|
|
|
|
|
|
|
// entries
|
|
|
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x06, // run length / timestamps.
|
|
|
|
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x08,
|
|
|
|
]);
|
|
|
|
|
2020-01-09 02:04:36 -05:00
|
|
|
cursor.find(b"stsz").await;
|
|
|
|
assert_eq!(cursor.get_all().await, &[
|
2016-12-02 23:40:55 -05:00
|
|
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
|
|
|
0x00, 0x00, 0x00, 0x00, // sample_size
|
|
|
|
0x00, 0x00, 0x00, 0x02, // sample_count
|
|
|
|
|
|
|
|
// entries
|
|
|
|
0x00, 0x00, 0x00, 0x09, // size
|
|
|
|
0x00, 0x00, 0x00, 0x0c,
|
|
|
|
]);
|
|
|
|
|
2020-01-09 02:04:36 -05:00
|
|
|
cursor.find(b"stss").await;
|
|
|
|
assert_eq!(cursor.get_all().await, &[
|
2016-12-02 23:40:55 -05:00
|
|
|
0x00, 0x00, 0x00, 0x00, // version + flags
|
|
|
|
0x00, 0x00, 0x00, 0x01, // entry_count
|
|
|
|
|
|
|
|
// entries
|
|
|
|
0x00, 0x00, 0x00, 0x01, // sample_number
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2020-01-09 02:04:36 -05:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_no_segments() {
|
2018-12-28 10:01:47 -05:00
|
|
|
testutil::init();
|
|
|
|
let db = TestDb::new(RealClocks {});
|
2020-08-07 18:30:22 -04:00
|
|
|
let e = make_mp4_from_encoders(Type::Normal, &db, vec![], 0 .. 0, true).err().unwrap();
|
2018-12-28 18:30:33 -05:00
|
|
|
assert_eq!(e.to_string(), "Invalid argument: no video_sample_entries");
|
2018-12-28 10:01:47 -05:00
|
|
|
}
|
|
|
|
|
2020-01-09 02:04:36 -05:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_multi_segment() {
|
2017-10-09 09:32:43 -04:00
|
|
|
testutil::init();
|
2018-03-23 16:31:23 -04:00
|
|
|
let db = TestDb::new(RealClocks {});
|
2017-10-09 09:32:43 -04:00
|
|
|
let mut encoders = Vec::new();
|
2018-03-02 18:40:32 -05:00
|
|
|
let mut r = db::RecordingToInsert::default();
|
2017-10-09 09:32:43 -04:00
|
|
|
let mut encoder = recording::SampleIndexEncoder::new();
|
2020-08-06 08:16:38 -04:00
|
|
|
encoder.add_sample(1, 1, true, &mut r);
|
|
|
|
encoder.add_sample(2, 2, false, &mut r);
|
|
|
|
encoder.add_sample(3, 3, true, &mut r);
|
2018-03-02 18:40:32 -05:00
|
|
|
encoders.push(r);
|
|
|
|
let mut r = db::RecordingToInsert::default();
|
2017-10-09 09:32:43 -04:00
|
|
|
let mut encoder = recording::SampleIndexEncoder::new();
|
2020-08-06 08:16:38 -04:00
|
|
|
encoder.add_sample(4, 4, true, &mut r);
|
|
|
|
encoder.add_sample(5, 5, false, &mut r);
|
2018-03-02 18:40:32 -05:00
|
|
|
encoders.push(r);
|
2017-10-09 09:32:43 -04:00
|
|
|
|
|
|
|
// This should include samples 3 and 4 only, both sync frames.
|
2020-08-07 18:30:22 -04:00
|
|
|
let mp4 = make_mp4_from_encoders(Type::Normal, &db, encoders, 1+2 .. 1+2+3+4, true)
|
|
|
|
.unwrap();
|
|
|
|
traverse(mp4.clone()).await;
|
2017-10-09 09:32:43 -04:00
|
|
|
let mut cursor = BoxCursor::new(mp4);
|
2020-01-09 02:04:36 -05:00
|
|
|
cursor.down().await;
|
|
|
|
assert!(cursor.find(b"moov").await);
|
|
|
|
cursor.down().await;
|
|
|
|
assert!(cursor.find(b"trak").await);
|
|
|
|
cursor.down().await;
|
|
|
|
assert!(cursor.find(b"mdia").await);
|
|
|
|
cursor.down().await;
|
|
|
|
assert!(cursor.find(b"minf").await);
|
|
|
|
cursor.down().await;
|
|
|
|
assert!(cursor.find(b"stbl").await);
|
|
|
|
cursor.down().await;
|
|
|
|
assert!(cursor.find(b"stss").await);
|
|
|
|
assert_eq!(cursor.get_u32(4).await, 2); // entry_count
|
|
|
|
assert_eq!(cursor.get_u32(8).await, 1);
|
|
|
|
assert_eq!(cursor.get_u32(12).await, 2);
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn test_zero_duration_recording() {
|
2017-10-17 09:14:47 -04:00
|
|
|
testutil::init();
|
2018-03-23 16:31:23 -04:00
|
|
|
let db = TestDb::new(RealClocks {});
|
2017-10-17 09:14:47 -04:00
|
|
|
let mut encoders = Vec::new();
|
2018-03-02 18:40:32 -05:00
|
|
|
let mut r = db::RecordingToInsert::default();
|
2017-10-17 09:14:47 -04:00
|
|
|
let mut encoder = recording::SampleIndexEncoder::new();
|
2020-08-06 08:16:38 -04:00
|
|
|
encoder.add_sample(2, 1, true, &mut r);
|
|
|
|
encoder.add_sample(3, 2, false, &mut r);
|
2018-03-02 18:40:32 -05:00
|
|
|
encoders.push(r);
|
|
|
|
let mut r = db::RecordingToInsert::default();
|
2017-10-17 09:14:47 -04:00
|
|
|
let mut encoder = recording::SampleIndexEncoder::new();
|
2020-08-06 08:16:38 -04:00
|
|
|
encoder.add_sample(0, 3, true, &mut r);
|
2018-03-02 18:40:32 -05:00
|
|
|
encoders.push(r);
|
2017-10-17 09:14:47 -04:00
|
|
|
|
|
|
|
// Multi-segment recording with an edit list, encoding with a zero-duration recording.
|
2020-08-07 18:30:22 -04:00
|
|
|
let mp4 = make_mp4_from_encoders(Type::Normal, &db, encoders, 1 .. 2+3, true).unwrap();
|
|
|
|
traverse(mp4.clone()).await;
|
2020-01-09 02:04:36 -05:00
|
|
|
let track = find_track(mp4, 1).await;
|
2017-10-17 09:14:47 -04:00
|
|
|
let mut cursor = track.edts_cursor.unwrap();
|
2020-01-09 02:04:36 -05:00
|
|
|
cursor.down().await;
|
|
|
|
cursor.find(b"elst").await;
|
|
|
|
assert_eq!(cursor.get_u32(4).await, 1); // entry_count
|
|
|
|
assert_eq!(cursor.get_u64(8).await, 4); // segment_duration
|
|
|
|
assert_eq!(cursor.get_u64(16).await, 1); // media_time
|
2017-10-17 09:14:47 -04:00
|
|
|
}
|
|
|
|
|
2020-01-09 02:04:36 -05:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_media_segment() {
|
2017-10-01 18:29:22 -04:00
|
|
|
testutil::init();
|
2018-03-23 16:31:23 -04:00
|
|
|
let db = TestDb::new(RealClocks {});
|
2018-03-02 18:40:32 -05:00
|
|
|
let mut r = db::RecordingToInsert::default();
|
2017-10-01 18:29:22 -04:00
|
|
|
let mut encoder = recording::SampleIndexEncoder::new();
|
|
|
|
for i in 1..6 {
|
|
|
|
let duration_90k = 2 * i;
|
|
|
|
let bytes = 3 * i;
|
2020-08-06 08:16:38 -04:00
|
|
|
encoder.add_sample(duration_90k, bytes, (i % 2) == 1, &mut r);
|
2017-10-01 18:29:22 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
// Time range [2+4+6, 2+4+6+8+1) means the 4th sample and part of the 5th are included.
|
|
|
|
// The 3rd gets pulled in also because it's a sync frame and the 4th isn't.
|
2018-03-02 18:40:32 -05:00
|
|
|
let mp4 = make_mp4_from_encoders(Type::MediaSegment, &db, vec![r],
|
2020-08-07 18:30:22 -04:00
|
|
|
2+4+6 .. 2+4+6+8+1, true).unwrap();
|
|
|
|
traverse(mp4.clone()).await;
|
2017-10-01 18:29:22 -04:00
|
|
|
let mut cursor = BoxCursor::new(mp4);
|
2020-01-09 02:04:36 -05:00
|
|
|
cursor.down().await;
|
2017-10-01 18:29:22 -04:00
|
|
|
|
|
|
|
let mut mdat = cursor.clone();
|
2020-01-09 02:04:36 -05:00
|
|
|
assert!(mdat.find(b"mdat").await);
|
|
|
|
|
|
|
|
assert!(cursor.find(b"moof").await);
|
|
|
|
cursor.down().await;
|
|
|
|
assert!(cursor.find(b"traf").await);
|
|
|
|
cursor.down().await;
|
|
|
|
assert!(cursor.find(b"trun").await);
|
|
|
|
assert_eq!(cursor.get_u32(4).await, 2);
|
|
|
|
assert_eq!(cursor.get_u32(8).await as u64, mdat.interior().start);
|
|
|
|
assert_eq!(cursor.get_u32(12).await, 174063616); // first_sample_flags
|
|
|
|
assert_eq!(cursor.get_u32(16).await, 6); // sample duration
|
|
|
|
assert_eq!(cursor.get_u32(20).await, 9); // sample size
|
|
|
|
assert_eq!(cursor.get_u32(24).await, 8); // sample duration
|
|
|
|
assert_eq!(cursor.get_u32(28).await, 12); // sample size
|
|
|
|
assert!(cursor.next().await);
|
2017-10-01 18:29:22 -04:00
|
|
|
assert_eq!(cursor.name(), "trun");
|
2020-01-09 02:04:36 -05:00
|
|
|
assert_eq!(cursor.get_u32(4).await, 1);
|
|
|
|
assert_eq!(cursor.get_u32(8).await as u64, mdat.interior().start + 9 + 12);
|
|
|
|
assert_eq!(cursor.get_u32(12).await, 174063616); // first_sample_flags
|
|
|
|
assert_eq!(cursor.get_u32(16).await, 1); // sample duration
|
|
|
|
assert_eq!(cursor.get_u32(20).await, 15); // sample size
|
2017-10-01 18:29:22 -04:00
|
|
|
}
|
|
|
|
|
2020-08-07 18:30:22 -04:00
|
|
|
/// Tests `.mp4` files which represent a single frame, as in the live view WebSocket stream.
|
|
|
|
#[tokio::test]
|
|
|
|
async fn test_single_frame_media_segment() {
|
|
|
|
testutil::init();
|
|
|
|
let db = TestDb::new(RealClocks {});
|
|
|
|
let mut r = db::RecordingToInsert::default();
|
|
|
|
let mut encoder = recording::SampleIndexEncoder::new();
|
|
|
|
for i in 1..6 {
|
|
|
|
let duration_90k = 2 * i;
|
|
|
|
let bytes = 3 * i;
|
|
|
|
encoder.add_sample(duration_90k, bytes, (i % 2) == 1, &mut r);
|
|
|
|
}
|
|
|
|
|
|
|
|
let mut pos = 0;
|
|
|
|
for i in 1..6 {
|
|
|
|
let duration_90k = 2 * i;
|
|
|
|
let mp4 = make_mp4_from_encoders(Type::MediaSegment, &db, vec![r.clone()],
|
|
|
|
pos .. pos+duration_90k, false).unwrap();
|
|
|
|
traverse(mp4.clone()).await;
|
|
|
|
let mut cursor = BoxCursor::new(mp4);
|
|
|
|
cursor.down().await;
|
|
|
|
assert!(cursor.find(b"moof").await);
|
|
|
|
cursor.down().await;
|
|
|
|
assert!(cursor.find(b"traf").await);
|
|
|
|
cursor.down().await;
|
|
|
|
assert!(cursor.find(b"trun").await);
|
|
|
|
pos += duration_90k;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-09 02:04:36 -05:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_round_trip() {
|
2016-11-30 14:17:46 -05:00
|
|
|
testutil::init();
|
2018-03-23 16:31:23 -04:00
|
|
|
let db = TestDb::new(RealClocks {});
|
2016-11-25 17:34:00 -05:00
|
|
|
copy_mp4_to_db(&db);
|
2018-02-12 01:45:51 -05:00
|
|
|
let mp4 = create_mp4_from_db(&db, 0, 0, false);
|
2020-08-07 18:30:22 -04:00
|
|
|
traverse(mp4.clone()).await;
|
2020-01-09 02:04:36 -05:00
|
|
|
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
2016-11-25 17:34:00 -05:00
|
|
|
compare_mp4s(&new_filename, 0, 0);
|
|
|
|
|
|
|
|
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
|
|
|
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
|
|
|
// combine ranges from the new format with ranges from the old format.
|
2020-03-20 23:52:30 -04:00
|
|
|
let hash = digest(&mp4).await;
|
|
|
|
assert_eq!("e95f2d261cdebac5b9983abeea59e8eb053dc4efac866722544c665d9de7c49d",
|
|
|
|
hash.to_hex().as_str());
|
|
|
|
const EXPECTED_ETAG: &'static str =
|
track cumulative duration and runs
This is useful for a combo scrub bar-based UI (#32) + live view UI (#59)
in a non-obvious way. When constructing a HTML Media Source Extensions
API SourceBuffer, the caller can specify a "mode" of either "segments"
or "sequence":
In "sequence" mode, playback assumes segments are added sequentially.
This is good enough for a live view-only UI (#59) but not for a scrub
bar UI in which you may want to seek backward to a segment you've never
seen before. You will then need to insert a segment out-of-sequence.
Imagine what happens when the user goes forward again until the end of
the segment inserted immediately before it. The user should see the
chronologically next segment or a pause for loading if it's unavailable.
The best approximation of this is to track the mapping of timestamps to
segments and insert a VTTCue with an enter/exit handler that seeks to
the right position. But seeking isn't instantaneous; the user will
likely briefly see first the segment they seeked to before. That's
janky. Additionally, the "canplaythrough" event will behave strangely.
In "segments" mode, playback respects the timestamps we set:
* The obvious choice is to use wall clock timestamps. This is fine if
they're known to be fixed and correct. They're not. The
currently-recording segment may be "unanchored", meaning its start
timestamp is not yet fixed. Older timestamps may overlap if the system
clock was stepped between runs. The latter isn't /too/ bad from a user
perspective, though it's confusing as a developer. We probably will
only end up showing the more recent recording for a given
timestamp anyway. But the former is quite annoying. It means we have
to throw away part of the SourceBuffer that we may want to seek back
(causing UI pauses when that happens) or keep our own spare copy of it
(memory bloat). I'd like to avoid the whole mess.
* Another approach is to use timestamps that are guaranteed to be in
the correct order but that may have gaps. In particular, a timestamp
of (recording_id * max_recording_duration) + time_within_recording.
But again seeking isn't instantaneous. In my experiments, there's a
visible pause between segments that drives me nuts.
* Finally, the approach that led me to this schema change. Use
timestamps that place each segment after the one before, possibly with
an intentional gap between runs (to force a wait where we have an
actual gap). This should make the browser's natural playback behavior
work properly: it never goes to an incorrect place, and it only waits
when/if we want it to. We have to maintain a mapping between its
timestamps and segment ids but that's doable.
This commit is only the schema change; the new data aren't exposed in
the API yet, much less used by a UI.
Note that stream.next_recording_id became stream.cum_recordings. I made
a slight definition change in the process: recording ids for new streams
start at 0 rather than 1. Various tests changed accordingly.
The upgrade process makes a best effort to backfill these new fields,
but of course it doesn't know the total duration or number of runs of
previously deleted rows. That's good enough.
2020-06-09 19:17:32 -04:00
|
|
|
"\"61031ab36449b4d1186e9513b5e40df84e78bfb2807c0035b360437bb905cdd5\"";
|
2018-08-30 01:26:19 -04:00
|
|
|
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
2016-11-25 17:34:00 -05:00
|
|
|
drop(db.syncer_channel);
|
2018-02-22 19:35:34 -05:00
|
|
|
db.db.lock().clear_on_flush();
|
2016-11-25 17:34:00 -05:00
|
|
|
db.syncer_join.join().unwrap();
|
|
|
|
}
|
|
|
|
|
2020-01-09 02:04:36 -05:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_round_trip_with_subtitles() {
|
2016-11-30 14:17:46 -05:00
|
|
|
testutil::init();
|
2018-03-23 16:31:23 -04:00
|
|
|
let db = TestDb::new(RealClocks {});
|
2016-11-25 17:34:00 -05:00
|
|
|
copy_mp4_to_db(&db);
|
2018-02-12 01:45:51 -05:00
|
|
|
let mp4 = create_mp4_from_db(&db, 0, 0, true);
|
2020-08-07 18:30:22 -04:00
|
|
|
traverse(mp4.clone()).await;
|
2020-01-09 02:04:36 -05:00
|
|
|
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
2016-11-25 17:34:00 -05:00
|
|
|
compare_mp4s(&new_filename, 0, 0);
|
|
|
|
|
|
|
|
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
|
|
|
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
|
|
|
// combine ranges from the new format with ranges from the old format.
|
2020-03-20 23:52:30 -04:00
|
|
|
let hash = digest(&mp4).await;
|
|
|
|
assert_eq!("77e09be8ee5ca353ca56f9a80bb7420680713c80a0831d236fac45a96aa3b3d4",
|
|
|
|
hash.to_hex().as_str());
|
|
|
|
const EXPECTED_ETAG: &'static str =
|
track cumulative duration and runs
This is useful for a combo scrub bar-based UI (#32) + live view UI (#59)
in a non-obvious way. When constructing a HTML Media Source Extensions
API SourceBuffer, the caller can specify a "mode" of either "segments"
or "sequence":
In "sequence" mode, playback assumes segments are added sequentially.
This is good enough for a live view-only UI (#59) but not for a scrub
bar UI in which you may want to seek backward to a segment you've never
seen before. You will then need to insert a segment out-of-sequence.
Imagine what happens when the user goes forward again until the end of
the segment inserted immediately before it. The user should see the
chronologically next segment or a pause for loading if it's unavailable.
The best approximation of this is to track the mapping of timestamps to
segments and insert a VTTCue with an enter/exit handler that seeks to
the right position. But seeking isn't instantaneous; the user will
likely briefly see first the segment they seeked to before. That's
janky. Additionally, the "canplaythrough" event will behave strangely.
In "segments" mode, playback respects the timestamps we set:
* The obvious choice is to use wall clock timestamps. This is fine if
they're known to be fixed and correct. They're not. The
currently-recording segment may be "unanchored", meaning its start
timestamp is not yet fixed. Older timestamps may overlap if the system
clock was stepped between runs. The latter isn't /too/ bad from a user
perspective, though it's confusing as a developer. We probably will
only end up showing the more recent recording for a given
timestamp anyway. But the former is quite annoying. It means we have
to throw away part of the SourceBuffer that we may want to seek back
(causing UI pauses when that happens) or keep our own spare copy of it
(memory bloat). I'd like to avoid the whole mess.
* Another approach is to use timestamps that are guaranteed to be in
the correct order but that may have gaps. In particular, a timestamp
of (recording_id * max_recording_duration) + time_within_recording.
But again seeking isn't instantaneous. In my experiments, there's a
visible pause between segments that drives me nuts.
* Finally, the approach that led me to this schema change. Use
timestamps that place each segment after the one before, possibly with
an intentional gap between runs (to force a wait where we have an
actual gap). This should make the browser's natural playback behavior
work properly: it never goes to an incorrect place, and it only waits
when/if we want it to. We have to maintain a mapping between its
timestamps and segment ids but that's doable.
This commit is only the schema change; the new data aren't exposed in
the API yet, much less used by a UI.
Note that stream.next_recording_id became stream.cum_recordings. I made
a slight definition change in the process: recording ids for new streams
start at 0 rather than 1. Various tests changed accordingly.
The upgrade process makes a best effort to backfill these new fields,
but of course it doesn't know the total duration or number of runs of
previously deleted rows. That's good enough.
2020-06-09 19:17:32 -04:00
|
|
|
"\"8e048b22b21c9b93d889e8dfbeeb56fa1b17dc0956190f5c3acc84f6f674089f\"";
|
2018-08-30 01:26:19 -04:00
|
|
|
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
2016-11-25 17:34:00 -05:00
|
|
|
drop(db.syncer_channel);
|
2018-02-22 19:35:34 -05:00
|
|
|
db.db.lock().clear_on_flush();
|
2016-11-25 17:34:00 -05:00
|
|
|
db.syncer_join.join().unwrap();
|
|
|
|
}
|
|
|
|
|
2020-01-09 02:04:36 -05:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_round_trip_with_edit_list() {
|
2016-11-30 14:17:46 -05:00
|
|
|
testutil::init();
|
2018-03-23 16:31:23 -04:00
|
|
|
let db = TestDb::new(RealClocks {});
|
2016-11-25 17:34:00 -05:00
|
|
|
copy_mp4_to_db(&db);
|
2018-02-12 01:45:51 -05:00
|
|
|
let mp4 = create_mp4_from_db(&db, 1, 0, false);
|
2020-08-07 18:30:22 -04:00
|
|
|
traverse(mp4.clone()).await;
|
2020-01-09 02:04:36 -05:00
|
|
|
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
2016-11-25 17:34:00 -05:00
|
|
|
compare_mp4s(&new_filename, 1, 0);
|
|
|
|
|
|
|
|
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
|
|
|
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
|
|
|
// combine ranges from the new format with ranges from the old format.
|
2020-03-20 23:52:30 -04:00
|
|
|
let hash = digest(&mp4).await;
|
|
|
|
assert_eq!("f9807cfc6b96a399f3a5ad62d090f55a18543a9eeb1f48d59f86564ffd9b1e84",
|
|
|
|
hash.to_hex().as_str());
|
|
|
|
const EXPECTED_ETAG: &'static str =
|
track cumulative duration and runs
This is useful for a combo scrub bar-based UI (#32) + live view UI (#59)
in a non-obvious way. When constructing a HTML Media Source Extensions
API SourceBuffer, the caller can specify a "mode" of either "segments"
or "sequence":
In "sequence" mode, playback assumes segments are added sequentially.
This is good enough for a live view-only UI (#59) but not for a scrub
bar UI in which you may want to seek backward to a segment you've never
seen before. You will then need to insert a segment out-of-sequence.
Imagine what happens when the user goes forward again until the end of
the segment inserted immediately before it. The user should see the
chronologically next segment or a pause for loading if it's unavailable.
The best approximation of this is to track the mapping of timestamps to
segments and insert a VTTCue with an enter/exit handler that seeks to
the right position. But seeking isn't instantaneous; the user will
likely briefly see first the segment they seeked to before. That's
janky. Additionally, the "canplaythrough" event will behave strangely.
In "segments" mode, playback respects the timestamps we set:
* The obvious choice is to use wall clock timestamps. This is fine if
they're known to be fixed and correct. They're not. The
currently-recording segment may be "unanchored", meaning its start
timestamp is not yet fixed. Older timestamps may overlap if the system
clock was stepped between runs. The latter isn't /too/ bad from a user
perspective, though it's confusing as a developer. We probably will
only end up showing the more recent recording for a given
timestamp anyway. But the former is quite annoying. It means we have
to throw away part of the SourceBuffer that we may want to seek back
(causing UI pauses when that happens) or keep our own spare copy of it
(memory bloat). I'd like to avoid the whole mess.
* Another approach is to use timestamps that are guaranteed to be in
the correct order but that may have gaps. In particular, a timestamp
of (recording_id * max_recording_duration) + time_within_recording.
But again seeking isn't instantaneous. In my experiments, there's a
visible pause between segments that drives me nuts.
* Finally, the approach that led me to this schema change. Use
timestamps that place each segment after the one before, possibly with
an intentional gap between runs (to force a wait where we have an
actual gap). This should make the browser's natural playback behavior
work properly: it never goes to an incorrect place, and it only waits
when/if we want it to. We have to maintain a mapping between its
timestamps and segment ids but that's doable.
This commit is only the schema change; the new data aren't exposed in
the API yet, much less used by a UI.
Note that stream.next_recording_id became stream.cum_recordings. I made
a slight definition change in the process: recording ids for new streams
start at 0 rather than 1. Various tests changed accordingly.
The upgrade process makes a best effort to backfill these new fields,
but of course it doesn't know the total duration or number of runs of
previously deleted rows. That's good enough.
2020-06-09 19:17:32 -04:00
|
|
|
"\"196192eccd8be2c840dfc4073355efe5c917999641e3d0a2b87e0d2eab40267f\"";
|
2018-08-30 01:26:19 -04:00
|
|
|
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
2016-11-25 17:34:00 -05:00
|
|
|
drop(db.syncer_channel);
|
2018-02-22 19:35:34 -05:00
|
|
|
db.db.lock().clear_on_flush();
|
2016-11-25 17:34:00 -05:00
|
|
|
db.syncer_join.join().unwrap();
|
|
|
|
}
|
|
|
|
|
2020-11-28 00:56:15 -05:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_round_trip_with_edit_list_and_subtitles() {
|
|
|
|
testutil::init();
|
|
|
|
let db = TestDb::new(RealClocks {});
|
|
|
|
copy_mp4_to_db(&db);
|
|
|
|
let off = 2*TIME_UNITS_PER_SEC;
|
|
|
|
let mp4 = create_mp4_from_db(&db, i32::try_from(off).unwrap(), 0, true);
|
|
|
|
traverse(mp4.clone()).await;
|
|
|
|
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
|
|
|
compare_mp4s(&new_filename, off, 0);
|
|
|
|
|
|
|
|
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
|
|
|
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
|
|
|
// combine ranges from the new format with ranges from the old format.
|
|
|
|
let hash = digest(&mp4).await;
|
|
|
|
assert_eq!("7aef371e9ab5e871893fd9b1963cb71c1c9b093b5d4ff36cb1340b65155a8aa2",
|
|
|
|
hash.to_hex().as_str());
|
|
|
|
const EXPECTED_ETAG: &'static str =
|
|
|
|
"\"84cafd9db7a5c0c32961d9848582d2dca436a58d25cbedfb02d7450bc6ce1229\"";
|
|
|
|
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
|
|
|
drop(db.syncer_channel);
|
|
|
|
db.db.lock().clear_on_flush();
|
|
|
|
db.syncer_join.join().unwrap();
|
|
|
|
}
|
|
|
|
|
2020-01-09 02:04:36 -05:00
|
|
|
#[tokio::test]
|
|
|
|
async fn test_round_trip_with_shorten() {
|
2016-11-30 14:17:46 -05:00
|
|
|
testutil::init();
|
2018-03-23 16:31:23 -04:00
|
|
|
let db = TestDb::new(RealClocks {});
|
2016-11-25 17:34:00 -05:00
|
|
|
copy_mp4_to_db(&db);
|
2018-02-12 01:45:51 -05:00
|
|
|
let mp4 = create_mp4_from_db(&db, 0, 1, false);
|
2020-08-07 18:30:22 -04:00
|
|
|
traverse(mp4.clone()).await;
|
2020-01-09 02:04:36 -05:00
|
|
|
let new_filename = write_mp4(&mp4, db.tmpdir.path()).await;
|
2016-11-25 17:34:00 -05:00
|
|
|
compare_mp4s(&new_filename, 0, 1);
|
|
|
|
|
|
|
|
// Test the metadata. This is brittle, which is the point. Any time the digest comparison
|
|
|
|
// here fails, it can be updated, but the etag must change as well! Otherwise clients may
|
|
|
|
// combine ranges from the new format with ranges from the old format.
|
2020-03-20 23:52:30 -04:00
|
|
|
let hash = digest(&mp4).await;
|
|
|
|
assert_eq!("5211104e1fdfe3bbc0d7d7d479933940305ff7f23201e97308db23a022ee6339",
|
|
|
|
hash.to_hex().as_str());
|
|
|
|
const EXPECTED_ETAG: &'static str =
|
track cumulative duration and runs
This is useful for a combo scrub bar-based UI (#32) + live view UI (#59)
in a non-obvious way. When constructing a HTML Media Source Extensions
API SourceBuffer, the caller can specify a "mode" of either "segments"
or "sequence":
In "sequence" mode, playback assumes segments are added sequentially.
This is good enough for a live view-only UI (#59) but not for a scrub
bar UI in which you may want to seek backward to a segment you've never
seen before. You will then need to insert a segment out-of-sequence.
Imagine what happens when the user goes forward again until the end of
the segment inserted immediately before it. The user should see the
chronologically next segment or a pause for loading if it's unavailable.
The best approximation of this is to track the mapping of timestamps to
segments and insert a VTTCue with an enter/exit handler that seeks to
the right position. But seeking isn't instantaneous; the user will
likely briefly see first the segment they seeked to before. That's
janky. Additionally, the "canplaythrough" event will behave strangely.
In "segments" mode, playback respects the timestamps we set:
* The obvious choice is to use wall clock timestamps. This is fine if
they're known to be fixed and correct. They're not. The
currently-recording segment may be "unanchored", meaning its start
timestamp is not yet fixed. Older timestamps may overlap if the system
clock was stepped between runs. The latter isn't /too/ bad from a user
perspective, though it's confusing as a developer. We probably will
only end up showing the more recent recording for a given
timestamp anyway. But the former is quite annoying. It means we have
to throw away part of the SourceBuffer that we may want to seek back
(causing UI pauses when that happens) or keep our own spare copy of it
(memory bloat). I'd like to avoid the whole mess.
* Another approach is to use timestamps that are guaranteed to be in
the correct order but that may have gaps. In particular, a timestamp
of (recording_id * max_recording_duration) + time_within_recording.
But again seeking isn't instantaneous. In my experiments, there's a
visible pause between segments that drives me nuts.
* Finally, the approach that led me to this schema change. Use
timestamps that place each segment after the one before, possibly with
an intentional gap between runs (to force a wait where we have an
actual gap). This should make the browser's natural playback behavior
work properly: it never goes to an incorrect place, and it only waits
when/if we want it to. We have to maintain a mapping between its
timestamps and segment ids but that's doable.
This commit is only the schema change; the new data aren't exposed in
the API yet, much less used by a UI.
Note that stream.next_recording_id became stream.cum_recordings. I made
a slight definition change in the process: recording ids for new streams
start at 0 rather than 1. Various tests changed accordingly.
The upgrade process makes a best effort to backfill these new fields,
but of course it doesn't know the total duration or number of runs of
previously deleted rows. That's good enough.
2020-06-09 19:17:32 -04:00
|
|
|
"\"9e50099d86ae1c742e65f7a4151c4427f42051a87158405a35b4e5550fd05c30\"";
|
2018-08-30 01:26:19 -04:00
|
|
|
assert_eq!(Some(HeaderValue::from_str(EXPECTED_ETAG).unwrap()), mp4.etag());
|
2016-11-25 17:34:00 -05:00
|
|
|
drop(db.syncer_channel);
|
2018-02-22 19:35:34 -05:00
|
|
|
db.db.lock().clear_on_flush();
|
2016-11-25 17:34:00 -05:00
|
|
|
db.syncer_join.join().unwrap();
|
|
|
|
}
|
2017-01-08 17:22:35 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(all(test, feature="nightly"))]
|
|
|
|
mod bench {
|
|
|
|
extern crate test;
|
|
|
|
|
2018-03-23 18:16:43 -04:00
|
|
|
use base::clock::RealClocks;
|
2018-02-21 02:15:39 -05:00
|
|
|
use db::recording;
|
|
|
|
use db::testutil::{self, TestDb};
|
2020-01-09 02:04:36 -05:00
|
|
|
use futures::future;
|
2017-01-08 17:22:35 -05:00
|
|
|
use hyper;
|
2018-01-23 14:08:21 -05:00
|
|
|
use http_serve;
|
2018-12-28 22:53:29 -05:00
|
|
|
use lazy_static::lazy_static;
|
2017-01-08 17:22:35 -05:00
|
|
|
use super::tests::create_mp4_from_db;
|
2017-06-11 22:40:36 -04:00
|
|
|
use url::Url;
|
2016-11-25 17:34:00 -05:00
|
|
|
|
|
|
|
/// An HTTP server for benchmarking.
|
2017-03-02 22:29:28 -05:00
|
|
|
/// It's used as a singleton via `lazy_static!` so that when getting a CPU profile of the
|
|
|
|
/// benchmark, more of the profile focuses on the HTTP serving rather than the setup.
|
2016-11-25 17:34:00 -05:00
|
|
|
///
|
|
|
|
/// Currently this only serves a single `.mp4` file but we could set up variations to benchmark
|
|
|
|
/// different scenarios: with/without subtitles and edit lists, different lengths, serving
|
|
|
|
/// different fractions of the file, etc.
|
|
|
|
struct BenchServer {
|
2017-06-11 22:40:36 -04:00
|
|
|
url: Url,
|
2016-11-25 17:34:00 -05:00
|
|
|
generated_len: u64,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl BenchServer {
|
|
|
|
fn new() -> BenchServer {
|
2018-03-23 16:31:23 -04:00
|
|
|
let db = TestDb::new(RealClocks {});
|
2017-02-12 23:37:03 -05:00
|
|
|
testutil::add_dummy_recordings_to_db(&db.db, 60);
|
2018-02-12 01:45:51 -05:00
|
|
|
let mp4 = create_mp4_from_db(&db, 0, 0, false);
|
2017-03-02 22:29:28 -05:00
|
|
|
let p = mp4.0.initial_sample_byte_pos;
|
2020-01-09 02:04:36 -05:00
|
|
|
let make_svc = hyper::service::make_service_fn(move |_conn| {
|
|
|
|
future::ok::<_, std::convert::Infallible>(hyper::service::service_fn({
|
|
|
|
let mp4 = mp4.clone();
|
|
|
|
move |req| future::ok::<hyper::Response<crate::body::Body>, hyper::Error>(
|
|
|
|
http_serve::serve(mp4.clone(), &req))
|
|
|
|
}))
|
|
|
|
});
|
2021-01-27 14:47:52 -05:00
|
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
|
|
let srv = {
|
|
|
|
let _guard = rt.enter();
|
2020-01-09 02:04:36 -05:00
|
|
|
let addr = ([127, 0, 0, 1], 0).into();
|
|
|
|
hyper::server::Server::bind(&addr)
|
2018-08-30 01:26:19 -04:00
|
|
|
.tcp_nodelay(true)
|
2020-01-09 02:04:36 -05:00
|
|
|
.serve(make_svc)
|
2021-01-27 14:47:52 -05:00
|
|
|
};
|
2020-01-09 02:04:36 -05:00
|
|
|
let addr = srv.local_addr(); // resolve port 0 to a real ephemeral port number.
|
|
|
|
::std::thread::spawn(move || {
|
|
|
|
rt.block_on(srv).unwrap();
|
2016-11-25 17:34:00 -05:00
|
|
|
});
|
2018-08-30 01:26:19 -04:00
|
|
|
BenchServer {
|
2017-06-11 22:40:36 -04:00
|
|
|
url: Url::parse(&format!("http://{}:{}/", addr.ip(), addr.port())).unwrap(),
|
2016-11-25 17:34:00 -05:00
|
|
|
generated_len: p,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
lazy_static! {
|
2020-08-07 18:30:22 -04:00
|
|
|
static ref SERVER: BenchServer = BenchServer::new();
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
2017-02-26 23:10:02 -05:00
|
|
|
#[bench]
|
2018-12-28 22:53:29 -05:00
|
|
|
fn build_index(b: &mut test::Bencher) {
|
2017-02-26 23:10:02 -05:00
|
|
|
testutil::init();
|
2018-03-23 16:31:23 -04:00
|
|
|
let db = TestDb::new(RealClocks {});
|
2017-02-26 23:10:02 -05:00
|
|
|
testutil::add_dummy_recordings_to_db(&db.db, 1);
|
|
|
|
|
2017-03-01 02:28:25 -05:00
|
|
|
let db = db.db.lock();
|
2017-02-26 23:10:02 -05:00
|
|
|
let segment = {
|
|
|
|
let all_time = recording::Time(i64::min_value()) .. recording::Time(i64::max_value());
|
|
|
|
let mut row = None;
|
2018-02-23 12:19:42 -05:00
|
|
|
db.list_recordings_by_time(testutil::TEST_STREAM_ID, all_time, &mut |r| {
|
2017-02-26 23:10:02 -05:00
|
|
|
row = Some(r);
|
|
|
|
Ok(())
|
|
|
|
}).unwrap();
|
|
|
|
let row = row.unwrap();
|
2020-08-07 18:30:22 -04:00
|
|
|
let rel_range_90k = 0 .. row.media_duration_90k;
|
|
|
|
super::Segment::new(&db, &row, rel_range_90k, 1, true).unwrap()
|
2017-02-26 23:10:02 -05:00
|
|
|
};
|
2018-08-24 01:34:40 -04:00
|
|
|
db.with_recording_playback(segment.s.id, &mut |playback| {
|
2017-03-01 02:28:25 -05:00
|
|
|
let v = segment.build_index(playback).unwrap(); // warm.
|
|
|
|
b.bytes = v.len() as u64; // define the benchmark performance in terms of output bytes.
|
|
|
|
b.iter(|| segment.build_index(playback).unwrap());
|
|
|
|
Ok(())
|
|
|
|
}).unwrap();
|
2017-02-26 23:10:02 -05:00
|
|
|
}
|
|
|
|
|
2016-11-25 17:34:00 -05:00
|
|
|
/// Benchmarks serving the generated part of a `.mp4` file (up to the first byte from disk).
|
|
|
|
#[bench]
|
2018-12-28 22:53:29 -05:00
|
|
|
fn serve_generated_bytes(b: &mut test::Bencher) {
|
2016-11-30 14:17:46 -05:00
|
|
|
testutil::init();
|
2016-11-25 17:34:00 -05:00
|
|
|
let server = &*SERVER;
|
|
|
|
let p = server.generated_len;
|
|
|
|
b.bytes = p;
|
2017-11-17 02:01:09 -05:00
|
|
|
let client = reqwest::Client::new();
|
2021-01-27 14:47:52 -05:00
|
|
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
|
|
let run = || {
|
2020-02-17 02:58:07 -05:00
|
|
|
rt.block_on(async {
|
|
|
|
let resp =
|
|
|
|
client.get(server.url.clone())
|
|
|
|
.header(reqwest::header::RANGE, format!("bytes=0-{}", p - 1))
|
|
|
|
.send()
|
|
|
|
.await
|
|
|
|
.unwrap();
|
|
|
|
let b = resp.bytes().await.unwrap();
|
|
|
|
assert_eq!(p, b.len() as u64);
|
|
|
|
});
|
2017-02-26 22:05:05 -05:00
|
|
|
};
|
|
|
|
run(); // warm.
|
|
|
|
b.iter(run);
|
2016-11-25 17:34:00 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
#[bench]
|
2018-12-28 22:53:29 -05:00
|
|
|
fn mp4_construction(b: &mut test::Bencher) {
|
2016-11-30 14:17:46 -05:00
|
|
|
testutil::init();
|
2018-03-23 16:31:23 -04:00
|
|
|
let db = TestDb::new(RealClocks {});
|
2017-02-12 23:37:03 -05:00
|
|
|
testutil::add_dummy_recordings_to_db(&db.db, 60);
|
2016-11-25 17:34:00 -05:00
|
|
|
b.iter(|| {
|
2018-02-12 01:45:51 -05:00
|
|
|
create_mp4_from_db(&db, 0, 0, false);
|
2016-11-25 17:34:00 -05:00
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|