Skip to content

Commit 729b770

Browse files
committed
Support file and stdin MP4 input
1 parent 3afde6a commit 729b770

8 files changed

Lines changed: 118 additions & 12 deletions

File tree

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,12 +157,35 @@ OPENMOQ_PICOQUIC_TRACE=1 ./build/openmoq-publisher-picoquic-smoke-tests
157157

158158
### CLI dry-run testing
159159

160+
Input source note:
161+
162+
- `--input <path>` reads an MP4 from a regular file
163+
- `--input -` reads the MP4 byte stream from standard input, which allows `cat`, `ffmpeg`, or other producer pipelines to feed the publisher directly
164+
- fragmented and progressive MP4 inputs are both supported through either source type
165+
160166
Use `--dump-plan` to inspect the generated publish plan without touching the network:
161167

162168
```bash
163169
./build/openmoq-publisher --input sample.mp4 --draft 14 --dump-plan
164170
```
165171

172+
Use stdin when the source is already being produced by another command:
173+
174+
```bash
175+
cat sample.mp4 | ./build/openmoq-publisher --input - --draft 14 --dump-plan
176+
```
177+
178+
For example, you can inspect an ffmpeg-produced fragmented stream without writing an intermediate file:
179+
180+
```bash
181+
ffmpeg -i input.mp4 \
182+
-map 0:v -map 0:a \
183+
-c:v copy \
184+
-c:a copy \
185+
-movflags +frag_keyframe+empty_moov+default_base_moof+separate_moof \
186+
-f mp4 - | ./build/openmoq-publisher --input - --draft 14 --dump-plan
187+
```
188+
166189
Use `--emit-dir` to inspect the emitted catalog and media objects on disk:
167190

168191
```bash
@@ -274,6 +297,19 @@ Transport-oriented CLI flags are also present now:
274297
--insecure
275298
```
276299

300+
The same CLI accepts stdin for transport publishing as well:
301+
302+
```bash
303+
cat sample.mp4 | ./build/openmoq-publisher \
304+
--input - \
305+
--endpoint localhost:4433 \
306+
--namespace media \
307+
--forward 0 \
308+
--timeout 3 \
309+
--paced \
310+
--insecure
311+
```
312+
277313
Chunk/object mapping:
278314

279315
- default behavior is lower-latency split publication, which emits multiple MOQT objects in the same group when CMAF chunk/sample boundaries are available

include/openmoq/publisher/cli_options.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,18 @@
1010

