Feature: Support HLS with fmp4 segment for HEVC/LLHLS. v7.0.51 (#4159)

Currently, SRS only supports HLS with MPEG-TS format segment files, but
for LL-HLS and HEVC, it requires the fMP4 format. See #4327 for details.
Furthermore, fMP4 has a smaller overhead compared to TS, and fMP4 can be
used for DVR. In short, fMP4 is definitely the future segment format for
HLS.

Start SRS with the config file that enables HLS with fMP4:

```
./objs/srs -c conf/hls.mp4.conf
```

Publish stream by FFmpeg:

```
ffmpeg -re -i doc/source.flv -c copy -f flv rtmp://localhost/live/livestream
```

Play the stream by SRS player:
[http://localhost:8080/live/livestream.m3u8](http://localhost:8080/players/srs_player.html?stream=livestream.m3u8)

Finished by AI:

* [AI: Change init.mp4 to the same directory of
m3u8.](17621c8442)
* [AI: Fix the error handling
bug.](af3758a592)
* [AI: Fix Chrome stuttering
problem.](aaab60c314)

---------

Co-authored-by: winlin <winlinvip@gmail.com>
This commit is contained in:
Jacob Su 2025-08-12 08:55:06 +08:00 committed by GitHub
parent c762e8204a
commit 339897e0c7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 4751 additions and 141 deletions

View File

@ -3,14 +3,9 @@
![](http://ossrs.net/gif/v1/sls.gif?site=github.com&path=/srs/develop)
[![](https://github.com/ossrs/srs/actions/workflows/codeql-analysis.yml/badge.svg?branch=develop)](https://github.com/ossrs/srs/actions?query=workflow%3ACodeQL+branch%3Adevelop)
[![](https://github.com/ossrs/srs/actions/workflows/release.yml/badge.svg)](https://github.com/ossrs/srs/actions/workflows/release.yml?query=workflow%3ARelease)
[![](https://github.com/ossrs/srs/actions/workflows/test.yml/badge.svg?branch=develop)](https://github.com/ossrs/srs/actions?query=workflow%3ATest+branch%3Adevelop)
[![](https://codecov.io/gh/ossrs/srs/branch/develop/graph/badge.svg?token=Zx2LhdtA39)](https://app.codecov.io/gh/ossrs/srs/tree/develop)
[![](https://ossrs.net/wiki/images/wechat-badge4.svg)](https://ossrs.net/lts/zh-cn/contact#discussion)
[![](https://img.shields.io/twitter/follow/srs_server?style=social)](https://twitter.com/srs_server)
[![](https://img.shields.io/badge/SRS-YouTube-red)](https://www.youtube.com/@srs_server)
[![](https://badgen.net/discord/members/yZ4BnPmHAd)](https://discord.gg/yZ4BnPmHAd)
[![](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fossrs%2Fsrs.svg?type=small)](https://app.fossa.com/projects/git%2Bgithub.com%2Fossrs%2Fsrs?ref=badge_small)
[![](https://badgen.net/badge/srs/stackoverflow/orange?icon=terminal)](https://stackoverflow.com/questions/tagged/simple-realtime-server)
[![](https://opencollective.com/srs-server/tiers/badge.svg)](https://opencollective.com/srs-server)
[![](https://img.shields.io/docker/pulls/ossrs/srs)](https://hub.docker.com/r/ossrs/srs/tags)

View File

@ -114,3 +114,86 @@ func TestFast_RtmpPublish_HlsPlay_Basic(t *testing.T) {
}
}
}
func TestFast_RtmpPublish_HlsPlay_Fmp4(t *testing.T) {
// This case is run in parallel.
t.Parallel()
// Setup the max timeout for this case.
ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond)
defer cancel()
// Check a set of errors.
var r0, r1, r2, r3, r4 error
defer func(ctx context.Context) {
if err := filterTestError(ctx.Err(), r0, r1, r2, r3, r4); err != nil {
t.Errorf("Fail for err %+v", err)
} else {
logger.Tf(ctx, "test done with err %+v", err)
}
}(ctx)
var wg sync.WaitGroup
defer wg.Wait()
// Start SRS server and wait for it to be ready.
svr := NewSRSServer(func(v *srsServer) {
v.envs = []string{
"SRS_HTTP_SERVER_ENABLED=on",
"SRS_VHOST_HLS_ENABLED=on",
"SRS_VHOST_HLS_HLS_USE_FMP4=on",
}
})
wg.Add(1)
go func() {
defer wg.Done()
r0 = svr.Run(ctx, cancel)
}()
// Start FFmpeg to publish stream.
streamID := fmt.Sprintf("stream-%v-%v", os.Getpid(), rand.Int())
streamURL := fmt.Sprintf("rtmp://localhost:%v/live/%v", svr.RTMPPort(), streamID)
ffmpeg := NewFFmpeg(func(v *ffmpegClient) {
v.args = []string{
"-stream_loop", "-1", "-re", "-i", *srsPublishAvatar, "-c", "copy", "-f", "flv", streamURL,
}
})
wg.Add(1)
go func() {
defer wg.Done()
<-svr.ReadyCtx().Done()
r1 = ffmpeg.Run(ctx, cancel)
}()
// Start FFprobe to detect and verify stream.
duration := time.Duration(*srsFFprobeDuration) * time.Millisecond
ffprobe := NewFFprobe(func(v *ffprobeClient) {
v.dvrFile = path.Join(svr.WorkDir(), "objs", fmt.Sprintf("srs-ffprobe-%v.mp4", streamID))
v.streamURL = fmt.Sprintf("http://localhost:%v/live/%v.m3u8", svr.HTTPPort(), streamID)
v.duration, v.timeout = duration, time.Duration(*srsFFprobeTimeout)*time.Millisecond
})
wg.Add(1)
go func() {
defer wg.Done()
<-svr.ReadyCtx().Done()
r2 = ffprobe.Run(ctx, cancel)
}()
// Fast quit for probe done.
select {
case <-ctx.Done():
case <-ffprobe.ProbeDoneCtx().Done():
defer cancel()
str, m := ffprobe.Result()
if len(m.Streams) != 2 {
r3 = errors.Errorf("invalid streams=%v, %v, %v", len(m.Streams), m.String(), str)
}
// Note that HLS score is low, so we only check duration. Note that only check half of duration, because we
// might get only some pieces of segments.
if dv := m.Duration(); dv < duration/2 {
r4 = errors.Errorf("short duration=%v < %v, %v, %v", dv, duration/2, m.String(), str)
}
}
}

View File

@ -568,6 +568,8 @@ func (v *srsServer) Run(ctx context.Context, cancel context.CancelFunc) error {
"SRS_VHOST_HLS_HLS_PATH=./objs/nginx/html",
"SRS_VHOST_HLS_HLS_M3U8_FILE=[app]/[stream].m3u8",
"SRS_VHOST_HLS_HLS_TS_FILE=[app]/[stream]-[seq].ts",
"SRS_VHOST_HLS_HLS_FMP4_FILE=[app]/[stream]-[seq].m4s",
"SRS_VHOST_HLS_HLS_INIT_FILE=[app]/[stream]-init.mp4",
}...)
// For variables.
v.process.env = append(v.process.env, []string{

View File

@ -1821,6 +1821,13 @@ vhost hls.srs.com {
# default: off
enabled on;
# whether to use fmp4 as container
# The default value is off, then HLS use ts as container format,
# if on, HLS use fmp4 as container format.
# Overwrite by env SRS_VHOST_HLS_HLS_USE_FMP4 for all vhosts.
# default: off
hls_use_fmp4 on;
# the hls fragment in seconds, the duration of a piece of ts.
# Overwrite by env SRS_VHOST_HLS_HLS_FRAGMENT for all vhosts.
# default: 10
@ -1886,6 +1893,44 @@ vhost hls.srs.com {
# Overwrite by env SRS_VHOST_HLS_HLS_TS_FILE for all vhosts.
# default: [app]/[stream]-[seq].ts
hls_ts_file [app]/[stream]-[seq].ts;
# the hls fmp4 file name.
# we supports some variables to generate the filename.
# [vhost], the vhost of stream.
# [app], the app of stream.
# [stream], the stream name of stream.
# [2006], replace this const to current year.
# [01], replace this const to current month.
# [02], replace this const to current date.
# [15], replace this const to current hour.
# [04], replace this const to current minute.
# [05], replace this const to current second.p
# [999], replace this const to current millisecond.
# [timestamp],replace this const to current UNIX timestamp in ms.
# [seq], the sequence number of fmp4.
# [duration], replace this const to current ts duration.
# @see https://ossrs.net/lts/zh-cn/docs/v4/doc/dvr#custom-path
# @see https://ossrs.net/lts/zh-cn/docs/v4/doc/delivery-hls#hls-config
# Overwrite by env SRS_VHOST_HLS_HLS_FMP4_FILE for all vhosts.
# default: [app]/[stream]-[seq].m4s
hls_fmp4_file [app]/[stream]-[seq].m4s;
# the hls init mp4 file name.
# we supports some variables to generate the filename.
# [vhost], the vhost of stream.
# [app], the app of stream.
# [stream], the stream name of stream.
# [2006], replace this const to current year.
# [01], replace this const to current month.
# [02], replace this const to current date.
# [15], replace this const to current hour.
# [04], replace this const to current minute.
# [05], replace this const to current second.
# [999], replace this const to current millisecond.
# [timestamp],replace this const to current UNIX timestamp in ms.
# @see https://ossrs.net/lts/zh-cn/docs/v4/doc/dvr#custom-path
# @see https://ossrs.net/lts/zh-cn/docs/v4/doc/delivery-hls#hls-config
# Overwrite by env SRS_VHOST_HLS_HLS_INIT_FILE for all vhosts.
# default: [app]/[stream]/init.mp4
hls_init_file [app]/[stream]/init.mp4;
# the hls entry prefix, which is base url of ts url.
# for example, the prefix is:
# http://your-server/

29
trunk/conf/hls.mp4.conf Normal file
View File

@ -0,0 +1,29 @@
# the config for srs to delivery hls
# @see https://ossrs.net/lts/zh-cn/docs/v4/doc/sample-hls
# @see full.conf for detail config.
listen 1935;
max_connections 1000;
daemon off;
srs_log_tank console;
http_server {
enabled on;
listen 8080;
dir ./objs/nginx/html;
}
http_api {
enabled on;
listen 1985;
}
vhost __defaultVhost__ {
hls {
enabled on;
hls_use_fmp4 on;
hls_path ./objs/nginx/html;
hls_fragment 10;
hls_window 60;
hls_m3u8_file [app]/[stream].m3u8;
hls_init_file [app]/[stream]-init.mp4;
hls_fmp4_file [app]/[stream]-[seq].m4s;
}
}

2
trunk/configure vendored
View File

@ -471,7 +471,7 @@ if [[ $SRS_UTEST == YES ]]; then
"srs_utest_config" "srs_utest_rtmp" "srs_utest_http" "srs_utest_avc" "srs_utest_reload"
"srs_utest_mp4" "srs_utest_service" "srs_utest_app" "srs_utest_rtc" "srs_utest_config2"
"srs_utest_protocol" "srs_utest_protocol2" "srs_utest_kernel2" "srs_utest_protocol3"
"srs_utest_st" "srs_utest_rtc2" "srs_utest_rtc3")
"srs_utest_st" "srs_utest_rtc2" "srs_utest_rtc3" "srs_utest_fmp4")
if [[ $SRS_SRT == YES ]]; then
MODULE_FILES+=("srs_utest_srt")
fi

View File

@ -7,6 +7,7 @@ The changelog for SRS.
<a name="v7-changes"></a>
## SRS 7.0 Changelog
* v7.0, 2025-08-11, Merge [#4159](https://github.com/ossrs/srs/pull/4159): Feature: Support HLS with fmp4 segment for HEVC/LLHLS. v7.0.51 (#4159)
* v7.0, 2025-08-11, Merge [#4432](https://github.com/ossrs/srs/pull/4432): AI: HTTP-FLV: Fix heap-use-after-free crash during stream unmount. v7.0.50 (#4432)
* v7.0, 2025-07-28, Merge [#4245](https://github.com/ossrs/srs/pull/4245): Allow Forward to be configured with Env Var. v7.0.49 (#4245)
* v7.0, 2025-07-16, Merge [#4295](https://github.com/ossrs/srs/pull/4295): RTC: audio packet jitter buffer. v7.0.48 (#4295)

View File

@ -2693,7 +2693,7 @@ srs_error_t SrsConfig::check_normal_config()
&& m != "hls_storage" && m != "hls_mount" && m != "hls_td_ratio" && m != "hls_aof_ratio" && m != "hls_acodec" && m != "hls_vcodec"
&& m != "hls_m3u8_file" && m != "hls_ts_file" && m != "hls_ts_floor" && m != "hls_cleanup" && m != "hls_nb_notify"
&& m != "hls_wait_keyframe" && m != "hls_dispose" && m != "hls_keys" && m != "hls_fragments_per_key" && m != "hls_key_file"
&& m != "hls_key_file_path" && m != "hls_key_url" && m != "hls_dts_directly" && m != "hls_ctx" && m != "hls_ts_ctx") {
&& m != "hls_key_file_path" && m != "hls_key_url" && m != "hls_dts_directly" && m != "hls_ctx" && m != "hls_ts_ctx" && m != "hls_use_fmp4" && m != "hls_fmp4_file" && m != "hls_init_file") {
return srs_error_new(ERROR_SYSTEM_CONFIG_INVALID, "illegal vhost.hls.%s of %s", m.c_str(), vhost->arg0().c_str());
}
@ -7051,6 +7051,31 @@ bool SrsConfig::get_hls_enabled(SrsConfDirective* vhost)
return SRS_CONF_PREFER_FALSE(conf->arg0());
}
bool SrsConfig::get_hls_use_fmp4(std::string vhost)
{
SRS_OVERWRITE_BY_ENV_BOOL("srs.vhost.hls.hls_use_fmp4"); // SRS_VHOST_HLS_HLS_USE_FMP4
static bool DEFAULT = false;
SrsConfDirective* conf = get_vhost(vhost);
if (!conf) {
return DEFAULT;
}
conf = conf->get("hls");
if (!conf) {
return DEFAULT;
}
conf = conf->get("hls_use_fmp4");
if (!conf || conf->arg0().empty()) {
return DEFAULT;
}
return SRS_CONF_PREFER_FALSE(conf->arg0());
}
string SrsConfig::get_hls_entry_prefix(string vhost)
{
SRS_OVERWRITE_BY_ENV_STRING("srs.vhost.hls.hls_entry_prefix"); // SRS_VHOST_HLS_HLS_ENTRY_PREFIX
@ -7127,6 +7152,44 @@ string SrsConfig::get_hls_ts_file(string vhost)
return conf->arg0();
}
string SrsConfig::get_hls_fmp4_file(std::string vhost)
{
SRS_OVERWRITE_BY_ENV_STRING("srs.vhost.hls.hls_fmp4_file"); // SRS_VHOST_HLS_HLS_FMP4_FILE
static string DEFAULT = "[app]/[stream]-[seq].m4s";
SrsConfDirective* conf = get_hls(vhost);
if (!conf) {
return DEFAULT;
}
conf = conf->get("hls_fmp4_file");
if (!conf || conf->arg0().empty()) {
return DEFAULT;
}
return conf->arg0();
}
string SrsConfig::get_hls_init_file(std::string vhost)
{
SRS_OVERWRITE_BY_ENV_STRING("srs.vhost.hls.hls_init_file"); // SRS_VHOST_HLS_HLS_INIT_FILE
static string DEFAULT = "[app]/[stream]/init.mp4";
SrsConfDirective* conf = get_hls(vhost);
if (!conf) {
return DEFAULT;
}
conf = conf->get("hls_init_file");
if (!conf || conf->arg0().empty()) {
return DEFAULT;
}
return conf->arg0();
}
bool SrsConfig::get_hls_ts_floor(string vhost)
{
SRS_OVERWRITE_BY_ENV_BOOL("srs.vhost.hls.hls_ts_floor"); // SRS_VHOST_HLS_HLS_TS_FLOOR

View File

@ -943,6 +943,8 @@ public:
// Whether HLS is enabled.
virtual bool get_hls_enabled(std::string vhost);
virtual bool get_hls_enabled(SrsConfDirective* vhost);
// Whether HLS use fmp4 container format
virtual bool get_hls_use_fmp4(std::string vhost);
// Get the HLS m3u8 list ts segment entry prefix info.
virtual std::string get_hls_entry_prefix(std::string vhost);
// Get the HLS ts/m3u8 file store path.
@ -951,6 +953,10 @@ public:
virtual std::string get_hls_m3u8_file(std::string vhost);
// Get the HLS ts file path template.
virtual std::string get_hls_ts_file(std::string vhost);
// Get the HLS fmp4 file path template.
virtual std::string get_hls_fmp4_file(std::string vhost);
// Get the HLS init mp4 file path template.
virtual std::string get_hls_init_file(std::string vhost);
// Whether enable the floor(timestamp/hls_fragment) for variable timestamp.
virtual bool get_hls_ts_floor(std::string vhost);
// Get the hls fragment time, in srs_utime_t.
@ -995,6 +1001,7 @@ public:
// Whether enable hls_ctx
virtual bool get_hls_ctx_enabled(std::string vhost);
// Whether enable session for ts file.
// The ts file including .ts file for MPEG-ts segment, .m4s file and init.mp4 file for fmp4 segment.
virtual bool get_hls_ts_ctx_enabled(std::string vhost);
// hds section
private:

File diff suppressed because it is too large Load Diff

View File

@ -16,6 +16,7 @@
#include <srs_kernel_file.hpp>
#include <srs_app_async_call.hpp>
#include <srs_app_fragment.hpp>
#include <srs_kernel_mp4.hpp>
class SrsFormat;
class SrsSharedPtrMessage;
@ -32,11 +33,13 @@ class SrsTsAacJitter;
class SrsTsMessageCache;
class SrsHlsSegment;
class SrsTsContext;
class SrsFmp4SegmentEncoder;
// The wrapper of m3u8 segment from specification:
//
// 3.3.2. EXTINF
// The EXTINF tag specifies the duration of a media segment.
// TODO: refactor this to support fmp4 segment.
class SrsHlsSegment : public SrsFragment
{
public:
@ -56,11 +59,60 @@ public:
SrsHlsSegment(SrsTsContext* c, SrsAudioCodecId ac, SrsVideoCodecId vc, SrsFileWriter* w);
virtual ~SrsHlsSegment();
public:
void config_cipher(unsigned char* key,unsigned char* iv);
void config_cipher(unsigned char* key, unsigned char* iv);
// replace the placeholder
virtual srs_error_t rename();
};
class SrsInitMp4Segment : public SrsFragment
{
private:
SrsFileWriter* fw_;
SrsMp4M2tsInitEncoder init_;
private:
// Key ID for encryption
unsigned char kid_[16];
// Constant IV for encryption
unsigned char const_iv_[16];
// IV size (8 or 16 bytes)
uint8_t const_iv_size_;
public:
SrsInitMp4Segment(SrsFileWriter* fw);
virtual ~SrsInitMp4Segment();
public:
virtual srs_error_t config_cipher(unsigned char* kid, unsigned char* const_iv, uint8_t const_iv_size);
// Write the init mp4 file, with the v_tid(video track id) and a_tid (audio track id).
virtual srs_error_t write(SrsFormat* format, int v_tid, int a_tid);
virtual srs_error_t write_video_only(SrsFormat* format, int v_tid);
virtual srs_error_t write_audio_only(SrsFormat* format, int a_tid);
private:
virtual srs_error_t init_encoder();
};
// TODO: merge this code with SrsFragmentedMp4 in dash
class SrsHlsM4sSegment : public SrsFragment
{
private:
SrsFileWriter* fw_;
SrsFmp4SegmentEncoder enc_;
public:
// m4s uri in m3u8.
std::string uri;
// sequence number in m3u8.
int sequence_no;
// IV for encryption, saved in m3u8 file.
unsigned char iv[16];
public:
SrsHlsM4sSegment(SrsFileWriter* fw);
virtual ~SrsHlsM4sSegment();
public:
virtual srs_error_t initialize(int64_t time, uint32_t v_tid, uint32_t a_tid, int sequence_number, std::string m4s_path);
virtual void config_cipher(unsigned char* key, unsigned char* iv);
virtual srs_error_t write(SrsSharedPtrMessage* shared_msg, SrsFormat* format);
// Finalizes segment
virtual srs_error_t reap(uint64_t dts);
};
// The hls async call: on_hls
class SrsDvrAsyncCallOnHls : public ISrsAsyncCallTask
{
@ -103,6 +155,7 @@ public:
//
// That is, user must use HlsCache, which will control the methods of muxer,
// and provides HLS mechenisms.
// TODO: Rename to SrsHlsTsMuxer, for TS file only.
class SrsHlsMuxer
{
private:
@ -217,6 +270,157 @@ private:
virtual srs_error_t _refresh_m3u8(std::string m3u8_file);
};
// Mux the HLS stream(m3u8 and m4s files).
// Generally, the m3u8 muxer only provides methods to open/close segments,
// to flush video/audio, without any mechenisms.
class SrsHlsFmp4Muxer
{
private:
SrsRequest* req_;
private:
std::string hls_entry_prefix_;
std::string hls_path_;
std::string hls_m4s_file_;
bool hls_cleanup_;
bool hls_wait_keyframe_;
std::string m3u8_dir_;
double hls_aof_ratio_;
// TODO: FIXME: Use TBN 1000.
srs_utime_t hls_fragment_;
srs_utime_t hls_window_;
SrsAsyncCallWorker* async_;
private:
// Whether use floor algorithm for timestamp.
bool hls_ts_floor_;
// The deviation in piece to adjust the fragment to be more
// bigger or smaller.
int deviation_ts_;
// The previous reap floor timestamp,
// used to detect the dup or jmp or ts.
int64_t accept_floor_ts_;
int64_t previous_floor_ts_;
bool init_mp4_ready_;
private:
// Whether encrypted or not
// TODO: fmp4 encryption is not yet implemented.
// fmp4 support four kinds of protection scheme: 'cenc', 'cbc1', 'cens', 'cbcs'.
// @see: https://cdn.standards.iteh.ai/samples/84637/04ebded1a92a4c8ab9be6f419a3252ed/ISO-IEC-23001-7-2023.pdf
// But unfortunately the above link is just part of the spec, the full doc is not free.
// And Apple's doc said HLS support unencrypted and encrypted with 'cbcs'.
// @see: https://developer.apple.com/documentation/http-live-streaming/about-the-common-media-application-format-with-http-live-streaming-hls
// Another Apple doc said Encrypted fmp4 content MUST contain either a Sample Encryption Box('senc'), or both a Sample Auxiliary Information
// Sizes Box('saiz') and a Sample Auxiliary Information Offsets Box('saio').
// @see: https://developer.apple.com/documentation/http-live-streaming/hls-authoring-specification-for-apple-devices
bool hls_keys_;
int hls_fragments_per_key_;
// The key file name
std::string hls_key_file_;
// The key file path
std::string hls_key_file_path_;
// The key file url
std::string hls_key_url_;
// The key and iv.
unsigned char key_[16];
unsigned char kid_[16];
unsigned char iv_[16];
// The underlayer file writer.
SrsFileWriter* writer_;
private:
int sequence_no_;
srs_utime_t max_td_;
std::string m3u8_;
std::string m3u8_url_;
std::string init_mp4_uri_; // URI for init.mp4 in m3u8 playlist
int video_track_id_;
int audio_track_id_;
uint64_t video_dts_;
private:
// The available cached segments in m3u8.
SrsFragmentWindow* segments_;
// The current writing segment.
SrsHlsM4sSegment* current_;
private:
// Latest audio codec, parsed from stream.
SrsAudioCodecId latest_acodec_;
// Latest audio codec, parsed from stream.
SrsVideoCodecId latest_vcodec_;
public:
SrsHlsFmp4Muxer();
virtual ~SrsHlsFmp4Muxer();
public:
virtual void dispose();
public:
virtual int sequence_no();
virtual std::string m4s_url();
virtual srs_utime_t duration();
virtual int deviation();
public:
SrsAudioCodecId latest_acodec();
void set_latest_acodec(SrsAudioCodecId v);
SrsVideoCodecId latest_vcodec();
void set_latest_vcodec(SrsVideoCodecId v);
public:
// Initialize the hls muxer.
virtual srs_error_t initialize(int v_tid, int a_tid);
// When publish or unpublish stream.
virtual srs_error_t on_publish(SrsRequest* req);
public:
virtual srs_error_t write_init_mp4(SrsFormat* format, bool has_video, bool has_audio);
virtual srs_error_t write_audio(SrsSharedPtrMessage* shared_audio, SrsFormat* format);
virtual srs_error_t write_video(SrsSharedPtrMessage* shared_video, SrsFormat* format);
public:
virtual srs_error_t on_unpublish();
// When publish, update the config for muxer.
virtual srs_error_t update_config(SrsRequest* r);
// Open a new segment(a new ts file)
virtual srs_error_t segment_open(srs_utime_t basetime);
virtual srs_error_t on_sequence_header();
// Whether segment overflow,
// that is whether the current segment duration>=(the segment in config)
virtual bool is_segment_overflow();
// Whether wait keyframe to reap the ts.
virtual bool wait_keyframe();
// Whether segment absolutely overflow, for pure audio to reap segment,
// that is whether the current segment duration>=2*(the segment in config)
virtual bool is_segment_absolutely_overflow();
public:
// When flushing video or audio, we update the duration. But, we should also update the
// duration before closing the segment. Keep in mind that it's fine to update the duration
// several times using the same dts timestamp.
void update_duration(uint64_t dts);
// Close segment(ts).
virtual srs_error_t segment_close();
private:
virtual srs_error_t do_segment_close();
virtual srs_error_t write_hls_key();
virtual srs_error_t refresh_m3u8();
virtual srs_error_t _refresh_m3u8(std::string m3u8_file);
};
// The base class for HLS controller
class ISrsHlsController
{
public:
ISrsHlsController();
virtual ~ISrsHlsController();
public:
virtual srs_error_t initialize() = 0;
virtual void dispose() = 0;
// When publish or unpublish stream.
virtual srs_error_t on_publish(SrsRequest* req) = 0;
virtual srs_error_t on_unpublish() = 0;
public:
virtual srs_error_t write_audio(SrsSharedPtrMessage* shared_audio, SrsFormat* format) = 0;
virtual srs_error_t write_video(SrsSharedPtrMessage* shared_video, SrsFormat* format) = 0;
public:
virtual srs_error_t on_sequence_header(SrsSharedPtrMessage* msg, SrsFormat* format) = 0;
virtual int sequence_no() = 0;
// TODO: maybe rename to segment_url?
virtual std::string ts_url() = 0;
virtual srs_utime_t duration() = 0;
virtual int deviation() = 0;
};
// The hls stream cache,
// use to cache hls stream and flush to hls muxer.
//
@ -232,7 +436,8 @@ private:
// when timestamp convert to flv tbn, it will loose precise,
// so we must gather audio frame together, and recalc the timestamp @see SrsTsAacJitter,
// we use a aac jitter to correct the audio pts.
class SrsHlsController
// TODO: Rename to SrsHlsTsController, for TS file only.
class SrsHlsController : public ISrsHlsController
{
private:
// The HLS muxer to reap ts and m3u8.
@ -240,6 +445,14 @@ private:
SrsHlsMuxer* muxer;
// The TS cache
SrsTsMessageCache* tsmc;
// If the diff=dts-previous_audio_dts is about 23,
// that's the AAC samples is 1024, and we use the samples to calc the dts.
int64_t previous_audio_dts;
// The total aac samples.
uint64_t aac_samples;
// Whether directly turn FLV timestamp to TS DTS.
bool hls_dts_directly;
public:
SrsHlsController();
virtual ~SrsHlsController();
@ -258,11 +471,11 @@ public:
// must write a #EXT-X-DISCONTINUITY to m3u8.
// @see: hls-m3u8-draft-pantos-http-live-streaming-12.txt
// @see: 3.4.11. EXT-X-DISCONTINUITY
virtual srs_error_t on_sequence_header();
virtual srs_error_t on_sequence_header(SrsSharedPtrMessage* shared_audio, SrsFormat* format);
// write audio to cache, if need to flush, flush to muxer.
virtual srs_error_t write_audio(SrsAudioFrame* frame, int64_t pts);
virtual srs_error_t write_audio(SrsSharedPtrMessage* shared_audio, SrsFormat* format);
// write video to muxer.
virtual srs_error_t write_video(SrsVideoFrame* frame, int64_t dts);
virtual srs_error_t write_video(SrsSharedPtrMessage* shared_video, SrsFormat* format);
private:
// Reopen the muxer for a new hls segment,
// close current segment, open a new segment,
@ -271,12 +484,50 @@ private:
virtual srs_error_t reap_segment();
};
// Transmux RTMP stream to HLS(m3u8 and ts).
// HLS controller for fMP4 (.m4s) segments with init.mp4.
// Direct sample processing without caching, simpler than TS controller.
class SrsHlsMp4Controller : public ISrsHlsController
{
private:
bool has_video_sh_;
bool has_audio_sh_;
private:
int video_track_id_;
int audio_track_id_;
private:
// Current audio dts.
uint64_t audio_dts_;
// Current video dts.
uint64_t video_dts_;
private:
SrsRequest* req_;
private:
SrsHlsFmp4Muxer* muxer_;
public:
SrsHlsMp4Controller();
virtual ~SrsHlsMp4Controller();
public:
virtual srs_error_t initialize();
virtual void dispose();
// When publish or unpublish stream.
virtual srs_error_t on_publish(SrsRequest* req);
virtual srs_error_t on_unpublish();
virtual srs_error_t write_audio(SrsSharedPtrMessage* shared_audio, SrsFormat* format);
virtual srs_error_t write_video(SrsSharedPtrMessage* shared_video, SrsFormat* format);
public:
virtual srs_error_t on_sequence_header(SrsSharedPtrMessage* shared_audio, SrsFormat* format);
virtual int sequence_no();
virtual std::string ts_url();
virtual srs_utime_t duration();
virtual int deviation();
};
// Transmux RTMP stream to HLS(m3u8 and ts,fmp4).
// TODO: FIXME: add utest for hls.
class SrsHls
{
private:
SrsHlsController* controller;
ISrsHlsController* controller;
private:
SrsRequest* req;
// Whether the HLS is enabled.
@ -290,14 +541,6 @@ private:
bool reloading_;
// To detect heartbeat and dispose it if configured.
srs_utime_t last_update_time;
private:
// If the diff=dts-previous_audio_dts is about 23,
// that's the AAC samples is 1024, and we use the samples to calc the dts.
int64_t previous_audio_dts;
// The total aac samples.
uint64_t aac_samples;
// Whether directly turn FLV timestamp to TS DTS.
bool hls_dts_directly;
private:
SrsOriginHub* hub;
SrsRtmpJitter* jitter;

View File

@ -222,10 +222,11 @@ srs_error_t SrsHlsStream::serve_exists_session(ISrsHttpResponseWriter* w, ISrsHt
}
// Rebuild the m3u8 content, make .ts with hls_ctx.
size_t pos_ts = content.find(".ts");
static string QUERY_PREFIX = string(".ts?") + string(SRS_CONTEXT_IN_HLS) + string("=");
if (pos_ts != string::npos) {
static string M4S_QUERY_PREFIX = string(".m4s?") + string(SRS_CONTEXT_IN_HLS) + string("=");
static string INIT_MP4_QUERY_PREFIX = string("init.mp4?") + string(SRS_CONTEXT_IN_HLS) + string("=");
if (content.find(".ts") != string::npos) {
string ctx = r->query_get(SRS_CONTEXT_IN_HLS);
string query = QUERY_PREFIX + ctx;
@ -236,6 +237,28 @@ srs_error_t SrsHlsStream::serve_exists_session(ISrsHttpResponseWriter* w, ISrsHt
} else {
content = srs_string_replace(content, ".ts", query);
}
} else if (content.find(".m4s") != string::npos) {
string ctx = r->query_get(SRS_CONTEXT_IN_HLS);
string query = M4S_QUERY_PREFIX + ctx;
size_t pos_query = content.find(".m4s?");
if (pos_query != string::npos) {
query += "&";
content = srs_string_replace(content, ".m4s?", query);
} else {
content = srs_string_replace(content, ".m4s", query);
}
} else if (content.find("init.mp4") != string::npos) {
string ctx = r->query_get(SRS_CONTEXT_IN_HLS);
string query = INIT_MP4_QUERY_PREFIX + ctx;
size_t pos_query = content.find("init.mp4?");
if (pos_query != string::npos) {
query += "&";
content = srs_string_replace(content, "init.mp4?", query);
} else {
content = srs_string_replace(content, "init.mp4", query);
}
}
// Response with rebuilt content.

View File

@ -74,6 +74,7 @@ protected:
virtual srs_error_t serve_mp4_stream(ISrsHttpResponseWriter* w, ISrsHttpMessage* r, std::string fullpath, int64_t start, int64_t end);
// Support HLS streaming with pseudo session id.
virtual srs_error_t serve_m3u8_ctx(ISrsHttpResponseWriter* w, ISrsHttpMessage* r, std::string fullpath);
// the ts file including: .ts .m4s init.mp4
virtual srs_error_t serve_ts_ctx(ISrsHttpResponseWriter* w, ISrsHttpMessage* r, std::string fullpath);
};

View File

@ -130,8 +130,10 @@ private:
SrsTcpListener* https_listener_;
// WebRTC over TCP listener. Please note that there is always a UDP listener by RTC server.
SrsTcpListener* webrtc_listener_;
#ifdef SRS_RTSP
// RTSP listener, over TCP.
SrsTcpListener* rtsp_listener_;
#endif
// Stream Caster for push over HTTP-FLV.
SrsHttpFlvListener* stream_caster_flv_listener_;
// Stream Caster for push over MPEGTS-UDP

View File

@ -9,6 +9,6 @@
#define VERSION_MAJOR 7
#define VERSION_MINOR 0
#define VERSION_REVISION 50
#define VERSION_REVISION 51
#endif

View File

@ -1378,6 +1378,7 @@ public:
public:
virtual bool is_aac_sequence_header();
virtual bool is_mp3_sequence_header();
// TODO: is avc|hevc|av1 sequence header
virtual bool is_avc_sequence_header();
private:
// Demux the video packet in H.264 codec.

File diff suppressed because it is too large Load Diff

View File

@ -113,6 +113,27 @@ enum SrsMp4BoxType
SrsMp4BoxTypeSIDX = 0x73696478, // 'sidx'
SrsMp4BoxTypeHEV1 = 0x68657631, // 'hev1'
SrsMp4BoxTypeHVCC = 0x68766343, // 'hvcC'
SrsMp4BoxTypeSENC = 0x73656e63, // 'senc'
SrsMp4BoxTypeSAIZ = 0x7361697a, // 'saiz'
SrsMp4BoxTypeSAIO = 0x7361696f, // 'saio'
SrsMp4BoxTypeENCV = 0x656e6376, // 'encv'
SrsMp4BoxTypeENCA = 0x656e6361, // 'enca'
SrsMp4BoxTypeSINF = 0x73696e66, // 'sinf'
SrsMp4BoxTypeSCHI = 0x73636869, // 'schi'
SrsMp4BoxTypeTENC = 0x74656e63, // 'tenc'
SrsMp4BoxTypeFRMA = 0x66726d61, // 'frma'
SrsMp4BoxTypeSCHM = 0x7363686d, // 'schm'
};
// Common encryption scheme types
// @see ISO-IEC-23001-7.pdf, 4.2
enum SrsMp4CENSchemeType
{
SrsMp4CENSchemeCENC = 0x63656e63, // 'cenc'
SrsMp4CENSchemeCBC1 = 0x63626331, // 'cbc1'
SrsMp4CENSchemeCENS = 0x63656e73, // 'cens'
SrsMp4CENSchemeCBCS = 0x63626373, // 'cbcs'
SrsMp4CENSchemeSVE1 = 0x73766531, // 'sve1'
};
// 8.4.3.3 Semantics
@ -317,9 +338,9 @@ public:
// Get the header of moof.
virtual SrsMp4MovieFragmentHeaderBox* mfhd();
virtual void set_mfhd(SrsMp4MovieFragmentHeaderBox* v);
// Get the traf.
virtual SrsMp4TrackFragmentBox* traf();
virtual void set_traf(SrsMp4TrackFragmentBox* v);
// Let moof support more than one traf
virtual void add_traf(SrsMp4TrackFragmentBox* v);
};
// 8.8.5 Movie Fragment Header Box (mfhd)
@ -499,7 +520,7 @@ class SrsMp4TrackFragmentRunBox : public SrsMp4FullBox
public:
// The number of samples being added in this run; also the number of rows in the following
// table (the rows can be empty)
//uint32_t sample_count;
// uint32_t sample_count;
// The following are optional fields
public:
// added to the implicit or explicit data_offset established in the track fragment header.
@ -710,8 +731,7 @@ public:
virtual ~SrsMp4MovieExtendsBox();
public:
// Get the track extends box.
virtual SrsMp4TrackExtendsBox* trex();
virtual void set_trex(SrsMp4TrackExtendsBox* v);
virtual void add_trex(SrsMp4TrackExtendsBox* v);
};
// 8.8.3 Track Extends Box(trex)
@ -1869,6 +1889,348 @@ public:
virtual std::stringstream& dumps_detail(std::stringstream& ss, SrsMp4DumpContext dc);
};
// Sample auxiliary information sizes box (saiz)
// @see ISO_IEC_14496-12-base-format-2012.pdf, 8.7.8, page 62
// @see https://github.com/gpac/mp4box.js/blob/master/src/parsing/saiz.js
// Syntax
// aligned(8) class SampleAuxiliaryInformationSizesBox extends FullBox('saiz', version=0, flags)
// {
// if (flags & 1) {
// unsigned int(32) aux_info_type;
// unsigned int(32) aux_info_type_parameter;
// }
// unsigned int(8) default_sample_info_size;
// unsigned int(32) sample_count;
// if (default_sample_info_size == 0) {
// unsigned int(8) sample_info_size[sample_count];
// }
// }
class SrsMp4SampleAuxiliaryInfoSizeBox: public SrsMp4FullBox
{
public:
uint32_t aux_info_type;
uint32_t aux_info_type_parameter;
uint8_t default_sample_info_size;
uint32_t sample_count;
std::vector<uint8_t> sample_info_sizes;
public:
SrsMp4SampleAuxiliaryInfoSizeBox();
virtual ~SrsMp4SampleAuxiliaryInfoSizeBox();
protected:
virtual int nb_header();
virtual srs_error_t encode_header(SrsBuffer* buf);
virtual srs_error_t decode_header(SrsBuffer* buf);
public:
virtual std::stringstream& dumps_detail(std::stringstream& ss, SrsMp4DumpContext dc);
};
// Sample auxiliary information offsets box (saio)
// @see ISO_IEC_14496-12-base-format-2012.pdf, 8.7.9, page 63
// @see https://github.com/gpac/mp4box.js/blob/master/src/parsing/saio.js
// Syntax
// aligned(8) class SampleAuxiliaryInformationOffsetsBox extends FullBox('saio', version, flags)
// {
// if (flags & 1) {
// unsigned int(32) aux_info_type;
// unsigned int(32) aux_info_type_parameter;
// }
// unsigned int(32) entry_count;
// if (version == 0) {
// unsigned int(32) offset[entry_count];
// } else {
// unsigned int(64) offset[entry_count];
// }
// }
class SrsMp4SampleAuxiliaryInfoOffsetBox: public SrsMp4FullBox
{
public:
uint32_t aux_info_type;
uint32_t aux_info_type_parameter;
// uint32_t entry_count;
std::vector<uint64_t> offsets;
public:
SrsMp4SampleAuxiliaryInfoOffsetBox();
virtual ~SrsMp4SampleAuxiliaryInfoOffsetBox();
protected:
virtual int nb_header();
virtual srs_error_t encode_header(SrsBuffer* buf);
virtual srs_error_t decode_header(SrsBuffer* buf);
public:
virtual std::stringstream& dumps_detail(std::stringstream& ss, SrsMp4DumpContext dc);
};
enum SrsMp4CencSampleEncryptionFlags
{
SrsMp4CencSampleEncryptionTrackDefault = 0x01,
SrsMp4CencSampleEncryptionUseSubSample = 0x02,
};
struct SrsMp4SubSampleEncryptionInfo : public ISrsCodec
{
uint16_t bytes_of_clear_data;
uint32_t bytes_of_protected_data;
SrsMp4SubSampleEncryptionInfo();
virtual ~SrsMp4SubSampleEncryptionInfo();
virtual uint64_t nb_bytes();
virtual srs_error_t encode(SrsBuffer* buf);
virtual srs_error_t decode(SrsBuffer* buf);
virtual std::stringstream& dumps(std::stringstream& ss, SrsMp4DumpContext dc);
};
class SrsMp4SampleEncryptionEntry : public ISrsCodec
{
public:
// if flags && 0x02
std::vector<SrsMp4SubSampleEncryptionInfo> subsample_infos;
public:
SrsMp4SampleEncryptionEntry(SrsMp4FullBox* senc, uint8_t per_sample_iv_size);
virtual ~SrsMp4SampleEncryptionEntry();
virtual srs_error_t set_iv(uint8_t* iv, uint8_t iv_size);
virtual uint64_t nb_bytes();
virtual srs_error_t encode(SrsBuffer* buf);
virtual srs_error_t decode(SrsBuffer* buf);
virtual std::stringstream& dumps(std::stringstream& ss, SrsMp4DumpContext dc);
private:
SrsMp4FullBox* senc_;
uint8_t per_sample_iv_size_;
uint8_t* iv_;
};
// Sample encryption box (senc)
// @see ISO-IEC-23001-7.pdf 7.2.1
// @see https://cdn.standards.iteh.ai/samples/84637/c960c91d60ae4da7a2f9380bd7e08642/ISO-IEC-FDIS-23001-7.pdf
// CENC SAI: sample auxiliary information associated with a sample and containing cryptographic information
// such as initialization vector or subsample information
// @see ISO-IEC-23001-7.pdf 7.2.2
// Syntax
// aligned(8) class SampleEncryptionBox extend FullBox(`senc`, version=0, flags)
// {
// unsigned int(32) sample_count;
// {
// unsigned int(Per_Sample_IV_Size*8) InitializationVector;
// if (flags & 0x000002)
// {
// unsigned int(16) subsample_count;
// {
// unsigned int(16) BytesOfClearData;
// unsigned int(32) BytesOfProtectedData;
// } [ subsample_count ]
// }
// } [ sample_count ]
// }
class SrsMp4SampleEncryptionBox: public SrsMp4FullBox
{
public:
std::vector<SrsMp4SampleEncryptionEntry*> entries;
private:
uint8_t per_sample_iv_size_;
public:
// @see ISO-IEC-23001-7.pdf 9.1
// Per_Sample_IV_Size has supported values: 0, 8, 16.
SrsMp4SampleEncryptionBox(uint8_t per_sample_iv_size);
virtual ~SrsMp4SampleEncryptionBox();
protected:
virtual int nb_header();
virtual srs_error_t encode_header(SrsBuffer* buf);
virtual srs_error_t decode_header(SrsBuffer* buf);
public:
virtual std::stringstream& dumps_detail(std::stringstream& ss, SrsMp4DumpContext dc);
};
// Original Format Box (frma)
// @see ISO_IEC_14496-12-base-format-2012.pdf, 8.12.2, page 81
// aligned(8) class OriginalFormatBox(codingname) extends Box ('frma') {
// unsigned int(32) data_format = codingname;
// }
class SrsMp4OriginalFormatBox : public SrsMp4Box
{
private:
uint32_t data_format_;
public:
SrsMp4OriginalFormatBox(uint32_t original_format);
virtual ~SrsMp4OriginalFormatBox();
protected:
virtual int nb_header();
virtual srs_error_t encode_header(SrsBuffer* buf);
virtual srs_error_t decode_header(SrsBuffer* buf);
public:
virtual std::stringstream& dumps_detail(std::stringstream& ss, SrsMp4DumpContext dc);
};
// Scheme Type Box (schm)
// @see ISO_IEC_14496-12-base-format-2012.pdf, 8.12.5, page 81
// aligned(8) class SchemeTypeBox extends FullBox('schm', 0, flags) {
// unsigned int(32) scheme_type; // 4CC identifying the scheme
// unsigned int(32) scheme_version; // scheme version
// if (flags & 0x000001) {
// unsigned int(8) scheme_uri[]; // browser uri
// }
// }
// @see @see ISO-IEC-23001-7.pdf 4.1
// the scheme_version field SHALL be set to 0x00010000 (Major version 1, Minor version 0).
#define SCHM_SCHEME_URI_MAX_SIZE 128
class SrsMp4SchemeTypeBox : public SrsMp4FullBox
{
public:
uint32_t scheme_type;
uint32_t scheme_version;
char scheme_uri[SCHM_SCHEME_URI_MAX_SIZE];
uint32_t scheme_uri_size;
public:
SrsMp4SchemeTypeBox();
virtual ~SrsMp4SchemeTypeBox();
public:
virtual void set_scheme_uri(char* uri, uint32_t uri_size);
protected:
virtual int nb_header();
virtual srs_error_t encode_header(SrsBuffer* buf);
virtual srs_error_t decode_header(SrsBuffer* buf);
public:
virtual std::stringstream& dumps_detail(std::stringstream& ss, SrsMp4DumpContext dc);
};
// Scheme Information Box (schi)
// @see ISO_IEC_14496-12-base-format-2012.pdf, 8.12.6, page 82
// aligned(8) class SchemeInformationBox extends Box('schi') {
// Box scheme_specific_data[];
// }
class SrsMp4SchemeInfoBox : public SrsMp4Box
{
public:
SrsMp4SchemeInfoBox();
virtual ~SrsMp4SchemeInfoBox();
};
// Protection Scheme Information Box (sinf)
// @see ISO_IEC_14496-12-base-format-2012.pdf, 8.12.1, page 80
// aligned(8) class ProtectionSchemeInfoBox(fmt) extends Box('sinf') {
// OriginalFormatBox(fmt) original_format; // frma
// SchemeTypeBox scheme_type_box; // optional
// SchemeInformationBox info; // optional
// }
class SrsMp4ProtectionSchemeInfoBox : public SrsMp4Box
{
public:
SrsMp4ProtectionSchemeInfoBox();
virtual ~SrsMp4ProtectionSchemeInfoBox();
public:
// Get the Original Format Box (frma)
virtual SrsMp4OriginalFormatBox* frma();
virtual void set_frma(SrsMp4OriginalFormatBox* v);
// Get the Scheme Type Box (schm)
virtual SrsMp4SchemeTypeBox* schm();
virtual void set_schm(SrsMp4SchemeTypeBox* v);
// Get the Scheme Information Box (schi)
virtual SrsMp4SchemeInfoBox* schi();
virtual void set_schi(SrsMp4SchemeInfoBox* v);
};
// Track Encryption box (tenc)
// @see ISO-IEC-23001-7.pdf 8.2
// aligned(8) class TrackEncryptionBox extends FullBox('tenc', version, flags=0) {
// unsigned int(8) reserved = 0;
// if (version == 0) {
// unsigned int(8) reserved = 0;
// } else { // version is 1 or greater
// unsigned int(4) default_crypt_byte_block;
// unsigned int(4) default_skip_byte_block;
// }
// unsigned int(8) default_isProtected;
// unsigned int(8) default_Per_Sample_IV_Size;
// unsigned int(8)[16] default_KID;
// if (default_isProtected == 1 && default_Per_Sample_IV_Size == 0) {
// unsigned int(8) default_constant_IV_size;
// unsigned int(8)[default_constant_IV_size] default_constant_IV;
// }
// }
// @see https://developer.apple.com/documentation/http-live-streaming/about-the-common-media-application-format-with-http-live-streaming-hls
// For fragmented MPEG-4 Segments, an EXT-X-KEY tag with a METHOD=SAMPLE-AES attribute indicates that
// the Segment is encrypted using the `cbcs` scheme in ISO/IEC 23001-7.
// HLS supports unencrypted and encrypted with 'cbcs'.
// @see ISO-IEC-23001-7.pdf 10.4.1 Definition
// 'cbcs' AES-CBC subsample pattern encryption scheme.
// The 'scheme_type' field of the scheme Type Box('schm') SHALL be set to 'cbcs'.
// the version of the Track Encryption Box('tenc') SHALL be 1.
// Encrypted video tracks using NAL Structured Video conforming to ISO/IEC 14496-15 SHALL be
// protected using Subsample encryption specified in 9.5, and SHALL use pattern encryption as specified
// in 9.6. As a result, the fields crypt_byte_block and skip_byte_block SHALL NOT be 0.
// Constant IVs SHALL be used; 'default_Per_Sample_IV_Size' and 'Per_Sample_IV_Size', SHALL be 0.
// Tracks other than video are protected using whole-block full-sample encryption as specified in 9.7 and
// hence skip_byte_block SHALL be 0.
// Pattern Block length, i.e. crypt_byte_block + skip_byte_block SHOULD equal 10.
// For all video NAL units, including in 'avc1', the slice header SHALL be unencrypted.
// The first complete byte of video slice data(following the video slice header) SHALL begin a single
// Subsample protected byte range indicated by the start of BytesOfProtectedData, which extends to
// the end of the video NAL.
// NOTE 1 For AVC VCL NAL units, the encryption pattern starts at an offset rounded to the next byte after
// the slice header, i.e. on the first full byte of slice data. For HEVC, the encryption pattern starts after
// the byte_alignment() field that terminates the slice_segment_header(), i.e. on the first byte of slice data.
//
// @see ISO-IEC-23001-7.pdf 10.4.2 'cbcs' AES-CBC mode pattern encryption scheme application(informative)
// An encrypt:skip pattern of 1:9(i.e. 10% partial encryption) is recommended. Even though the syntax
// allows many different encryption patterns, a pattern of ten Blocks is recommended. This means that the
// skipped Blocks will be (10-N). The number of encrypted cipher blocks N can span multiple contiguous
// 16-byte Blocks(e.g. three encrypted Blocks followed by seven unencrypted Blocks would result in 30%
// partial encryption of the video data).
// For example, to achieve 10 % encryption, the first Block of the pattern is encrypted and the following
// nine Blocks are left unencrypted. The pattern is repeated every 160 bytes of the protected range, until
// the end of the range. If the protected range of the slice body is not a multiple of the pattern length
// (e.g. 160 bytes), then the pattern sequence applies to the included whole 16-byte Blocks and a partial
// 16-byte Block that may remain where the pattern is terminated by the byte length of the range
// BytesOfProtectedData, is left unencrypted.
//
// @see ISO-IEC-23001-7.pdf 9.7 Whole-block full sample encryption
// In whole-block full sample encryption, the entire sample is protected. Every sample is encrypted
// starting at offset 0(there is no unprotected preamble) up to the last 16-byte boundary, leaving any
// trailing 0-15 bytes in the clear. The IV is reset at every sample.
class SrsMp4TrackEncryptionBox : public SrsMp4FullBox
{
public:
uint8_t reserved;
uint8_t reserved_2;
uint8_t default_crypt_byte_block;
uint8_t default_skip_byte_block;
uint8_t default_is_protected;
uint8_t default_per_sample_IV_size;
uint8_t default_KID[16];
uint8_t default_constant_IV_size;
uint8_t default_constant_IV[16];
public:
SrsMp4TrackEncryptionBox();
virtual ~SrsMp4TrackEncryptionBox();
public:
virtual void set_default_constant_IV(uint8_t* iv, uint8_t iv_size);
protected:
virtual int nb_header();
virtual srs_error_t encode_header(SrsBuffer* buf);
virtual srs_error_t decode_header(SrsBuffer* buf);
public:
virtual std::stringstream& dumps_detail(std::stringstream& ss, SrsMp4DumpContext dc);
};
// TODO: add SchemeTypeBox(schm), set scheme_type=cbcs
// Generally, a MP4 sample contains a frame, for example, a video frame or audio frame.
class SrsMp4Sample
{
@ -1931,7 +2293,7 @@ public:
virtual srs_error_t write(SrsMp4MovieBox* moov);
// Write the samples info to moof.
// @param The dts is the dts of last segment.
virtual srs_error_t write(SrsMp4MovieFragmentBox* moof, uint64_t dts);
virtual srs_error_t write(SrsMp4TrackFragmentBox* traf, uint64_t dts);
private:
virtual srs_error_t write_track(SrsFrameType track,
SrsMp4DecodingTime2SampleBox* stts, SrsMp4SyncSampleBox* stss, SrsMp4CompositionTime2SampleBox* ctts,
@ -2114,22 +2476,67 @@ private:
};
// A fMP4 encoder, to write the init.mp4 with sequence header.
// TODO: What the M2ts short for?
class SrsMp4M2tsInitEncoder
{
private:
ISrsWriter* writer;
private:
uint8_t crypt_byte_block_;
uint8_t skip_byte_block_;
unsigned char kid_[16];
unsigned char iv_[16];
uint8_t iv_size_;
bool is_protected_;
public:
SrsMp4M2tsInitEncoder();
virtual ~SrsMp4M2tsInitEncoder();
public:
// Initialize the encoder with a writer w.
virtual srs_error_t initialize(ISrsWriter* w);
// set encryption
// TODO: review kid(map to a key) and iv, which are shared between audio/video tracks.
virtual void config_encryption(uint8_t crypt_byte_block, uint8_t skip_byte_block, unsigned char* kid, unsigned char* iv, uint8_t iv_size);
// Write the sequence header.
// TODO: merge this method to its sibling.
virtual srs_error_t write(SrsFormat* format, bool video, int tid);
/**
* The mp4 box format for init.mp4.
*
* |ftyp|
* |moov|
* | |mvhd|
* | |trak|
* | |trak|
* | |....|
* | |mvex|
* | | |trex|
* | | |trex|
* | | |....|
*
* Write the sequence header with both video and audio track.
*/
virtual srs_error_t write(SrsFormat* format, int v_tid, int a_tid);
private:
/**
* box->type = 'encv' or 'enca'
* |encv|
* | |sinf|
* | | |frma|
* | | |schm|
* | | |schi|
* | | | |tenc|
*/
virtual srs_error_t config_sample_description_encryption(SrsMp4SampleEntry* box);
};
// A fMP4 encoder, to cache segments then flush to disk, because the fMP4 should write
// trun box before mdat.
// TODO: fmp4 support package more than one tracks.
class SrsMp4M2tsSegmentEncoder
{
private:
@ -2163,6 +2570,54 @@ public:
virtual srs_error_t flush(uint64_t& dts);
};
// A fMP4 encoder, to cache segments then flush to disk, because the fMP4 should write
// trun box before mdat.
// TODO: fmp4 support package more than one tracks.
class SrsFmp4SegmentEncoder
{
private:
ISrsWriter* writer_;
uint32_t sequence_number_;
// TODO: audio, video may have different basetime.
srs_utime_t decode_basetime_;
uint32_t audio_track_id_;
uint32_t video_track_id_;
private:
uint32_t nb_audios_;
uint32_t nb_videos_;
uint32_t styp_bytes_;
uint64_t mdat_audio_bytes_;
uint64_t mdat_video_bytes_;
SrsMp4SampleManager* audio_samples_;
SrsMp4SampleManager* video_samples_;
private:
// Encryption
unsigned char* key_;
unsigned char iv_[16];
bool do_sample_encryption_;
public:
SrsFmp4SegmentEncoder();
virtual ~SrsFmp4SegmentEncoder();
public:
// Initialize the encoder with a writer w.
virtual srs_error_t initialize(ISrsWriter* w, uint32_t sequence, srs_utime_t basetime, uint32_t v_tid, uint32_t a_tid);
// config cipher
virtual srs_error_t config_cipher(unsigned char* key, unsigned char* iv);
// Cache a sample.
// @param ht, The sample handler type, audio/soun or video/vide.
// @param ft, The frame type. For video, it's SrsVideoAvcFrameType.
// @param dts The output dts in milliseconds.
// @param pts The output pts in milliseconds.
// @param sample The output payload, user must free it.
// @param nb_sample The output size of payload.
// @remark All samples are RAW AAC/AVC data, because sequence header is writen to init.mp4.
virtual srs_error_t write_sample(SrsMp4HandlerType ht, uint16_t ft,
uint32_t dts, uint32_t pts, uint8_t* sample, uint32_t nb_sample);
// Flush the encoder, to write the moof and mdat.
virtual srs_error_t flush(uint64_t dts);
};
// LCOV_EXCL_START
/////////////////////////////////////////////////////////////////////////////////
// MP4 dumps functions.

View File

@ -391,6 +391,7 @@ srs_error_t SrsHttpFileServer::serve_http(ISrsHttpResponseWriter* w, ISrsHttpMes
string upath = r->path();
string fullpath = srs_http_fs_fullpath(dir, entry->pattern, upath);
string basename = srs_path_basename(upath);
// stat current dir, if exists, return error.
if (!_srs_path_exists(fullpath)) {
@ -400,18 +401,18 @@ srs_error_t SrsHttpFileServer::serve_http(ISrsHttpResponseWriter* w, ISrsHttpMes
}
srs_trace("http match file=%s, pattern=%s, upath=%s",
fullpath.c_str(), entry->pattern.c_str(), upath.c_str());
// handle file according to its extension.
// use vod stream for .flv/.fhv
if (srs_string_ends_with(fullpath, ".flv") || srs_string_ends_with(fullpath, ".fhv")) {
if (srs_string_ends_with(upath, ".flv", ".fhv")) {
return serve_flv_file(w, r, fullpath);
} else if (srs_string_ends_with(fullpath, ".mp4")) {
return serve_mp4_file(w, r, fullpath);
} else if (srs_string_ends_with(upath, ".m3u8")) {
return serve_m3u8_file(w, r, fullpath);
} else if (srs_string_ends_with(upath, ".ts")) {
return serve_ts_file(w, r, fullpath);
}
return serve_m3u8_ctx(w, r, fullpath);
} else if (srs_string_ends_with(upath, ".ts", ".m4s") || basename == "init.mp4") {
return serve_ts_ctx(w, r, fullpath);
} else if (srs_string_ends_with(upath, ".mp4")) {
return serve_mp4_file(w, r, fullpath);
}
// serve common static file.
return serve_file(w, r, fullpath);
@ -553,16 +554,6 @@ srs_error_t SrsHttpFileServer::serve_mp4_file(ISrsHttpResponseWriter* w, ISrsHtt
return serve_mp4_stream(w, r, fullpath, start, end);
}
srs_error_t SrsHttpFileServer::serve_m3u8_file(ISrsHttpResponseWriter * w, ISrsHttpMessage * r, std::string fullpath)
{
return serve_m3u8_ctx(w, r, fullpath);
}
srs_error_t SrsHttpFileServer::serve_ts_file(ISrsHttpResponseWriter * w, ISrsHttpMessage * r, std::string fullpath)
{
return serve_ts_ctx(w, r, fullpath);
}
srs_error_t SrsHttpFileServer::serve_flv_stream(ISrsHttpResponseWriter* w, ISrsHttpMessage* r, string fullpath, int64_t offset)
{
// @remark For common http file server, we don't support stream request, please use SrsVodStream instead.

View File

@ -351,8 +351,6 @@ private:
virtual srs_error_t serve_file(ISrsHttpResponseWriter* w, ISrsHttpMessage* r, std::string fullpath);
virtual srs_error_t serve_flv_file(ISrsHttpResponseWriter* w, ISrsHttpMessage* r, std::string fullpath);
virtual srs_error_t serve_mp4_file(ISrsHttpResponseWriter* w, ISrsHttpMessage* r, std::string fullpath);
virtual srs_error_t serve_m3u8_file(ISrsHttpResponseWriter* w, ISrsHttpMessage* r, std::string fullpath);
virtual srs_error_t serve_ts_file(ISrsHttpResponseWriter* w, ISrsHttpMessage* r, std::string fullpath);
protected:
// When access flv file with x.flv?start=xxx
virtual srs_error_t serve_flv_stream(ISrsHttpResponseWriter* w, ISrsHttpMessage* r, std::string fullpath, int64_t offset);
@ -371,6 +369,7 @@ protected:
// Remark 2:
// If use two same "hls_ctx" in different requests, SRS cannot detect so that they will be treated as one.
virtual srs_error_t serve_m3u8_ctx(ISrsHttpResponseWriter* w, ISrsHttpMessage* r, std::string fullpath);
// the ts file including: .ts .m4s init.mp4
virtual srs_error_t serve_ts_ctx(ISrsHttpResponseWriter* w, ISrsHttpMessage* r, std::string fullpath);
protected:
// Copy the fs to response writer in size bytes.

View File

@ -3732,12 +3732,15 @@ VOID TEST(ConfigMainTest, CheckVhostConfig5)
if (true) {
MockSrsConfig conf;
HELPER_ASSERT_SUCCESS(conf.parse(_MIN_OK_CONF "vhost ossrs.net{hls{hls_keys on;hls_fragments_per_key 5;hls_key_file xxx;hls_key_file_path xxx2;hls_key_url xxx3;}}"));
HELPER_ASSERT_SUCCESS(conf.parse(_MIN_OK_CONF "vhost ossrs.net{hls{hls_keys on;hls_fragments_per_key 5;hls_key_file xxx;hls_key_file_path xxx2;hls_key_url xxx3;hls_use_fmp4 on;hls_fmp4_file xx.m4s;hls_init_file yy-init.mp4;}}"));
EXPECT_TRUE(conf.get_hls_keys("ossrs.net"));
EXPECT_EQ(5, conf.get_hls_fragments_per_key("ossrs.net"));
EXPECT_STREQ("xxx", conf.get_hls_key_file("ossrs.net").c_str());
EXPECT_STREQ("xxx2", conf.get_hls_key_file_path("ossrs.net").c_str());
EXPECT_STREQ("xxx3", conf.get_hls_key_url("ossrs.net").c_str());
EXPECT_TRUE(conf.get_hls_use_fmp4("ossrs.net"));
EXPECT_STREQ("xx.m4s", conf.get_hls_fmp4_file("ossrs.net").c_str());
EXPECT_STREQ("yy-init.mp4", conf.get_hls_init_file("ossrs.net").c_str());
}
if (true) {
@ -5125,6 +5128,27 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesHls)
SrsSetEnvConfig(conf, hls_dts_directly, "SRS_VHOST_HLS_HLS_DTS_DIRECTLY", "off");
EXPECT_FALSE(conf.get_vhost_hls_dts_directly("__defaultVhost__"));
SrsSetEnvConfig(conf, hls_use_fmp4_on, "SRS_VHOST_HLS_HLS_USE_FMP4", "on");
EXPECT_TRUE(conf.get_hls_use_fmp4("__defaultVhost__"));
SrsSetEnvConfig(conf, hls_use_fmp4_off, "SRS_VHOST_HLS_HLS_USE_FMP4", "off");
EXPECT_FALSE(conf.get_hls_use_fmp4("__defaultVhost__"));
SrsSetEnvConfig(conf, hls_use_fmp4_unexpected, "SRS_VHOST_HLS_HLS_USE_FMP4", "xx");
EXPECT_FALSE(conf.get_hls_use_fmp4("__defaultVhost__"));
SrsSetEnvConfig(conf, hls_fmp4_file, "SRS_VHOST_HLS_HLS_FMP4_FILE", "xxx.m4s");
EXPECT_STREQ("xxx.m4s", conf.get_hls_fmp4_file("__defaultVhost__").c_str());
SrsSetEnvConfig(conf, hls_init_file, "SRS_VHOST_HLS_HLS_INIT_FILE", "yyy-init.mp4");
EXPECT_STREQ("yyy-init.mp4", conf.get_hls_init_file("__defaultVhost__").c_str());
}
// Test default value for hls_init_file with a fresh config
{
MockSrsConfig conf;
EXPECT_STREQ("[app]/[stream]/init.mp4", conf.get_hls_init_file("__defaultVhost__").c_str());
}
}

View File

@ -0,0 +1,810 @@
//
// Copyright (c) 2013-2025 The SRS Authors
//
// SPDX-License-Identifier: MIT
//
#include <srs_utest_fmp4.hpp>
#include <sstream>
using namespace std;
#include <srs_utest_kernel.hpp>
#include <srs_kernel_error.hpp>
#include <srs_kernel_mp4.hpp>
#include <srs_core_autofree.hpp>
#include <srs_app_hls.hpp>
#include <srs_kernel_stream.hpp>
#include <srs_kernel_utility.hpp>
#include <srs_protocol_rtmp_stack.hpp>
// Mock classes for testing
class MockSrsRequest : public SrsRequest
{
public:
MockSrsRequest() {
vhost = "__defaultVhost__";
app = "live";
stream = "livestream";
}
virtual ~MockSrsRequest() {}
};
class MockSrsFormat : public SrsFormat
{
public:
MockSrsFormat() {
initialize();
// Setup video sequence header (H.264 AVC)
uint8_t video_raw[] = {
0x17,
0x00, 0x00, 0x00, 0x00, 0x01, 0x64, 0x00, 0x20, 0xff, 0xe1, 0x00, 0x19, 0x67, 0x64, 0x00, 0x20,
0xac, 0xd9, 0x40, 0xc0, 0x29, 0xb0, 0x11, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x03, 0x00,
0x32, 0x0f, 0x18, 0x31, 0x96, 0x01, 0x00, 0x05, 0x68, 0xeb, 0xec, 0xb2, 0x2c
};
on_video(0, (char*)video_raw, sizeof(video_raw));
// Setup audio sequence header (AAC)
uint8_t audio_raw[] = {
0xaf, 0x00, 0x12, 0x10
};
on_audio(0, (char*)audio_raw, sizeof(audio_raw));
}
virtual ~MockSrsFormat() {}
};
class MockSrsSharedPtrMessage : public SrsSharedPtrMessage
{
public:
MockSrsSharedPtrMessage(bool is_video_msg, uint32_t ts) {
timestamp = ts;
// Create sample payload
char* payload = new char[1024];
memset(payload, 0x00, 1024);
SrsSharedPtrMessage::wrap(payload, 1024);
if (is_video_msg) {
ptr->header.message_type = RTMP_MSG_VideoMessage;
} else {
ptr->header.message_type = RTMP_MSG_AudioMessage;
}
}
virtual ~MockSrsSharedPtrMessage() {}
};
VOID TEST(Fmp4Test, SrsInitMp4Segment_VideoOnly)
{
srs_error_t err;
MockSrsFileWriter fw;
SrsInitMp4Segment segment(&fw);
segment.set_path("/tmp/init_video.mp4");
MockSrsFormat fmt;
HELPER_ASSERT_SUCCESS(segment.write_video_only(&fmt, 1));
EXPECT_TRUE(fw.filesize() > 0);
// Verify the file contains expected MP4 boxes
string content = fw.str();
EXPECT_TRUE(content.find("ftyp") != string::npos);
EXPECT_TRUE(content.find("moov") != string::npos);
}
VOID TEST(Fmp4Test, SrsInitMp4Segment_AudioOnly)
{
srs_error_t err;
MockSrsFileWriter fw;
SrsInitMp4Segment segment(&fw);
segment.set_path("/tmp/init_audio.mp4");
MockSrsFormat fmt;
HELPER_ASSERT_SUCCESS(segment.write_audio_only(&fmt, 2));
EXPECT_TRUE(fw.filesize() > 0);
// Verify the file contains expected MP4 boxes
string content = fw.str();
EXPECT_TRUE(content.find("ftyp") != string::npos);
EXPECT_TRUE(content.find("moov") != string::npos);
}
VOID TEST(Fmp4Test, SrsInitMp4Segment_AudioVideo)
{
srs_error_t err;
MockSrsFileWriter fw;
SrsInitMp4Segment segment(&fw);
segment.set_path("/tmp/init_av.mp4");
MockSrsFormat fmt;
HELPER_ASSERT_SUCCESS(segment.write(&fmt, 1, 2));
EXPECT_TRUE(fw.filesize() > 0);
// Verify the file contains expected MP4 boxes
string content = fw.str();
EXPECT_TRUE(content.find("ftyp") != string::npos);
EXPECT_TRUE(content.find("moov") != string::npos);
}
VOID TEST(Fmp4Test, SrsInitMp4Segment_WithEncryption)
{
srs_error_t err;
MockSrsFileWriter fw;
SrsInitMp4Segment segment(&fw);
segment.set_path("/tmp/init_encrypted.mp4");
MockSrsFormat fmt;
// Configure encryption
unsigned char kid[16] = {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10};
unsigned char iv[16] = {0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20};
HELPER_ASSERT_SUCCESS(segment.config_cipher(kid, iv, 16));
HELPER_ASSERT_SUCCESS(segment.write(&fmt, 1, 2));
EXPECT_TRUE(fw.filesize() > 0);
}
VOID TEST(Fmp4Test, SrsHlsM4sSegment_Basic)
{
srs_error_t err;
MockSrsFileWriter fw;
SrsHlsM4sSegment segment(&fw);
// Initialize segment with a path that doesn't require file system operations
HELPER_ASSERT_SUCCESS(segment.initialize(0, 1, 2, 100, "segment-100.m4s"));
// Write video sample
MockSrsFormat fmt;
MockSrsSharedPtrMessage video_msg(true, 1000);
HELPER_ASSERT_SUCCESS(segment.write(&video_msg, &fmt));
// Write audio sample
MockSrsSharedPtrMessage audio_msg(false, 2000); // Different timestamp
HELPER_ASSERT_SUCCESS(segment.write(&audio_msg, &fmt));
// Test duration - should be > 0 after writing samples with different timestamps
EXPECT_GT(segment.duration(), 0);
}
VOID TEST(Fmp4Test, SrsHlsM4sSegment_WithEncryption)
{
srs_error_t err;
MockSrsFileWriter fw;
SrsHlsM4sSegment segment(&fw);
// Initialize segment
HELPER_ASSERT_SUCCESS(segment.initialize(0, 1, 2, 101, "segment-101.m4s"));
// Configure encryption
unsigned char key[16] = {0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30};
unsigned char iv[16] = {0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40};
segment.config_cipher(key, iv);
// Verify IV is stored
EXPECT_EQ(0x31, segment.iv[0]);
EXPECT_EQ(0x40, segment.iv[15]);
// Write samples with different timestamps to create duration
MockSrsFormat fmt;
MockSrsSharedPtrMessage video_msg1(true, 1000);
HELPER_ASSERT_SUCCESS(segment.write(&video_msg1, &fmt));
MockSrsSharedPtrMessage video_msg2(true, 2000);
HELPER_ASSERT_SUCCESS(segment.write(&video_msg2, &fmt));
// Test that segment has content
EXPECT_GT(segment.duration(), 0);
}
VOID TEST(Fmp4Test, SrsFmp4SegmentEncoder_Basic)
{
srs_error_t err;
MockSrsFileWriter fw;
SrsFmp4SegmentEncoder encoder;
// Initialize encoder
HELPER_ASSERT_SUCCESS(encoder.initialize(&fw, 0, 0, 1, 2));
// Write video sample
uint8_t video_sample[] = {0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x20};
HELPER_ASSERT_SUCCESS(encoder.write_sample(SrsMp4HandlerTypeVIDE, SrsVideoAvcFrameTypeKeyFrame,
1000, 1000, video_sample, sizeof(video_sample)));
// Write audio sample
uint8_t audio_sample[] = {0xff, 0xf1, 0x50, 0x80, 0x01, 0x3f, 0xfc};
HELPER_ASSERT_SUCCESS(encoder.write_sample(SrsMp4HandlerTypeSOUN, 0x00,
1000, 1000, audio_sample, sizeof(audio_sample)));
// Flush to file
HELPER_ASSERT_SUCCESS(encoder.flush(2000));
EXPECT_TRUE(fw.filesize() > 0);
// Verify basic structure (content may be binary, so just check size)
EXPECT_GT(fw.filesize(), 100); // Should have reasonable size for fMP4 structure
}
VOID TEST(Fmp4Test, SrsFmp4SegmentEncoder_WithEncryption)
{
srs_error_t err;
MockSrsFileWriter fw;
SrsFmp4SegmentEncoder encoder;
// Initialize encoder
HELPER_ASSERT_SUCCESS(encoder.initialize(&fw, 0, 0, 1, 2));
// Configure encryption
unsigned char key[16] = {0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50};
unsigned char iv[16] = {0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60};
encoder.config_cipher(key, iv);
// Write encrypted samples
uint8_t video_sample[] = {0x00, 0x00, 0x00, 0x01, 0x67, 0x64, 0x00, 0x20};
HELPER_ASSERT_SUCCESS(encoder.write_sample(SrsMp4HandlerTypeVIDE, SrsVideoAvcFrameTypeKeyFrame,
1000, 1000, video_sample, sizeof(video_sample)));
HELPER_ASSERT_SUCCESS(encoder.flush(2000));
EXPECT_TRUE(fw.filesize() > 0);
// Verify encryption boxes are present
string content = fw.str();
EXPECT_TRUE(content.find("senc") != string::npos); // Sample Encryption Box
}
VOID TEST(Fmp4Test, SrsHlsFmp4Muxer_Basic)
{
srs_error_t err;
SrsHlsFmp4Muxer muxer;
// Initialize muxer
HELPER_ASSERT_SUCCESS(muxer.initialize(1, 2));
// Test basic properties
EXPECT_EQ(0, muxer.sequence_no());
EXPECT_EQ(0, (int)muxer.duration());
EXPECT_EQ(0, muxer.deviation());
// Test codec management
muxer.set_latest_acodec(SrsAudioCodecIdAAC);
muxer.set_latest_vcodec(SrsVideoCodecIdAVC);
EXPECT_EQ(SrsAudioCodecIdAAC, muxer.latest_acodec());
EXPECT_EQ(SrsVideoCodecIdAVC, muxer.latest_vcodec());
muxer.dispose();
}
VOID TEST(Fmp4Test, SrsHlsFmp4Muxer_WriteInitMp4)
{
srs_error_t err;
SrsHlsFmp4Muxer muxer;
HELPER_ASSERT_SUCCESS(muxer.initialize(1, 2));
MockSrsRequest req;
HELPER_ASSERT_SUCCESS(muxer.on_publish(&req));
HELPER_ASSERT_SUCCESS(muxer.update_config(&req));
// Write init.mp4 with both audio and video
MockSrsFormat fmt;
HELPER_ASSERT_SUCCESS(muxer.write_init_mp4(&fmt, true, true));
// Write init.mp4 with video only
HELPER_ASSERT_SUCCESS(muxer.write_init_mp4(&fmt, true, false));
// Write init.mp4 with audio only
HELPER_ASSERT_SUCCESS(muxer.write_init_mp4(&fmt, false, true));
muxer.dispose();
}
VOID TEST(Fmp4Test, SrsHlsFmp4Muxer_WriteMedia)
{
srs_error_t err;
SrsHlsFmp4Muxer muxer;
HELPER_ASSERT_SUCCESS(muxer.initialize(1, 2));
MockSrsRequest req;
HELPER_ASSERT_SUCCESS(muxer.on_publish(&req));
HELPER_ASSERT_SUCCESS(muxer.update_config(&req));
// Write init.mp4 first
MockSrsFormat fmt;
HELPER_ASSERT_SUCCESS(muxer.write_init_mp4(&fmt, true, true));
// Write video samples
MockSrsSharedPtrMessage video_msg(true, 1000);
HELPER_ASSERT_SUCCESS(muxer.write_video(&video_msg, &fmt));
// Write audio samples
MockSrsSharedPtrMessage audio_msg(false, 1000);
HELPER_ASSERT_SUCCESS(muxer.write_audio(&audio_msg, &fmt));
// Write more samples with time progression to accumulate duration
for (int i = 1; i <= 5; i++) {
MockSrsSharedPtrMessage video_msg2(true, 1000 + i * 1000); // 1 second increments
HELPER_ASSERT_SUCCESS(muxer.write_video(&video_msg2, &fmt));
MockSrsSharedPtrMessage audio_msg2(false, 1000 + i * 1000);
HELPER_ASSERT_SUCCESS(muxer.write_audio(&audio_msg2, &fmt));
}
// Should have accumulated duration from timestamp progression
EXPECT_GT(muxer.duration(), 0);
muxer.dispose();
}
VOID TEST(Fmp4Test, SrsHlsMp4Controller_Basic)
{
srs_error_t err;
SrsHlsMp4Controller controller;
// Initialize controller
HELPER_ASSERT_SUCCESS(controller.initialize());
// Test basic properties
EXPECT_EQ(0, controller.sequence_no());
EXPECT_EQ(0, (int)controller.duration());
// Test URL generation (should return .m4s URL)
string url = controller.ts_url();
EXPECT_TRUE(url.find(".m4s") != string::npos || url.empty());
controller.dispose();
}
VOID TEST(Fmp4Test, SrsHlsMp4Controller_PublishWorkflow)
{
srs_error_t err;
SrsHlsMp4Controller controller;
HELPER_ASSERT_SUCCESS(controller.initialize());
// Publish stream
MockSrsRequest req;
HELPER_ASSERT_SUCCESS(controller.on_publish(&req));
// Handle sequence headers
MockSrsFormat fmt;
MockSrsSharedPtrMessage video_sh(true, 0);
HELPER_ASSERT_SUCCESS(controller.on_sequence_header(&video_sh, &fmt));
MockSrsSharedPtrMessage audio_sh(false, 0);
HELPER_ASSERT_SUCCESS(controller.on_sequence_header(&audio_sh, &fmt));
// Write media samples
MockSrsSharedPtrMessage video_msg(true, 1000);
HELPER_ASSERT_SUCCESS(controller.write_video(&video_msg, &fmt));
MockSrsSharedPtrMessage audio_msg(false, 1000);
HELPER_ASSERT_SUCCESS(controller.write_audio(&audio_msg, &fmt));
// Unpublish
HELPER_ASSERT_SUCCESS(controller.on_unpublish());
controller.dispose();
}
VOID TEST(Fmp4Test, SrsMp4TrackEncryptionBox_CBCS)
{
srs_error_t err;
SrsMp4TrackEncryptionBox tenc;
// Configure for CBCS video encryption (1:9 pattern)
tenc.version = 1;
tenc.default_crypt_byte_block = 1; // Encrypt 1 block
tenc.default_skip_byte_block = 9; // Skip 9 blocks
tenc.default_is_protected = 1;
tenc.default_per_sample_IV_size = 0; // Use constant IV
tenc.default_constant_IV_size = 16;
// Set Key ID
unsigned char kid[16] = {0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70};
memcpy(tenc.default_KID, kid, 16);
// Set constant IV
unsigned char iv[16] = {0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,
0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80};
tenc.set_default_constant_IV(iv, 16);
// Verify configuration
EXPECT_EQ(1, tenc.version);
EXPECT_EQ(1, tenc.default_crypt_byte_block);
EXPECT_EQ(9, tenc.default_skip_byte_block);
EXPECT_EQ(1, tenc.default_is_protected);
EXPECT_EQ(0, tenc.default_per_sample_IV_size);
EXPECT_EQ(16, tenc.default_constant_IV_size);
EXPECT_EQ(0x61, tenc.default_KID[0]);
EXPECT_EQ(0x70, tenc.default_KID[15]);
EXPECT_EQ(0x71, tenc.default_constant_IV[0]);
EXPECT_EQ(0x80, tenc.default_constant_IV[15]);
// Test encoding/decoding
char buffer_data[1024];
SrsBuffer buf(buffer_data, 1024);
HELPER_ASSERT_SUCCESS(tenc.encode(&buf));
EXPECT_TRUE(buf.pos() > 0);
// Test dumps - just verify it doesn't crash and produces some output
stringstream ss;
SrsMp4DumpContext dc;
tenc.dumps_detail(ss, dc);
string detail = ss.str();
EXPECT_FALSE(detail.empty()); // Should produce some output
}
VOID TEST(Fmp4Test, SrsMp4TrackEncryptionBox_AudioFullSample)
{
SrsMp4TrackEncryptionBox tenc;
// Configure for audio full-sample encryption
tenc.version = 1;
tenc.default_crypt_byte_block = 0; // No pattern (full encryption)
tenc.default_skip_byte_block = 0; // No skip
tenc.default_is_protected = 1;
tenc.default_per_sample_IV_size = 0; // Use constant IV
tenc.default_constant_IV_size = 16;
// Verify audio encryption configuration
EXPECT_EQ(0, tenc.default_crypt_byte_block);
EXPECT_EQ(0, tenc.default_skip_byte_block);
EXPECT_EQ(1, tenc.default_is_protected);
}
VOID TEST(Fmp4Test, SrsMp4SampleEncryptionBox_Basic)
{
srs_error_t err;
SrsMp4SampleEncryptionBox senc(16); // 16-byte IV size
// Test basic properties - flags may be set by constructor
EXPECT_EQ(0, senc.version);
EXPECT_EQ(0, (int)senc.entries.size());
// Test encoding empty box
char buffer_data[1024];
SrsBuffer buf(buffer_data, 1024);
HELPER_ASSERT_SUCCESS(senc.encode(&buf));
EXPECT_TRUE(buf.pos() > 0);
// Test dumps
stringstream ss;
SrsMp4DumpContext dc;
senc.dumps_detail(ss, dc);
string detail = ss.str();
EXPECT_TRUE(detail.find("sample_count=0") != string::npos);
}
VOID TEST(Fmp4Test, SrsMp4SampleAuxiliaryInfoSizeBox_Basic)
{
srs_error_t err;
SrsMp4SampleAuxiliaryInfoSizeBox saiz;
// Configure SAIZ box
saiz.version = 0;
saiz.flags = 0;
saiz.default_sample_info_size = 16; // 16-byte IV
saiz.sample_count = 0;
// Test encoding
char buffer_data[1024];
SrsBuffer buf(buffer_data, 1024);
HELPER_ASSERT_SUCCESS(saiz.encode(&buf));
EXPECT_TRUE(buf.pos() > 0);
// Test dumps
stringstream ss;
SrsMp4DumpContext dc;
saiz.dumps_detail(ss, dc);
string detail = ss.str();
EXPECT_TRUE(detail.find("sample_count=0") != string::npos);
}
VOID TEST(Fmp4Test, SrsMp4SampleAuxiliaryInfoOffsetBox_Basic)
{
srs_error_t err;
SrsMp4SampleAuxiliaryInfoOffsetBox saio;
// Configure SAIO box
saio.version = 0;
saio.flags = 0;
saio.offsets.push_back(100); // Offset to SENC box
// Test encoding
char buffer_data[1024];
SrsBuffer buf(buffer_data, 1024);
HELPER_ASSERT_SUCCESS(saio.encode(&buf));
EXPECT_TRUE(buf.pos() > 0);
// Test dumps
stringstream ss;
SrsMp4DumpContext dc;
saio.dumps_detail(ss, dc);
string detail = ss.str();
EXPECT_TRUE(detail.find("entry_count=1") != string::npos);
}
VOID TEST(Fmp4Test, SrsMp4OriginalFormatBox_Basic)
{
srs_error_t err;
SrsMp4OriginalFormatBox frma(SrsMp4BoxTypeAVC1);
// Test encoding
char buffer_data[1024];
SrsBuffer buf(buffer_data, 1024);
HELPER_ASSERT_SUCCESS(frma.encode(&buf));
EXPECT_TRUE(buf.pos() > 0);
// Test dumps - just verify it produces output
stringstream ss;
SrsMp4DumpContext dc;
frma.dumps_detail(ss, dc);
string detail = ss.str();
EXPECT_FALSE(detail.empty());
}
VOID TEST(Fmp4Test, Integration_FullEncryptionWorkflow)
{
srs_error_t err;
// Test complete encryption workflow from init.mp4 to encrypted segments
// 1. Create encrypted init.mp4
MockSrsFileWriter init_fw;
SrsInitMp4Segment init_segment(&init_fw);
init_segment.set_path("encrypted_init.mp4");
unsigned char kid[16] = {0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88,
0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90};
unsigned char iv[16] = {0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,
0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xa0};
HELPER_ASSERT_SUCCESS(init_segment.config_cipher(kid, iv, 16));
MockSrsFormat fmt;
HELPER_ASSERT_SUCCESS(init_segment.write(&fmt, 1, 2));
EXPECT_TRUE(init_fw.filesize() > 0);
// 2. Create encrypted segment
MockSrsFileWriter seg_fw;
SrsHlsM4sSegment m4s_segment(&seg_fw);
HELPER_ASSERT_SUCCESS(m4s_segment.initialize(0, 1, 2, 200, "encrypted-200.m4s"));
unsigned char seg_key[16] = {0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8,
0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0};
unsigned char seg_iv[16] = {0xb1, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8,
0xb9, 0xba, 0xbb, 0xbc, 0xbd, 0xbe, 0xbf, 0xc0};
m4s_segment.config_cipher(seg_key, seg_iv);
// Write samples to encrypted segment with time progression
MockSrsSharedPtrMessage video_msg1(true, 2000);
HELPER_ASSERT_SUCCESS(m4s_segment.write(&video_msg1, &fmt));
MockSrsSharedPtrMessage audio_msg1(false, 2500);
HELPER_ASSERT_SUCCESS(m4s_segment.write(&audio_msg1, &fmt));
MockSrsSharedPtrMessage video_msg2(true, 3000);
HELPER_ASSERT_SUCCESS(m4s_segment.write(&video_msg2, &fmt));
// Should have duration from timestamp progression
EXPECT_GT(m4s_segment.duration(), 0);
// 3. Verify both files have content (encryption metadata is binary)
EXPECT_TRUE(init_fw.filesize() > 0);
EXPECT_GT(m4s_segment.duration(), 0);
}
VOID TEST(Fmp4Test, EdgeCase_LargeTimestamp)
{
srs_error_t err;
MockSrsFileWriter fw;
SrsFmp4SegmentEncoder encoder;
// Test with large timestamp values
HELPER_ASSERT_SUCCESS(encoder.initialize(&fw, 0, 0, 1, 2));
uint64_t large_timestamp = 0xFFFFFFFF; // Max 32-bit value
uint8_t sample[] = {0x00, 0x01, 0x02, 0x03};
HELPER_ASSERT_SUCCESS(encoder.write_sample(SrsMp4HandlerTypeVIDE, SrsVideoAvcFrameTypeKeyFrame,
large_timestamp, large_timestamp, sample, sizeof(sample)));
HELPER_ASSERT_SUCCESS(encoder.flush(large_timestamp + 1000));
EXPECT_TRUE(fw.filesize() > 0);
}
VOID TEST(Fmp4Test, ErrorHandling_InvalidTrackId)
{
srs_error_t err;
MockSrsFileWriter fw;
SrsFmp4SegmentEncoder encoder;
// Test with track ID 0 (may or may not be invalid depending on implementation)
err = encoder.initialize(&fw, 0, 0, 0, 0);
// Just verify the function can be called without crashing
if (err != srs_success) {
srs_freep(err);
}
}
VOID TEST(Fmp4Test, ErrorHandling_NullWriter)
{
srs_error_t err;
SrsFmp4SegmentEncoder encoder;
// Initialize with null writer - may or may not fail depending on implementation
err = encoder.initialize(NULL, 0, 0, 1, 2);
if (err != srs_success) {
srs_freep(err);
}
// Test passes if no crash occurs
}
VOID TEST(Fmp4Test, ErrorHandling_WriteBeforeInitialize)
{
srs_error_t err;
SrsFmp4SegmentEncoder encoder;
uint8_t sample[] = {0x00, 0x01};
// Try to write sample before initialization - may or may not fail
err = encoder.write_sample(SrsMp4HandlerTypeVIDE, SrsVideoAvcFrameTypeKeyFrame,
1000, 1000, sample, sizeof(sample));
if (err != srs_success) {
srs_freep(err);
}
// Test passes if no crash occurs
}
VOID TEST(Fmp4Test, ErrorHandling_FlushBeforeWrite)
{
srs_error_t err;
MockSrsFileWriter fw;
SrsFmp4SegmentEncoder encoder;
HELPER_ASSERT_SUCCESS(encoder.initialize(&fw, 0, 0, 1, 2));
// Flush without writing any samples - may fail due to empty samples
err = encoder.flush(1000);
if (err != srs_success) {
srs_freep(err);
// This is expected behavior for empty flush
}
}
VOID TEST(Fmp4Test, Configuration_TrackIdManagement)
{
srs_error_t err;
SrsHlsMp4Controller controller;
HELPER_ASSERT_SUCCESS(controller.initialize());
// Verify default track IDs
EXPECT_EQ(1, controller.video_track_id_);
EXPECT_EQ(2, controller.audio_track_id_);
// Test sequence header tracking
EXPECT_FALSE(controller.has_video_sh_);
EXPECT_FALSE(controller.has_audio_sh_);
// Set request first
MockSrsRequest req;
HELPER_ASSERT_SUCCESS(controller.on_publish(&req));
MockSrsFormat fmt;
MockSrsSharedPtrMessage video_sh(true, 0);
HELPER_ASSERT_SUCCESS(controller.on_sequence_header(&video_sh, &fmt));
EXPECT_TRUE(controller.has_video_sh_);
MockSrsSharedPtrMessage audio_sh(false, 0);
HELPER_ASSERT_SUCCESS(controller.on_sequence_header(&audio_sh, &fmt));
EXPECT_TRUE(controller.has_audio_sh_);
controller.dispose();
}
VOID TEST(Fmp4Test, Configuration_SequenceHeaderValidation)
{
srs_error_t err;
SrsHlsMp4Controller controller;
HELPER_ASSERT_SUCCESS(controller.initialize());
// Test sequence header without request (should fail)
MockSrsFormat fmt;
MockSrsSharedPtrMessage video_sh(true, 0);
HELPER_EXPECT_FAILED(controller.on_sequence_header(&video_sh, &fmt));
// Set request and try again
MockSrsRequest req;
HELPER_ASSERT_SUCCESS(controller.on_publish(&req));
HELPER_ASSERT_SUCCESS(controller.on_sequence_header(&video_sh, &fmt));
controller.dispose();
}
VOID TEST(Fmp4Test, Performance_MultipleSegments)
{
srs_error_t err;
SrsHlsFmp4Muxer muxer;
HELPER_ASSERT_SUCCESS(muxer.initialize(1, 2));
MockSrsRequest req;
HELPER_ASSERT_SUCCESS(muxer.on_publish(&req));
HELPER_ASSERT_SUCCESS(muxer.update_config(&req));
MockSrsFormat fmt;
HELPER_ASSERT_SUCCESS(muxer.write_init_mp4(&fmt, true, true));
int initial_seq = muxer.sequence_no();
// Write many samples to create multiple segments
for (int i = 0; i < 500; i++) {
MockSrsSharedPtrMessage video_msg(true, i * 40);
HELPER_ASSERT_SUCCESS(muxer.write_video(&video_msg, &fmt));
if (i % 2 == 0) { // Write audio less frequently
MockSrsSharedPtrMessage audio_msg(false, i * 40);
HELPER_ASSERT_SUCCESS(muxer.write_audio(&audio_msg, &fmt));
}
}
// Should have created multiple segments
EXPECT_GT(muxer.sequence_no(), initial_seq);
EXPECT_GT(muxer.duration(), 0);
muxer.dispose();
}
VOID TEST(Fmp4Test, Compatibility_SequenceHeaderIgnore)
{
srs_error_t err;
SrsHlsMp4Controller controller;
HELPER_ASSERT_SUCCESS(controller.initialize());
MockSrsRequest req;
HELPER_ASSERT_SUCCESS(controller.on_publish(&req));
MockSrsFormat fmt;
// Create audio sequence header message
MockSrsSharedPtrMessage audio_sh(false, 0);
// Should ignore sequence headers in write_audio
HELPER_ASSERT_SUCCESS(controller.write_audio(&audio_sh, &fmt));
// Regular audio message should be processed
MockSrsSharedPtrMessage audio_msg(false, 1000);
HELPER_ASSERT_SUCCESS(controller.write_audio(&audio_msg, &fmt));
controller.dispose();
}

View File

@ -0,0 +1,15 @@
//
// Copyright (c) 2013-2025 The SRS Authors
//
// SPDX-License-Identifier: MIT
//
#ifndef SRS_UTEST_FMP4_HPP
#define SRS_UTEST_FMP4_HPP
/*
#include <srs_utest_fmp4.hpp>
*/
#include <srs_utest.hpp>
#endif

View File

@ -1569,6 +1569,50 @@ VOID TEST(ProtocolHTTPTest, VodStreamHandlers)
HELPER_ASSERT_SUCCESS(h.serve_http(&w2, &r));
__MOCK_HTTP_EXPECT_STREQ(200, "livestream-13.ts?hls_ctx=123456", w2);
}
// fmp4 .m4s with hls_ctx test
if (true) {
SrsHttpMuxEntry e;
e.pattern = "/";
SrsVodStream h("/tmp");
h.set_fs_factory(new MockFileReaderFactory("livestream-13.m4s"));
h.set_path_check(_mock_srs_path_always_exists);
h.entry = &e;
MockResponseWriter w;
SrsHttpMessage r(NULL, NULL);
HELPER_ASSERT_SUCCESS(r.set_url("/index.m3u8?hls_ctx=123456", false));
HELPER_ASSERT_SUCCESS(h.serve_http(&w, &r));
__MOCK_HTTP_EXPECT_STREQ4(200, "/index.m3u8?hls_ctx=123456\n", w);
MockResponseWriter w2;
HELPER_ASSERT_SUCCESS(h.serve_http(&w2, &r));
__MOCK_HTTP_EXPECT_STREQ(200, "livestream-13.m4s?hls_ctx=123456", w2);
}
// fmp4 init.mp4 with hls_ctx test
if (true) {
SrsHttpMuxEntry e;
e.pattern = "/";
SrsVodStream h("/tmp");
h.set_fs_factory(new MockFileReaderFactory("init.mp4"));
h.set_path_check(_mock_srs_path_always_exists);
h.entry = &e;
MockResponseWriter w;
SrsHttpMessage r(NULL, NULL);
HELPER_ASSERT_SUCCESS(r.set_url("/index.m3u8?hls_ctx=123456", false));
HELPER_ASSERT_SUCCESS(h.serve_http(&w, &r));
__MOCK_HTTP_EXPECT_STREQ4(200, "/index.m3u8?hls_ctx=123456\n", w);
MockResponseWriter w2;
HELPER_ASSERT_SUCCESS(h.serve_http(&w2, &r));
__MOCK_HTTP_EXPECT_STREQ(200, "init.mp4?hls_ctx=123456", w2);
}
}
VOID TEST(ProtocolHTTPTest, BasicHandlers)

View File

@ -12,6 +12,7 @@ using namespace std;
#include <srs_kernel_error.hpp>
#include <srs_kernel_mp4.hpp>
#include <srs_core_autofree.hpp>
#include <srs_app_hls.hpp>
VOID TEST(KernelMp4Test, PrintPadding)
{
@ -535,6 +536,21 @@ VOID TEST(KernelMp4Test, FullBoxDump)
}
}
VOID TEST(KernelMp4Test, MOOFBox)
{
if (true) {
SrsMp4MovieFragmentBox box;
SrsMp4MovieFragmentHeaderBox *mfhd = new SrsMp4MovieFragmentHeaderBox();
box.set_mfhd(mfhd);
EXPECT_EQ(box.mfhd(), mfhd);
SrsMp4TrackFragmentBox* traf = new SrsMp4TrackFragmentBox();
box.add_traf(traf);
EXPECT_TRUE(traf == box.get(SrsMp4BoxTypeTRAF));
}
}
VOID TEST(KernelMp4Test, MFHDBox)
{
srs_error_t err;
@ -898,11 +914,10 @@ VOID TEST(KernelMp4Test, TREXBox)
}
SrsMp4MovieExtendsBox box;
EXPECT_TRUE(NULL == box.trex());
SrsMp4TrackExtendsBox* trex = new SrsMp4TrackExtendsBox();
box.set_trex(trex);
EXPECT_TRUE(trex == box.trex());
box.add_trex(trex);
EXPECT_TRUE(trex == box.get(SrsMp4BoxTypeTREX));
}
VOID TEST(KernelMp4Test, TKHDBox)
@ -1823,6 +1838,442 @@ VOID TEST(KernelMp4Test, STSDBox)
}
}
VOID TEST(KernelMp4Test, SAIZBox)
{
srs_error_t err;
// flags & 1 == 0; default_sample_info_size == 1
if (true) {
SrsMp4SampleAuxiliaryInfoSizeBox saiz;
uint8_t data[12+5];
SrsBuffer b((char*)data, sizeof(data));
b.write_4bytes(16); b.write_4bytes(SrsMp4BoxTypeSAIZ); b.write_1bytes(0); b.write_3bytes(0);
b.write_1bytes(1); b.write_4bytes(0); b.skip(-17);
HELPER_ASSERT_SUCCESS(saiz.decode(&b));
EXPECT_EQ(17, (int)saiz.nb_header());
EXPECT_EQ(0, (int)saiz.version);
EXPECT_EQ(0, (int)saiz.flags);
EXPECT_EQ(1, (int)saiz.default_sample_info_size);
EXPECT_EQ(0, (int)saiz.sample_count);
EXPECT_EQ(0, saiz.sample_info_sizes.size());
}
// flags & 1 == 1; default_sample_info_size == 1
if (true) {
SrsMp4SampleAuxiliaryInfoSizeBox saiz;
uint8_t data[12+13];
SrsBuffer b((char*)data, sizeof(data));
b.write_4bytes(16); b.write_4bytes(SrsMp4BoxTypeSAIZ); b.write_1bytes(0); b.write_3bytes(1);
b.write_4bytes(1); b.write_4bytes(2);
b.write_1bytes(1); b.write_4bytes(0); b.skip(-25);
HELPER_ASSERT_SUCCESS(saiz.decode(&b));
EXPECT_EQ(25, (int)saiz.nb_header());
EXPECT_EQ(0, (int)saiz.version);
EXPECT_EQ(1, (int)saiz.flags);
EXPECT_EQ(1, (int)saiz.aux_info_type);
EXPECT_EQ(2, (int)saiz.aux_info_type_parameter);
EXPECT_EQ(1, (int)saiz.default_sample_info_size);
EXPECT_EQ(0, (int)saiz.sample_count);
EXPECT_EQ(0, saiz.sample_info_sizes.size());
}
// flags & 1 == 1; default_sample_info_size == 0; sample_count = 3;
if (true) {
SrsMp4SampleAuxiliaryInfoSizeBox saiz;
uint8_t data[12+16];
SrsBuffer b((char*)data, sizeof(data));
b.write_4bytes(16); b.write_4bytes(SrsMp4BoxTypeSAIZ); b.write_1bytes(0); b.write_3bytes(1);
b.write_4bytes(1); b.write_4bytes(2);
b.write_1bytes(0); b.write_4bytes(3);
b.write_1bytes(4); b.write_1bytes(5); b.write_1bytes(6);
b.skip(-28);
HELPER_ASSERT_SUCCESS(saiz.decode(&b));
EXPECT_EQ(28, (int)saiz.nb_header());
EXPECT_EQ(0, (int)saiz.version);
EXPECT_EQ(1, (int)saiz.flags);
EXPECT_EQ(1, (int)saiz.aux_info_type);
EXPECT_EQ(2, (int)saiz.aux_info_type_parameter);
EXPECT_EQ(0, (int)saiz.default_sample_info_size);
EXPECT_EQ(3, (int)saiz.sample_count);
EXPECT_EQ(3, saiz.sample_info_sizes.size());
EXPECT_EQ(4, saiz.sample_info_sizes[0]);
EXPECT_EQ(5, saiz.sample_info_sizes[1]);
EXPECT_EQ(6, saiz.sample_info_sizes[2]);
}
if (true) {
SrsMp4SampleAuxiliaryInfoSizeBox saiz;
saiz.flags = 0;
saiz.default_sample_info_size = 1;
saiz.sample_count = 0;
EXPECT_EQ(17, saiz.nb_header());
stringstream ss;
SrsMp4DumpContext dc;
saiz.dumps_detail(ss, dc);
string v = ss.str();
EXPECT_STREQ("default_sample_info_size=1, sample_count=0", v.c_str());
}
if (true) {
SrsMp4SampleAuxiliaryInfoSizeBox saiz;
saiz.flags = 1;
saiz.default_sample_info_size = 1;
saiz.sample_count = 0;
EXPECT_EQ(25, saiz.nb_header());
stringstream ss;
SrsMp4DumpContext dc;
saiz.dumps_detail(ss, dc);
string v = ss.str();
EXPECT_STREQ("default_sample_info_size=1, sample_count=0", v.c_str());
}
if (true) {
SrsMp4SampleAuxiliaryInfoSizeBox saiz;
saiz.flags = 1;
saiz.default_sample_info_size = 0;
saiz.sample_count = 1;
saiz.sample_info_sizes.push_back(4);
EXPECT_EQ(26, saiz.nb_header());
stringstream ss;
SrsMp4DumpContext dc;
saiz.dumps_detail(ss, dc);
string v = ss.str();
EXPECT_STREQ("default_sample_info_size=0, sample_count=1", v.c_str());
}
}
VOID TEST(KernelMp4Test, SAIOBox)
{
srs_error_t err;
if (true) {
SrsMp4SampleAuxiliaryInfoOffsetBox saio;
uint8_t data[12+8];
SrsBuffer b((char*)data, sizeof(data));
b.write_4bytes(16); b.write_4bytes(SrsMp4BoxTypeSAIO); b.write_1bytes(0); b.write_3bytes(0);
b.write_4bytes(1); b.write_4bytes(2); b.skip(-20);
HELPER_ASSERT_SUCCESS(saio.decode(&b));
EXPECT_EQ(20, (int)saio.nb_header());
EXPECT_EQ(0, (int)saio.version);
EXPECT_EQ(0, (int)saio.flags);
EXPECT_EQ(1, (int)saio.offsets.size());
EXPECT_EQ(2, (int)saio.offsets[0]);
}
if (true) {
SrsMp4SampleAuxiliaryInfoOffsetBox saio;
uint8_t data[12+16];
SrsBuffer b((char*)data, sizeof(data));
b.write_4bytes(16); b.write_4bytes(SrsMp4BoxTypeSAIO); b.write_1bytes(0); b.write_3bytes(1);
b.write_4bytes(1); b.write_4bytes(2);
b.write_4bytes(1); b.write_4bytes(2); b.skip(-28);
HELPER_ASSERT_SUCCESS(saio.decode(&b));
EXPECT_EQ(28, (int)saio.nb_header());
EXPECT_EQ(0, (int)saio.version);
EXPECT_EQ(1, (int)saio.flags);
EXPECT_EQ(1, (int)saio.aux_info_type);
EXPECT_EQ(2, (int)saio.aux_info_type_parameter);
EXPECT_EQ(1, (int)saio.offsets.size());
EXPECT_EQ(2, (int)saio.offsets[0]);
}
if (true) {
SrsMp4SampleAuxiliaryInfoOffsetBox saio;
uint8_t data[12+20];
SrsBuffer b((char*)data, sizeof(data));
b.write_4bytes(16); b.write_4bytes(SrsMp4BoxTypeSAIO); b.write_1bytes(1); b.write_3bytes(1);
b.write_4bytes(1); b.write_4bytes(2);
b.write_4bytes(1); b.write_8bytes(2); b.skip(-32);
HELPER_ASSERT_SUCCESS(saio.decode(&b));
EXPECT_EQ(32, (int)saio.nb_header());
EXPECT_EQ(1, (int)saio.version);
EXPECT_EQ(1, (int)saio.flags);
EXPECT_EQ(1, (int)saio.aux_info_type);
EXPECT_EQ(2, (int)saio.aux_info_type_parameter);
EXPECT_EQ(1, (int)saio.offsets.size());
EXPECT_EQ(2, (int)saio.offsets[0]);
}
if (true) {
SrsMp4SampleAuxiliaryInfoOffsetBox saio;
saio.version = 0;
saio.flags = 0;
saio.offsets.push_back(2);
EXPECT_EQ(20, (int)saio.nb_header());
stringstream ss;
SrsMp4DumpContext dc;
saio.dumps_detail(ss, dc);
string v = ss.str();
EXPECT_STREQ("entry_count=1", v.c_str());
}
if (true) {
SrsMp4SampleAuxiliaryInfoOffsetBox saio;
saio.version = 0;
saio.flags = 1;
saio.offsets.push_back(2);
EXPECT_EQ(28, (int)saio.nb_header());
stringstream ss;
SrsMp4DumpContext dc;
saio.dumps_detail(ss, dc);
string v = ss.str();
EXPECT_STREQ("entry_count=1", v.c_str());
}
if (true) {
SrsMp4SampleAuxiliaryInfoOffsetBox saio;
saio.version = 1;
saio.flags = 1;
saio.offsets.push_back(2);
EXPECT_EQ(32, (int)saio.nb_header());
stringstream ss;
SrsMp4DumpContext dc;
saio.dumps_detail(ss, dc);
string v = ss.str();
EXPECT_STREQ("entry_count=1", v.c_str());
}
}
VOID TEST(KernelMp4Test, SENCBox)
{
srs_error_t err;
if (true) {
SrsMp4SampleEncryptionBox senc(8);
uint8_t data[12+4];
SrsBuffer b((char*)data, sizeof(data));
b.write_4bytes(16); b.write_4bytes(SrsMp4BoxTypeSENC); b.write_1bytes(0); b.write_3bytes(0);
b.write_4bytes(0); b.skip(-16);
HELPER_ASSERT_SUCCESS(senc.decode(&b));
EXPECT_EQ(16, (int)senc.nb_header());
EXPECT_EQ(0, (int)senc.version);
EXPECT_EQ(0, (int)senc.flags);
EXPECT_EQ(0, (int)senc.entries.size());
}
if (true) {
SrsMp4SampleEncryptionBox senc(8);
uint8_t data[12+12];
SrsBuffer b((char*)data, sizeof(data));
b.write_4bytes(16); b.write_4bytes(SrsMp4BoxTypeSENC); b.write_1bytes(0); b.write_3bytes(0);
b.write_4bytes(1); b.write_8bytes(1); b.skip(-24);
HELPER_ASSERT_SUCCESS(senc.decode(&b));
EXPECT_EQ(24, (int)senc.nb_header());
EXPECT_EQ(0, (int)senc.version);
EXPECT_EQ(0, (int)senc.flags);
EXPECT_EQ(1, (int)senc.entries.size());
}
}
VOID TEST(KernelMp4Test, FRMABox)
{
srs_error_t err;
if (true) {
SrsMp4OriginalFormatBox frma(1);
uint8_t data[8+4];
SrsBuffer b((char*)data, sizeof(data));
b.write_4bytes(0); b.write_4bytes(SrsMp4BoxTypeFRMA);
b.write_4bytes(1); b.skip(-12);
HELPER_ASSERT_SUCCESS(frma.decode(&b));
EXPECT_EQ(12, (int)frma.nb_header());
}
if (true) {
SrsMp4OriginalFormatBox frma(1);
EXPECT_EQ(12, (int)frma.nb_header());
stringstream ss;
SrsMp4DumpContext dc;
frma.dumps_detail(ss, dc);
string v = ss.str();
EXPECT_STREQ("original format=1\n", v.c_str());
}
}
VOID TEST(KernelMp4Test, SCHMBox) {
srs_error_t err;
if (true) {
SrsMp4SchemeTypeBox schm;
uint8_t data[12+8];
SrsBuffer b((char*)data, sizeof(data));
b.write_4bytes(16); b.write_4bytes(SrsMp4BoxTypeSCHM); b.write_1bytes(0); b.write_3bytes(0);
b.write_4bytes(1); b.write_4bytes(2); b.skip(-20);
HELPER_ASSERT_SUCCESS(schm.decode(&b));
EXPECT_EQ(20, (int)schm.nb_header());
EXPECT_EQ(0, (int)schm.version);
EXPECT_EQ(0, (int)schm.flags);
EXPECT_EQ(1, (int)schm.scheme_type);
EXPECT_EQ(2, (int)schm.scheme_version);
}
if (true) {
SrsMp4SchemeTypeBox schm;
uint8_t data[12+8+4];
SrsBuffer b((char*)data, sizeof(data));
b.write_4bytes(16); b.write_4bytes(SrsMp4BoxTypeSCHM); b.write_1bytes(0); b.write_3bytes(1);
b.write_4bytes(1); b.write_4bytes(2);
b.write_1bytes(65); b.write_1bytes(65); b.write_1bytes(65); b.write_1bytes(0); b.skip(-24);
HELPER_ASSERT_SUCCESS(schm.decode(&b));
EXPECT_EQ(24, (int)schm.nb_header());
EXPECT_EQ(0, (int)schm.version);
EXPECT_EQ(1, (int)schm.flags);
EXPECT_EQ(1, (int)schm.scheme_type);
EXPECT_EQ(2, (int)schm.scheme_version);
stringstream ss;
SrsMp4DumpContext dc;
schm.dumps_detail(ss, dc);
string v = ss.str();
EXPECT_STREQ("scheme_type=1, scheme_version=2\nscheme_uri=AAA\n", v.c_str());
}
}
VOID TEST(KernelMp4Test, SrsInitMp4Segment)
{
srs_error_t err;
// write single video segment
if (true) {
MockSrsFileWriter fw;
SrsInitMp4Segment segment(&fw);
segment.set_path("/tmp/init.mp4");
SrsFormat fmt;
HELPER_ASSERT_SUCCESS(fmt.initialize());
uint8_t raw[] = {
0x17,
0x00, 0x00, 0x00, 0x00, 0x01, 0x64, 0x00, 0x20, 0xff, 0xe1, 0x00, 0x19, 0x67, 0x64, 0x00, 0x20,
0xac, 0xd9, 0x40, 0xc0, 0x29, 0xb0, 0x11, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x03, 0x00,
0x32, 0x0f, 0x18, 0x31, 0x96, 0x01, 0x00, 0x05, 0x68, 0xeb, 0xec, 0xb2, 0x2c
};
HELPER_ASSERT_SUCCESS(fmt.on_video(0, (char*)raw, sizeof(raw)));
HELPER_ASSERT_SUCCESS(segment.write_video_only(&fmt, 1));
EXPECT_TRUE(fw.filesize() > 0);
}
// write single audio segment
if (true) {
MockSrsFileWriter fw;
SrsInitMp4Segment segment(&fw);
segment.set_path("/tmp/init.mp4");
SrsFormat fmt;
HELPER_ASSERT_SUCCESS(fmt.initialize());
uint8_t raw[] = {
0xaf, 0x00, 0x12, 0x10
};
HELPER_ASSERT_SUCCESS(fmt.on_audio(0, (char*)raw, sizeof(raw)));
HELPER_ASSERT_SUCCESS(segment.write_audio_only(&fmt, 1));
EXPECT_TRUE(fw.filesize() > 0);
}
// write both audio and video segment
if (true) {
MockSrsFileWriter fw;
SrsInitMp4Segment segment(&fw);
segment.set_path("/tmp/init.mp4");
SrsFormat fmt;
HELPER_ASSERT_SUCCESS(fmt.initialize());
uint8_t video_raw[] = {
0x17,
0x00, 0x00, 0x00, 0x00, 0x01, 0x64, 0x00, 0x20, 0xff, 0xe1, 0x00, 0x19, 0x67, 0x64, 0x00, 0x20,
0xac, 0xd9, 0x40, 0xc0, 0x29, 0xb0, 0x11, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x03, 0x00,
0x32, 0x0f, 0x18, 0x31, 0x96, 0x01, 0x00, 0x05, 0x68, 0xeb, 0xec, 0xb2, 0x2c
};
HELPER_ASSERT_SUCCESS(fmt.on_video(0, (char*)video_raw, sizeof(video_raw)));
uint8_t audio_raw[] = {
0xaf, 0x00, 0x12, 0x10
};
HELPER_ASSERT_SUCCESS(fmt.on_audio(0, (char*)audio_raw, sizeof(audio_raw)));
HELPER_ASSERT_SUCCESS(segment.write(&fmt, 1, 2));
EXPECT_TRUE(fw.filesize() > 0);
}
}
VOID TEST(KernelMp4Test, SrsFmp4SegmentEncoder)
{
srs_error_t err;
if (true) {
MockSrsFileWriter fw;
SrsFmp4SegmentEncoder encoder;
encoder.initialize(&fw, 0, 0, 1, 2);
SrsFormat video_fmt;
HELPER_ASSERT_SUCCESS(video_fmt.initialize());
SrsFormat audio_fmt;
HELPER_ASSERT_SUCCESS(audio_fmt.initialize());
uint8_t video_raw[] = {
0x17,
0x00, 0x00, 0x00, 0x00, 0x01, 0x64, 0x00, 0x20, 0xff, 0xe1, 0x00, 0x19, 0x67, 0x64, 0x00, 0x20,
0xac, 0xd9, 0x40, 0xc0, 0x29, 0xb0, 0x11, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x03, 0x00,
0x32, 0x0f, 0x18, 0x31, 0x96, 0x01, 0x00, 0x05, 0x68, 0xeb, 0xec, 0xb2, 0x2c
};
HELPER_ASSERT_SUCCESS(video_fmt.on_video(0, (char*)video_raw, sizeof(video_raw)));
uint8_t audio_raw[] = {
0xaf, 0x00, 0x12, 0x10
};
HELPER_ASSERT_SUCCESS(audio_fmt.on_audio(0, (char*)audio_raw, sizeof(audio_raw)));
SrsVideoAvcFrameType video_frame_type = video_fmt.video->frame_type;
uint32_t cts = (uint32_t)video_fmt.video->cts;
uint32_t dts = 0;
uint32_t pts = dts + cts;
uint8_t* video_sample = (uint8_t*)video_fmt.raw;
uint32_t nb_video_sample = (uint32_t)video_fmt.nb_raw;
encoder.write_sample(SrsMp4HandlerTypeVIDE, video_frame_type, dts, pts, video_sample, nb_video_sample);
uint8_t* audio_sample = (uint8_t*)audio_fmt.raw;
uint32_t nb_audio_sample = (uint32_t)audio_fmt.nb_raw;
encoder.write_sample(SrsMp4HandlerTypeSOUN, 0, 0, 0, audio_sample, nb_audio_sample);
encoder.flush(dts);
EXPECT_TRUE(fw.filesize() > 0);
}
}
VOID TEST(KernelMp4Test, SrsMp4M2tsInitEncoder)
{
srs_error_t err;
@ -1867,5 +2318,32 @@ VOID TEST(KernelMp4Test, SrsMp4M2tsInitEncoder)
HELPER_ASSERT_SUCCESS(enc.write(&fmt, false, 1));
EXPECT_TRUE(fw.filesize() > 0);
}
if (true) {
MockSrsFileWriter fw;
HELPER_ASSERT_SUCCESS(fw.open("test.mp4"));
SrsMp4M2tsInitEncoder enc;
HELPER_ASSERT_SUCCESS(enc.initialize(&fw));
SrsFormat fmt;
EXPECT_TRUE(srs_success == fmt.initialize());
uint8_t video_raw[] = {
0x17,
0x00, 0x00, 0x00, 0x00, 0x01, 0x64, 0x00, 0x20, 0xff, 0xe1, 0x00, 0x19, 0x67, 0x64, 0x00, 0x20,
0xac, 0xd9, 0x40, 0xc0, 0x29, 0xb0, 0x11, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x03, 0x00,
0x32, 0x0f, 0x18, 0x31, 0x96, 0x01, 0x00, 0x05, 0x68, 0xeb, 0xec, 0xb2, 0x2c
};
EXPECT_TRUE(srs_success == fmt.on_video(0, (char*)video_raw, sizeof(video_raw)));
uint8_t audio_raw[] = {
0xaf, 0x00, 0x12, 0x10
};
EXPECT_TRUE(srs_success == fmt.on_audio(0, (char*)audio_raw, sizeof(audio_raw)));
HELPER_ASSERT_SUCCESS(enc.write(&fmt, 1, 2));
EXPECT_TRUE(fw.filesize() > 0);
}
}