Skip to content

Commit

Permalink
HLS: Support low latency mode about 5s. v5.13.7
Browse files Browse the repository at this point in the history
  • Loading branch information
winlinvip committed Jan 6, 2024
1 parent ad3e2ef commit a6b709f
Show file tree
Hide file tree
Showing 14 changed files with 300 additions and 37 deletions.
4 changes: 4 additions & 0 deletions DEVELOPER.md
Expand Up @@ -885,6 +885,8 @@ Platform, with token authentication:
* `/terraform/v1/mgmt/secret/query` Query the api secret for OpenAPI.
* `/terraform/v1/mgmt/hphls/update` HLS delivery in high performance mode.
* `/terraform/v1/mgmt/hphls/query` Query HLS delivery in high performance mode.
* `/terraform/v1/mgmt/hlsll/update` Setup HLS low latency mode.
* `/terraform/v1/mgmt/hlsll/query` Query state of HLS low latency mode.
* `/terraform/v1/mgmt/ssl` Config the system SSL config.
* `/terraform/v1/mgmt/auto-self-signed-certificate` Create the self-signed certificate if no cert.
* `/terraform/v1/mgmt/letsencrypt` Config the let's encrypt SSL.
Expand Down Expand Up @@ -1062,6 +1064,7 @@ The following are the update records for the SRS Stack server.
* Fix bug: Remove HTTP port for SRS. v5.13.4
* Refine API with Bearer token. v5.13.5
* Switch to fluid max width. v5.13.6
* HLS: Support low latency mode about 5s. v5.13.7
* v5.12
* Refine local variable name conf to config. v5.12.1
* Add forced exit on timeout for program termination. v5.12.1
Expand All @@ -1083,6 +1086,7 @@ The following are the update records for the SRS Stack server.
* Transcript: Support clear the subtitle of segment in fixing queue. [v5.12.15](https://github.com/ossrs/srs-stack/releases/tag/v5.12.15)
* VLive: Fix bug for url with query string. v5.12.16
* Transcript: Check the base url for OpenAI. [v5.12.17](https://github.com/ossrs/srs-stack/releases/tag/v5.12.17)
* HLS: Support low latency mode about 5s. v5.12.18
* v5.11
* VLive: Decrease the latency for virtual live. v5.11.1
* Live: Refine multiple language. v5.11.2
Expand Down
7 changes: 0 additions & 7 deletions platform/containers/www/tools/js/flv-1.5.0.min.js

This file was deleted.

1 change: 0 additions & 1 deletion platform/containers/www/tools/js/flv.min.js.map

This file was deleted.

2 changes: 0 additions & 2 deletions platform/containers/www/tools/js/hls-0.14.17.min.js

This file was deleted.

2 changes: 2 additions & 0 deletions platform/containers/www/tools/js/hls-1.4.14.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion platform/containers/www/tools/js/hls.min.js.map

Large diffs are not rendered by default.

9 changes: 9 additions & 0 deletions platform/containers/www/tools/js/mpegts-1.7.3.min.js

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions platform/containers/www/tools/js/mpegts.js.map

Large diffs are not rendered by default.

73 changes: 52 additions & 21 deletions platform/containers/www/tools/player.html
Expand Up @@ -13,43 +13,74 @@
<video id="player" autoplay controls muted></video>
</body>
<script type="text/javascript" src="js/jquery-1.10.2.min.js"></script>
<script type="text/javascript" src="js/flv-1.5.0.min.js"></script>
<script type="text/javascript" src="js/hls-0.14.17.min.js"></script>
<script type="text/javascript" src="js/mpegts-1.7.3.min.js"></script>
<script type="text/javascript" src="js/hls-1.4.14.min.js"></script>
<script type="text/javascript">
$(function(){
// See https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams
const q = new URLSearchParams(location.search);

const resource = q.get('url');
if (!resource) return alert(`没有指定URL`);
if (!resource) return alert(`No URL`);

if (resource.indexOf('.flv') > 0) {
if (!flvjs.isSupported()) return alert(`你的浏览器不支持HTTP-FLV`);
const video = document.getElementById('player');
if (resource.indexOf('.m3u8') > 0) {
if (!Hls.isSupported()) {
// See https://stackoverflow.com/a/12905122/17679565
if (document.createElement('video').canPlayType('application/vnd.apple.mpegURL')) {
console.log(`Play by native for ${resource}`);
$('#player').prop('src', resource);
return;
}
return alert(`HLS is not supported, url is ${resource}`);
}

const player = flvjs.createPlayer({type: 'flv', url: resource});
player.attachMediaElement(document.getElementById('player'));
player.load();
player.play();
console.log(`Play by flv.js for ${resource}`);
// See https://github.com/video-dev/hls.js/blob/master/docs/API.md#maxlivesyncplaybackrate
// See https://github.com/video-dev/hls.js/issues/3077#issuecomment-705152394
const player = new Hls({
enableWorker: true, // Improve performance and avoid lag/frame drops.
lowLatencyMode: true, // Enable Low-Latency HLS part playlist and segment loading.
liveSyncDurationCount: 0, // Start from the last segment.
liveMaxLatencyDurationCount: 4, // Maximum delay allowed from edge of live.
maxBufferLength: 5, // Maximum buffer length in seconds.
maxMaxBufferLength: 8, // The max Maximum buffer length in seconds.
maxLiveSyncPlaybackRate: 2, // Catch up if the latency is large.
liveDurationInfinity: true // Override current Media Source duration to Infinity for a live broadcast.
});
player.loadSource(resource);
player.attachMedia(video);
video.play();
console.log(`Play by hls.js for ${resource}`);
return;
}

if (resource.indexOf('.m3u8') > 0) {
// See https://stackoverflow.com/a/12905122/17679565
if (document.createElement('video').canPlayType('application/vnd.apple.mpegURL')) {
console.log(`Play by native for ${resource}`);
$('#player').prop('src', resource);
return;
// Start play HTTP-FLV.
let isFlv = resource.indexOf('.flv') > 0;
// Compatible with NGINX-HTTP-FLV module, see https://github.com/winshining/nginx-http-flv-module and the stream
// url without .flv, such as:
// http://localhost:8080/live?app=live&stream=livestream
isFlv = isFlv || resource && resource.indexOf('http') === 0;
if (isFlv) {
if (!mpegts.getFeatureList().mseLivePlayback) {
return alert(`HTTP-FLV is not supported, url is ${resource}`);
}

const player = new Hls();
player.loadSource(resource);
player.attachMedia(document.getElementById('player'));
console.log(`Play by hls.js for ${resource}`);
// See https://github.com/xqq/mpegts.js/blob/master/src/config.js#L32
// See https://github.com/xqq/mpegts.js/blob/master/docs/api.md
const player = mpegts.createPlayer({
type: 'flv', url: resource,
isLive: true, enableStashBuffer: false, liveSync: true
});
player.attachMediaElement(video);
player.load();
player.play();
console.log(`Play by flv.js for ${resource}`);
return;
}

alert(`不支持的URL:${resource}`);
const msg = `Not supported URL ${resource}`;
console.error(msg);
alert(msg);
});
</script>
</html>
Expand Down
80 changes: 80 additions & 0 deletions platform/service.go
Expand Up @@ -241,6 +241,8 @@ func handleHTTPService(ctx context.Context, handler *http.ServeMux) error {
handleMgmtBeianUpdate(ctx, handler)
handleMgmtNginxHlsUpdate(ctx, handler)
handleMgmtNginxHlsQuery(ctx, handler)
handleMgmtHlsLowLatencyUpdate(ctx, handler)
handleMgmtHlsLowLatencyQuery(ctx, handler)
handleMgmtAutoSelfSignedCertificate(ctx, handler)
handleMgmtSsl(ctx, handler)
handleMgmtLetsEncrypt(ctx, handler)
Expand Down Expand Up @@ -1078,6 +1080,84 @@ func handleMgmtNginxHlsQuery(ctx context.Context, handler *http.ServeMux) {
})
}

func handleMgmtHlsLowLatencyUpdate(ctx context.Context, handler *http.ServeMux) {
ep := "/terraform/v1/mgmt/hlsll/update"
logger.Tf(ctx, "Handle %v", ep)
handler.HandleFunc(ep, func(w http.ResponseWriter, r *http.Request) {
if err := func() error {
var token string
var hlsLowLatency bool
if err := ParseBody(ctx, r.Body, &struct {
Token *string `json:"token"`
HlsLowLatency *bool `json:"hlsLowLatency"`
}{
Token: &token, HlsLowLatency: &hlsLowLatency,
}); err != nil {
return errors.Wrapf(err, "parse body")
}

apiSecret := os.Getenv("SRS_PLATFORM_SECRET")
if err := Authenticate(ctx, apiSecret, token, r.Header); err != nil {
return errors.Wrapf(err, "authenticate")
}

hlsLowLatencyValue := fmt.Sprintf("%v", hlsLowLatency)
if err := rdb.HSet(ctx, SRS_LL_HLS, "hlsLowLatency", hlsLowLatencyValue).Err(); err != nil && err != redis.Nil {
return errors.Wrapf(err, "hset %v hlsLowLatency %v", SRS_LL_HLS, hlsLowLatencyValue)
}

if err := srsGenerateConfig(ctx); err != nil {
return errors.Wrapf(err, "generate SRS config")
}

ohttp.WriteData(ctx, w, r, nil)
logger.Tf(ctx, "hls low latency update ok, enabled=%v, token=%vB", hlsLowLatency, len(token))
return nil
}(); err != nil {
ohttp.WriteError(ctx, w, r, err)
}
})
}

func handleMgmtHlsLowLatencyQuery(ctx context.Context, handler *http.ServeMux) {
ep := "/terraform/v1/mgmt/hlsll/query"
logger.Tf(ctx, "Handle %v", ep)
handler.HandleFunc(ep, func(w http.ResponseWriter, r *http.Request) {
if err := func() error {
var token string
if err := ParseBody(ctx, r.Body, &struct {
Token *string `json:"token"`
}{
Token: &token,
}); err != nil {
return errors.Wrapf(err, "parse body")
}

apiSecret := os.Getenv("SRS_PLATFORM_SECRET")
if err := Authenticate(ctx, apiSecret, token, r.Header); err != nil {
return errors.Wrapf(err, "authenticate")
}

var enabled bool
if v, err := rdb.HGet(ctx, SRS_LL_HLS, "hlsLowLatency").Result(); err != nil && err != redis.Nil {
return errors.Wrapf(err, "hget %v %v", SRS_LL_HLS, "hlsLowLatency")
} else {
enabled = v == "true"
}

ohttp.WriteData(ctx, w, r, &struct {
HlsLowLatency bool `json:"hlsLowLatency"`
}{
HlsLowLatency: enabled,
})
logger.Tf(ctx, "hls low latency query ok, enabled=%v, token=%vB", enabled, len(token))
return nil
}(); err != nil {
ohttp.WriteError(ctx, w, r, err)
}
})
}

func handleMgmtAutoSelfSignedCertificate(ctx context.Context, handler *http.ServeMux) {
ep := "/terraform/v1/mgmt/auto-self-signed-certificate"
logger.Tf(ctx, "Handle %v", ep)
Expand Down
22 changes: 19 additions & 3 deletions platform/utils.go
Expand Up @@ -253,6 +253,7 @@ const (
SRS_TENCENT_LH = "SRS_TENCENT_LH"
// For SRS stream status.
SRS_HP_HLS = "SRS_HP_HLS"
SRS_LL_HLS = "SRS_LL_HLS"
// For tencent cloud products.
SRS_TENCENT_CAM = "SRS_TENCENT_CAM"
SRS_TENCENT_COS = "SRS_TENCENT_COS"
Expand Down Expand Up @@ -464,15 +465,30 @@ func srsGenerateConfig(ctx context.Context) error {
"",
"hls {",
" enabled on;",
" hls_fragment 10;",
" hls_window 60;",
}
if hlsLowLatency, err := rdb.HGet(ctx, SRS_LL_HLS, "hlsLowLatency").Result(); err != nil && err != redis.Nil {
return errors.Wrapf(err, "hget %v hls", SRS_LL_HLS)
} else {
if hlsLowLatency != "true" {
hlsConf = append(hlsConf, []string{
" hls_fragment 10;",
" hls_window 60;",
}...)
} else {
hlsConf = append(hlsConf, []string{
" hls_fragment 2;",
" hls_window 12;",
}...)
}
}
hlsConf = append(hlsConf, []string{
" hls_aof_ratio 2.1;",
" hls_path ./objs/nginx/html;",
" hls_m3u8_file [app]/[stream].m3u8;",
" hls_ts_file [app]/[stream]-[seq]-[timestamp].ts;",
" hls_wait_keyframe on;",
" hls_dispose 10;",
}
}...)
if noHlsCtx, err := rdb.HGet(ctx, SRS_HP_HLS, "noHlsCtx").Result(); err != nil && err != redis.Nil {
return errors.Wrapf(err, "hget %v hls", SRS_HP_HLS)
} else if noHlsCtx == "true" {
Expand Down
90 changes: 90 additions & 0 deletions test/api_test.go
Expand Up @@ -425,6 +425,96 @@ func TestApi_SetupHpHLSWithHlsCtx(t *testing.T) {
}
}

func TestApi_SetupHlsLowLatencyEnable(t *testing.T) {
ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond)
defer cancel()

var r0 error
defer func(ctx context.Context) {
if err := filterTestError(ctx.Err(), r0); err != nil {
t.Errorf("Fail for err %+v", err)
} else {
logger.Tf(ctx, "test done")
}
}(ctx)

type Data struct {
HlsLowLatency bool `json:"hlsLowLatency"`
}

if true {
initData := Data{}
if err := NewApi().WithAuth(ctx, "/terraform/v1/mgmt/hlsll/query", nil, &initData); err != nil {
r0 = err
return
}
defer func() {
if err := NewApi().WithAuth(ctx, "/terraform/v1/mgmt/hlsll/update", &initData, nil); err != nil {
logger.Tf(ctx, "restore hlsll config failed %+v", err)
}
}()
}

hlsLowLatency := Data{HlsLowLatency: true}
if err := NewApi().WithAuth(ctx, "/terraform/v1/mgmt/hlsll/update", &hlsLowLatency, nil); err != nil {
r0 = err
return
}

verifyData := Data{}
if err := NewApi().WithAuth(ctx, "/terraform/v1/mgmt/hlsll/query", nil, &verifyData); err != nil {
r0 = err
return
} else if verifyData.HlsLowLatency != true {
r0 = errors.Errorf("invalid response %+v", verifyData)
}
}

func TestApi_SetupHlsLowLatencyDisable(t *testing.T) {
ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond)
defer cancel()

var r0 error
defer func(ctx context.Context) {
if err := filterTestError(ctx.Err(), r0); err != nil {
t.Errorf("Fail for err %+v", err)
} else {
logger.Tf(ctx, "test done")
}
}(ctx)

type Data struct {
HlsLowLatency bool `json:"hlsLowLatency"`
}

if true {
initData := Data{}
if err := NewApi().WithAuth(ctx, "/terraform/v1/mgmt/hlsll/query", nil, &initData); err != nil {
r0 = err
return
}
defer func() {
if err := NewApi().WithAuth(ctx, "/terraform/v1/mgmt/hlsll/update", &initData, nil); err != nil {
logger.Tf(ctx, "restore hlsll config failed %+v", err)
}
}()
}

hlsLowLatency := Data{HlsLowLatency: false}
if err := NewApi().WithAuth(ctx, "/terraform/v1/mgmt/hlsll/update", &hlsLowLatency, nil); err != nil {
r0 = err
return
}

verifyData := Data{}
if err := NewApi().WithAuth(ctx, "/terraform/v1/mgmt/hlsll/query", nil, &verifyData); err != nil {
r0 = err
return
} else if verifyData.HlsLowLatency != false {
r0 = errors.Errorf("invalid response %+v", verifyData)
}
}

func TestApi_SrsApiNoAuth(t *testing.T) {
ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond)
defer cancel()
Expand Down

0 comments on commit a6b709f

Please sign in to comment.