1111
namespace openmoq::publisher {
1212

13+
enum class InputSourceKind {
14+
kFile,
15+
kStdin,
16+
};
17+
18+
struct InputSource {
19+
InputSourceKind kind = InputSourceKind::kFile;
20+
std::filesystem::path path;
21+
};
22+
1323
struct CliOptions {
14-
std::filesystem::path input_path;
24+
InputSource input_source;
1525
std::optional<std::filesystem::path> emit_dir;
1626
std::optional<transport::EndpointConfig> endpoint;
1727
transport::TlsConfig tls;

include/openmoq/publisher/mp4_box.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
#include <cstddef>
44
#include <cstdint>
5+
#include <iosfwd>
56
#include <span>
67
#include <string>
78
#include <string_view>
@@ -46,6 +47,7 @@ struct ParsedMp4 {
4647
};
4748

4849
ParsedMp4 parse_mp4_file(const std::string& path);
50+
ParsedMp4 parse_mp4_stream(std::istream& input, std::string_view source_name);
4951
std::vector<Mp4Box> parse_mp4_boxes(std::span<const std::uint8_t> bytes);
5052
std::vector<TrackDescription> extract_tracks(const std::vector<Mp4Box>& top_level_boxes,
5153
std::span<const std::uint8_t> bytes);

src/cli_options.cpp

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ namespace openmoq::publisher {
77

88
namespace {
99

10+
InputSource parse_input_source(std::string_view value) {
11+
if (value == "-") {
12+
return InputSource{.kind = InputSourceKind::kStdin, .path = {}};
13+
}
14+
return InputSource{.kind = InputSourceKind::kFile, .path = std::filesystem::path(value)};
15+
}
16+
1017
DraftVersion parse_draft(std::string_view value) {
1118
if (value == "14") {
1219
return DraftVersion::kDraft14;
@@ -84,7 +91,7 @@ CliOptions parse_cli_options(int argc, char** argv) {
8491
};
8592

8693
if (argument == "--input") {
87-
options.input_path = require_value("--input");
94+
options.input_source = parse_input_source(require_value("--input"));
8895
} else if (argument == "--endpoint") {
8996
options.endpoint = parse_endpoint(require_value("--endpoint"));
9097
} else if (argument == "--alpn") {
@@ -131,7 +138,7 @@ CliOptions parse_cli_options(int argc, char** argv) {
131138
}
132139
}
133140

134-
if (options.input_path.empty()) {
141+
if (options.input_source.kind == InputSourceKind::kFile && options.input_source.path.empty()) {
135142
throw std::runtime_error("missing required --input argument");
136143
}
137144

@@ -147,7 +154,7 @@ CliOptions parse_cli_options(int argc, char** argv) {
147154

148155
std::string build_usage(const char* argv0) {
149156
return std::string("Usage: ") + argv0 +
150-
" --input <mp4> [--draft 14|16] [--namespace <value>] [--forward 0|1] [--timeout <seconds>]"
157+
" --input <mp4|-> [--draft 14|16] [--namespace <value>] [--forward 0|1] [--timeout <seconds>]"
151158
" [--publish-catalog] [--coalesce-cmaf-chunks|--coalesce-cmaf-chunk] [--paced] [--dump-plan] [--emit-dir <dir>]"
152159
" [--endpoint host:port|moqt://host:port/path] [--alpn value] [--sni value]"
153160
" [--cert file] [--key file] [--ca file] [--insecure]";

src/main.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ int main(int argc, char** argv) {
1313

1414
try {
1515
const CliOptions options = parse_cli_options(argc, argv);
16-
const ParsedMp4 parsed_mp4 = parse_mp4_file(options.input_path.string());
16+
const ParsedMp4 parsed_mp4 = options.input_source.kind == InputSourceKind::kStdin
17+
? parse_mp4_stream(std::cin, "stdin")
18+
: parse_mp4_file(options.input_source.path.string());
1719
const SegmentedMp4 segmented_mp4 = segment_for_cmaf(parsed_mp4,
1820
options.split_cmaf_chunks ? CmafObjectMode::kSplit
1921
: CmafObjectMode::kCoalesced);

src/mp4_box.cpp

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -285,16 +285,43 @@ ParsedMp4 parse_mp4_file(const std::string& path) {
285285
throw std::runtime_error("failed to open MP4 file: " + path);
286286
}
287287

288-
input.seekg(0, std::ios::end);
289-
const auto size = static_cast<std::size_t>(input.tellg());
290-
input.seekg(0, std::ios::beg);
288+
return parse_mp4_stream(input, path);
289+
}
291290

291+
ParsedMp4 parse_mp4_stream(std::istream& input, std::string_view source_name) {
292292
ParsedMp4 parsed;
293-
parsed.bytes.resize(size);
294-
input.read(reinterpret_cast<char*>(parsed.bytes.data()), static_cast<std::streamsize>(size));
295293

296-
if (!input) {
297-
throw std::runtime_error("failed to read MP4 file: " + path);
294+
input.seekg(0, std::ios::end);
295+
if (input.good()) {
296+
const auto end = input.tellg();
297+
if (end >= 0) {
298+
parsed.bytes.resize(static_cast<std::size_t>(end));
299+
input.seekg(0, std::ios::beg);
300+
input.read(reinterpret_cast<char*>(parsed.bytes.data()), static_cast<std::streamsize>(parsed.bytes.size()));
301+
if (!input) {
302+
throw std::runtime_error("failed to read MP4 input: " + std::string(source_name));
303+
}
304+
}
305+
}
306+
307+
if (parsed.bytes.empty()) {
308+
input.clear();
309+
input.seekg(0, std::ios::beg);
310+
311+
constexpr std::size_t kChunkSize = 16 * 1024;
312+
std::array<char, kChunkSize> buffer{};
313+
while (input) {
314+
input.read(buffer.data(), static_cast<std::streamsize>(buffer.size()));
315+
const auto bytes_read = input.gcount();
316+
if (bytes_read > 0) {
317+
parsed.bytes.insert(parsed.bytes.end(),
318+
reinterpret_cast<const std::uint8_t*>(buffer.data()),
319+
reinterpret_cast<const std::uint8_t*>(buffer.data()) + bytes_read);
320+
}
321+
}
322+
if (!input.eof()) {
323+
throw std::runtime_error("failed to read MP4 input: " + std::string(source_name));
324+
}
298325
}
299326

300327
parsed.top_level_boxes = parse_mp4_boxes(parsed.bytes);

tests/cli_options_test.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ int main() {
3838
ok &= expect(options.subscriber_timeout == std::chrono::seconds(3),
3939
"expected default subscriber timeout to remain 3 seconds");
4040
ok &= expect(options.split_cmaf_chunks, "expected chunk splitting to be enabled by default");
41+
ok &= expect(options.input_source.kind == openmoq::publisher::InputSourceKind::kFile,
42+
"expected file input to remain the default input source kind");
43+
ok &= expect(options.input_source.path == "sample.mp4",
44+
"expected file input path to be preserved");
4145
}
4246

4347
{
@@ -93,5 +97,13 @@ int main() {
9397
ok &= expect(threw, "expected negative --timeout to be rejected");
9498
}
9599

100+
{
101+
const CliOptions options = parse({"openmoq-publisher", "--input", "-"});
102+
ok &= expect(options.input_source.kind == openmoq::publisher::InputSourceKind::kStdin,
103+
"expected --input - to select stdin");
104+
ok &= expect(options.input_source.path.empty(),
105+
"expected stdin input source to avoid storing a file path");
106+
}
107+
96108
return ok ? 0 : 1;
97109
}

tests/cmaf_segmenter_test.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <cstdint>
66
#include <iostream>
77
#include <map>
8+
#include <sstream>
89
#include <string>
910
#include <vector>
1011

@@ -333,6 +334,15 @@ int main() {
333334
ok &= expect(payload_size(segmented.initialization_segment) > 0, "expected fragmented init payload");
334335
ok &= expect(payload_size(segmented.fragments.front().payload) > 0, "expected fragmented media payload");
335336

337+
std::stringstream fragmented_stream(std::ios::in | std::ios::out | std::ios::binary);
338+
fragmented_stream.write(reinterpret_cast<const char*>(fragmented_bytes.data()),
339+
static_cast<std::streamsize>(fragmented_bytes.size()));
340+
fragmented_stream.seekg(0, std::ios::beg);
341+
const ParsedMp4 parsed_from_stream = parse_mp4_stream(fragmented_stream, "memory");
342+
ok &= expect(parsed_from_stream.bytes == fragmented_bytes, "expected stream parser to preserve input bytes");
343+
ok &= expect(parsed_from_stream.top_level_boxes.size() == 4, "expected stream parser to decode top-level boxes");
344+
ok &= expect(parsed_from_stream.tracks.size() == 1, "expected stream parser to extract track metadata");
345+
336346
const auto multitrack_fragmented_bytes = make_multitrack_fragmented_test_mp4();
337347
ParsedMp4 multitrack_fragmented{
338348
.bytes = multitrack_fragmented_bytes,

0 commit comments

Comments
 (0)