1
0
Fork 0
mirror of https://github.com/ossrs/srs.git synced 2025-03-09 15:49:59 +00:00

SRS5: DVR: Support blackbox test based on hooks. v5.0.132 (#3365)

PICK e655948e96
This commit is contained in:
Winlin 2023-01-07 20:36:59 +08:00 committed by winlin
parent 3c6ade8721
commit f06a2d61f7
31 changed files with 4704 additions and 3925 deletions

View file

@ -4,6 +4,7 @@ name: "Test"
on: [push, pull_request] on: [push, pull_request]
# The dependency graph: # The dependency graph:
# test(6m)
# multiple-arch-armv7(13m) # multiple-arch-armv7(13m)
# multiple-arch-aarch64(7m) # multiple-arch-aarch64(7m)
# cygwin64-cache(1m) # cygwin64-cache(1m)
@ -16,9 +17,7 @@ on: [push, pull_request]
# build-cross-arm(3m) # build-cross-arm(3m)
# build-cross-aarch64(3m) # build-cross-aarch64(3m)
# multiple-arch-amd64(2m) # multiple-arch-amd64(2m)
# utest(3m)
# coverage(3m) # coverage(3m)
# blackbox(3m)
jobs: jobs:
cygwin64-cache: cygwin64-cache:
@ -163,30 +162,8 @@ jobs:
run: DOCKER_BUILDKIT=1 docker build -f trunk/Dockerfile.builds --target ubuntu20-cross-aarch64 . run: DOCKER_BUILDKIT=1 docker build -f trunk/Dockerfile.builds --target ubuntu20-cross-aarch64 .
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
utest: test:
name: utest name: utest-regression-blackbox-test
needs:
- fast
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Tests
- name: Build test image
run: docker build --tag srs:test --build-arg MAKEARGS='-j2' -f trunk/Dockerfile.test .
# For utest
- name: Run SRS utest
run: docker run --rm srs:test ./objs/srs_utest
# For regression-test
- name: Run SRS regression-test
run: |
docker run --rm srs:test bash -c './objs/srs -c conf/regression-test.conf && \
cd 3rdparty/srs-bench && ./objs/srs_test -test.v && ./objs/srs_gb28181_test -test.v'
runs-on: ubuntu-20.04
blackbox:
name: blackbox
needs:
- fast
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v3 uses: actions/checkout@v3
@ -196,10 +173,21 @@ jobs:
# For blackbox-test # For blackbox-test
- name: Run SRS blackbox-test - name: Run SRS blackbox-test
run: | run: |
#docker run --rm -w /srs/trunk/3rdparty/srs-bench srs:test ./objs/srs_blackbox_test -test.v \
# -test.run 'TestFast_RtmpPublish_DvrFlv_Basic' -srs-log -srs-stdout srs-ffmpeg-stderr -srs-dvr-stderr \
# -srs-ffprobe-stdout
docker run --rm -w /srs/trunk/3rdparty/srs-bench srs:test \ docker run --rm -w /srs/trunk/3rdparty/srs-bench srs:test \
./objs/srs_blackbox_test -test.v -test.run '^TestFast' -test.parallel 64 ./objs/srs_blackbox_test -test.v -test.run '^TestFast' -test.parallel 64
docker run --rm -w /srs/trunk/3rdparty/srs-bench srs:test \ docker run --rm -w /srs/trunk/3rdparty/srs-bench srs:test \
./objs/srs_blackbox_test -test.v -test.run '^TestSlow' -test.parallel 4 ./objs/srs_blackbox_test -test.v -test.run '^TestSlow' -test.parallel 4
# For utest
- name: Run SRS utest
run: docker run --rm srs:test ./objs/srs_utest
# For regression-test
- name: Run SRS regression-test
run: |
docker run --rm srs:test bash -c './objs/srs -c conf/regression-test.conf && \
cd 3rdparty/srs-bench && ./objs/srs_test -test.v && ./objs/srs_gb28181_test -test.v'
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
coverage: coverage:
@ -312,8 +300,7 @@ jobs:
needs: needs:
- cygwin64 - cygwin64
- coverage - coverage
- blackbox - test
- utest
- build-centos7 - build-centos7
- build-ubuntu16 - build-ubuntu16
- build-ubuntu18 - build-ubuntu18

View file

@ -0,0 +1,143 @@
// The MIT License (MIT)
//
// # Copyright (c) 2023 Winlin
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package blackbox
import (
"context"
"fmt"
"github.com/ossrs/go-oryx-lib/errors"
"github.com/ossrs/go-oryx-lib/logger"
"math/rand"
"os"
"path"
"sync"
"testing"
"time"
)
func TestFast_RtmpPublish_DvrFlv_Basic(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, r5, r6 error
defer func(ctx context.Context) {
if err := filterTestError(ctx.Err(), r0, r1, r2, r3, r4, r5, r6); 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 hooks service.
hooks := NewHooksService()
wg.Add(1)
go func() {
defer wg.Done()
r6 = hooks.Run(ctx, cancel)
}()
// Start SRS server and wait for it to be ready.
svr := NewSRSServer(func(v *srsServer) {
v.envs = []string{
"SRS_VHOST_DVR_ENABLED=on",
"SRS_VHOST_DVR_DVR_PLAN=session",
"SRS_VHOST_DVR_DVR_PATH=./objs/nginx/html/[app]/[stream].[timestamp].flv",
fmt.Sprintf("SRS_VHOST_DVR_DVR_DURATION=%v", *srsFFprobeDuration),
"SRS_VHOST_HTTP_HOOKS_ENABLED=on",
fmt.Sprintf("SRS_VHOST_HTTP_HOOKS_ON_DVR=http://localhost:%v/api/v1/dvrs", hooks.HooksAPI()),
}
})
wg.Add(1)
go func() {
defer wg.Done()
<-hooks.ReadyCtx().Done()
r0 = svr.Run(ctx, cancel)
}()
// Start FFmpeg to publish stream.
duration := time.Duration(*srsFFprobeDuration) * time.Millisecond
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) {
// When process quit, still keep case to run.
v.cancelCaseWhenQuit, v.ffmpegDuration = false, duration
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.
ffprobe := NewFFprobe(func(v *ffprobeClient) {
v.dvrByFFmpeg, v.streamURL = false, streamURL
v.duration, v.timeout = duration, time.Duration(*srsFFprobeTimeout)*time.Millisecond
wg.Add(1)
go func() {
defer wg.Done()
for evt := range hooks.HooksEvents() {
if onDvrEvt, ok := evt.(*HooksEventOnDvr); ok {
fp := path.Join(svr.WorkDir(), onDvrEvt.File)
logger.Tf(ctx, "FFprobe: Set the dvrFile=%v from callback", fp)
v.dvrFile = fp
}
}
}()
})
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)
}
if ts := 90; m.Format.ProbeScore < ts {
r4 = errors.Errorf("low score=%v < %v, %v, %v", m.Format.ProbeScore, ts, m.String(), str)
}
if dv := m.Duration(); dv < duration/2 {
r5 = errors.Errorf("short duration=%v < %v, %v, %v", dv, duration/2, m.String(), str)
}
}
}

View file

@ -27,6 +27,7 @@ import (
"flag" "flag"
"fmt" "fmt"
"github.com/ossrs/go-oryx-lib/errors" "github.com/ossrs/go-oryx-lib/errors"
ohttp "github.com/ossrs/go-oryx-lib/http"
"github.com/ossrs/go-oryx-lib/logger" "github.com/ossrs/go-oryx-lib/logger"
"io/ioutil" "io/ioutil"
"math/rand" "math/rand"
@ -200,6 +201,8 @@ type backendService struct {
name string name string
args []string args []string
env []string env []string
// If timeout, kill the process.
duration time.Duration
// The process stdout and stderr. // The process stdout and stderr.
stdout bytes.Buffer stdout bytes.Buffer
@ -315,6 +318,22 @@ func (v *backendService) Run(ctx context.Context, cancel context.CancelFunc) err
// The context for SRS process. // The context for SRS process.
processDone, processDoneCancel := context.WithCancel(context.Background()) processDone, processDoneCancel := context.WithCancel(context.Background())
// If exceed timeout, kill the process.
v.wg.Add(1)
go func() {
defer v.wg.Done()
if v.duration <= 0 {
return
}
select {
case <- ctx.Done():
case <-time.After(v.duration):
logger.Tf(ctx, "Process killed duration=%v, pid=%v, name=%v, args=%v", v.duration, v.pid, v.name, v.args)
cmd.Process.Kill()
}
}()
// If SRS process terminated, notify case to stop. // If SRS process terminated, notify case to stop.
v.wg.Add(1) v.wg.Add(1)
go func() { go func() {
@ -327,12 +346,12 @@ func (v *backendService) Run(ctx context.Context, cancel context.CancelFunc) err
defer processDoneCancel() defer processDoneCancel()
if err := cmd.Wait(); err != nil && !v.ignoreExitStatusError { if err := cmd.Wait(); err != nil && !v.ignoreExitStatusError {
v.r0 = errors.Wrapf(err, "Process wait err, name=%v, args=%v", v.name, v.args) v.r0 = errors.Wrapf(err, "Process wait err, pid=%v, name=%v, args=%v", v.pid, v.name, v.args)
} }
if v.onStop != nil { if v.onStop != nil {
if err := v.onStop(ctx, v, cmd, v.r0, &v.stdout, &v.stderr); err != nil { if err := v.onStop(ctx, v, cmd, v.r0, &v.stdout, &v.stderr); err != nil {
if v.r0 == nil { if v.r0 == nil {
v.r0 = errors.Wrapf(err, "Process onStop err, name=%v, args=%v", v.name, v.args) v.r0 = errors.Wrapf(err, "Process onStop err, pid=%v, name=%v, args=%v", v.pid, v.name, v.args)
} else { } else {
logger.Ef(ctx, "Process onStop err %v", err) logger.Ef(ctx, "Process onStop err %v", err)
} }
@ -435,7 +454,7 @@ type srsServer struct {
func NewSRSServer(opts ...func(v *srsServer)) SRSServer { func NewSRSServer(opts ...func(v *srsServer)) SRSServer {
rid := fmt.Sprintf("%v-%v", os.Getpid(), rand.Int()) rid := fmt.Sprintf("%v-%v", os.Getpid(), rand.Int())
v := &srsServer{ v := &srsServer{
workDir: "./", workDir: path.Join("objs", fmt.Sprintf("%v", rand.Int())),
srsID: fmt.Sprintf("srs-id-%v", rid), srsID: fmt.Sprintf("srs-id-%v", rid),
process: newBackendService(), process: newBackendService(),
} }
@ -443,7 +462,7 @@ func NewSRSServer(opts ...func(v *srsServer)) SRSServer {
// If we run in GoLand, the current directory is in blackbox, so we use parent directory. // If we run in GoLand, the current directory is in blackbox, so we use parent directory.
if _, err := os.Stat("objs"); err != nil { if _, err := os.Stat("objs"); err != nil {
v.workDir = "../" v.workDir = path.Join("..", "objs", fmt.Sprintf("%v", rand.Int()))
} }
// Do allocate resource. // Do allocate resource.
@ -461,22 +480,12 @@ func NewSRSServer(opts ...func(v *srsServer)) SRSServer {
allocator.Free(v.httpListen) allocator.Free(v.httpListen)
allocator.Free(v.srtListen) allocator.Free(v.srtListen)
pidFile := path.Join(v.workDir, v.srsRelativePidFile) if _, err := os.Stat(v.workDir); err == nil {
if _, err := os.Stat(pidFile); err == nil { os.RemoveAll(v.workDir)
os.Remove(pidFile)
} }
idFile := path.Join(v.workDir, v.srsRelativeIDFile) logger.Tf(ctx, "SRS server is closed, id=%v, pid=%v, cleanup=%v r0=%v",
if _, err := os.Stat(idFile); err == nil { v.srsID, bs.pid, v.workDir, bs.r0)
os.Remove(idFile)
}
hlsFiles := path.Join(v.workDir, "objs", "live")
if _, err := os.Stat(hlsFiles); err == nil {
os.RemoveAll(hlsFiles)
}
logger.Tf(ctx, "SRS server is closed, id=%v, pid=%v, r0=%v", v.srsID, bs.pid, bs.r0)
return nil return nil
} }
@ -512,20 +521,39 @@ func (v *srsServer) Run(ctx context.Context, cancel context.CancelFunc) error {
v.workDir, *srsBinary, v.srsID, v.srsRelativePidFile, v.rtmpListen, v.workDir, *srsBinary, v.srsID, v.srsRelativePidFile, v.rtmpListen,
) )
// Create directories.
if err := os.MkdirAll(path.Join(v.workDir, "./objs/nginx/html"), os.FileMode(0755) | os.ModeDir); err != nil {
return errors.Wrapf(err, "SRS create directory %v", path.Join(v.workDir, "./objs/nginx/html"))
}
// Setup the name and args of process. // Setup the name and args of process.
v.process.name = *srsBinary v.process.name = *srsBinary
v.process.args = []string{"-e"} v.process.args = []string{"-e"}
// Setup the envrionment variables. // Setup the constant values.
v.process.env = []string{ v.process.env = []string{
// SRS working directory.
fmt.Sprintf("SRS_WORK_DIR=%v", v.workDir),
// Run in frontend. // Run in frontend.
"SRS_DAEMON=off", "SRS_DAEMON=off",
// Write logs to stdout and stderr. // Write logs to stdout and stderr.
"SRS_SRS_LOG_FILE=console", "SRS_SRS_LOG_FILE=console",
// Disable warning for asan. // Disable warning for asan.
"MallocNanoZone=0", "MallocNanoZone=0",
// Avoid error for macOS, which ulimit to 256.
"SRS_MAX_CONNECTIONS=100",
}
// For directories.
v.process.env = append(v.process.env, []string{
// SRS working directory.
fmt.Sprintf("SRS_WORK_DIR=%v", v.workDir),
// Setup the default directory for HTTP server.
"SRS_HTTP_SERVER_DIR=./objs/nginx/html",
// Setup the default directory for HLS stream.
"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",
}...)
// For variables.
v.process.env = append(v.process.env, []string{
// SRS PID file. // SRS PID file.
fmt.Sprintf("SRS_PID=%v", v.srsRelativePidFile), fmt.Sprintf("SRS_PID=%v", v.srsRelativePidFile),
// SRS ID file. // SRS ID file.
@ -533,19 +561,13 @@ func (v *srsServer) Run(ctx context.Context, cancel context.CancelFunc) error {
// HTTP API to detect the service. // HTTP API to detect the service.
fmt.Sprintf("SRS_HTTP_API_ENABLED=on"), fmt.Sprintf("SRS_HTTP_API_ENABLED=on"),
fmt.Sprintf("SRS_HTTP_API_LISTEN=%v", v.apiListen), fmt.Sprintf("SRS_HTTP_API_LISTEN=%v", v.apiListen),
// Avoid error for macOS, which ulimit to 256.
"SRS_MAX_CONNECTIONS=100",
// Setup the default directory for HTTP server.
"SRS_HTTP_SERVER_DIR=objs",
// Setup the default directory for HLS stream.
"SRS_VHOST_HLS_HLS_PATH=objs",
// Setup the RTMP listen port. // Setup the RTMP listen port.
fmt.Sprintf("SRS_LISTEN=%v", v.rtmpListen), fmt.Sprintf("SRS_LISTEN=%v", v.rtmpListen),
// Setup the HTTP sever listen port. // Setup the HTTP sever listen port.
fmt.Sprintf("SRS_HTTP_SERVER_LISTEN=%v", v.httpListen), fmt.Sprintf("SRS_HTTP_SERVER_LISTEN=%v", v.httpListen),
// Setup the SRT server listen port. // Setup the SRT server listen port.
fmt.Sprintf("SRS_SRT_SERVER_LISTEN=%v", v.srtListen), fmt.Sprintf("SRS_SRT_SERVER_LISTEN=%v", v.srtListen),
} }...)
// Rewrite envs by case. // Rewrite envs by case.
if v.envs != nil { if v.envs != nil {
v.process.env = append(v.process.env, v.envs...) v.process.env = append(v.process.env, v.envs...)
@ -588,8 +610,8 @@ func (v *srsServer) Run(ctx context.Context, cancel context.CancelFunc) error {
// Hooks for process. // Hooks for process.
v.process.onBeforeStart = func(ctx context.Context, bs *backendService, cmd *exec.Cmd) error { v.process.onBeforeStart = func(ctx context.Context, bs *backendService, cmd *exec.Cmd) error {
logger.Tf(ctx, "SRS id=%v, env=%v, cmd is %v %v", logger.Tf(ctx, "SRS id=%v, env %v %v %v",
v.srsID, cmd.Env, bs.name, strings.Join(bs.args, " ")) v.srsID, strings.Join(cmd.Env, " "), bs.name, strings.Join(bs.args, " "))
return nil return nil
} }
v.process.onAfterStart = func(ctx context.Context, bs *backendService, cmd *exec.Cmd) error { v.process.onAfterStart = func(ctx context.Context, bs *backendService, cmd *exec.Cmd) error {
@ -625,11 +647,16 @@ type ffmpegClient struct {
// FFmpeg cli args, without ffmpeg binary. // FFmpeg cli args, without ffmpeg binary.
args []string args []string
// Let the process quit, do not cancel the case.
cancelCaseWhenQuit bool
// When timeout, stop FFmpeg, sometimes the '-t' does not work.
ffmpegDuration time.Duration
} }
func NewFFmpeg(opts ...func(v *ffmpegClient)) FFmpegClient { func NewFFmpeg(opts ...func(v *ffmpegClient)) FFmpegClient {
v := &ffmpegClient{ v := &ffmpegClient{
process: newBackendService(), process: newBackendService(),
cancelCaseWhenQuit: true,
} }
// Do cleanup. // Do cleanup.
@ -657,6 +684,7 @@ func (v *ffmpegClient) Run(ctx context.Context, cancel context.CancelFunc) error
v.process.name = *srsFFmpeg v.process.name = *srsFFmpeg
v.process.args = v.args v.process.args = v.args
v.process.env = os.Environ() v.process.env = os.Environ()
v.process.duration = v.ffmpegDuration
v.process.onStop = func(ctx context.Context, bs *backendService, cmd *exec.Cmd, r0 error, stdout, stderr *bytes.Buffer) error { v.process.onStop = func(ctx context.Context, bs *backendService, cmd *exec.Cmd, r0 error, stdout, stderr *bytes.Buffer) error {
logger.Tf(ctx, "FFmpeg process pid=%v exit, r0=%v, stdout=%v", bs.pid, r0, stdout.String()) logger.Tf(ctx, "FFmpeg process pid=%v exit, r0=%v, stdout=%v", bs.pid, r0, stdout.String())
@ -666,7 +694,20 @@ func (v *ffmpegClient) Run(ctx context.Context, cancel context.CancelFunc) error
return nil return nil
} }
return v.process.Run(ctx, cancel) // We might not want to cancel the case, for example, when check DVR by session, we just let the FFmpeg process to
// quit and we should check the callback and DVR file.
ffCtx, ffCancel := context.WithCancel(ctx)
go func() {
select {
case <- ctx.Done():
case <-ffCtx.Done():
if v.cancelCaseWhenQuit {
cancel()
}
}
}()
return v.process.Run(ffCtx, ffCancel)
} }
type FFprobeClient interface { type FFprobeClient interface {
@ -678,16 +719,19 @@ type FFprobeClient interface {
} }
type ffprobeClient struct { type ffprobeClient struct {
// The stream to probe. // The DVR file for ffprobe. Stream should be DVR to file, then use ffprobe to detect it. If DVR by FFmpeg, we will
streamURL string // start a FFmpeg process to do the DVR, or the DVR should be done by other tools.
// The DVR file for ffprobe. We DVR stream to file, then use ffprobe to detect it.
dvrFile string dvrFile string
// The duration of video file for DVR.
duration time.Duration
// The timeout to wait for task to done. // The timeout to wait for task to done.
timeout time.Duration timeout time.Duration
// Whether do DVR by FFmpeg, if using SRS DVR, please set to false.
dvrByFFmpeg bool
// The stream to DVR for probing. Ignore if not DVR by ffmpeg
streamURL string
// The duration of video file for DVR and probing.
duration time.Duration
// When probe stream metadata object. // When probe stream metadata object.
doneCtx context.Context doneCtx context.Context
doneCancel context.CancelFunc doneCancel context.CancelFunc
@ -700,6 +744,7 @@ type ffprobeClient struct {
func NewFFprobe(opts ...func(v *ffprobeClient)) FFprobeClient { func NewFFprobe(opts ...func(v *ffprobeClient)) FFprobeClient {
v := &ffprobeClient{ v := &ffprobeClient{
metadata: &ffprobeObject{}, metadata: &ffprobeObject{},
dvrByFFmpeg: true,
} }
v.doneCtx, v.doneCancel = context.WithCancel(context.Background()) v.doneCtx, v.doneCancel = context.WithCancel(context.Background())
@ -728,9 +773,12 @@ func (v *ffprobeClient) Run(ctxCase context.Context, cancelCase context.CancelFu
// Try to start a DVR process. // Try to start a DVR process.
for ctx.Err() == nil { for ctx.Err() == nil {
// If not DVR by FFmpeg, we just wait the DVR file to be ready, and it should be done by SRS or other tools.
if v.dvrByFFmpeg {
// If error, just ignore and retry, because the stream might not be ready. For example, for HLS, the DVR process // If error, just ignore and retry, because the stream might not be ready. For example, for HLS, the DVR process
// might need to wait for a duration of segment, 10s as such. // might need to wait for a duration of segment, 10s as such.
_ = v.doDVR(ctx) _ = v.doDVR(ctx)
}
// Check whether DVR file is ok. // Check whether DVR file is ok.
if fs, err := os.Stat(v.dvrFile); err == nil && fs.Size() > 1024 { if fs, err := os.Stat(v.dvrFile); err == nil && fs.Size() > 1024 {
@ -738,9 +786,14 @@ func (v *ffprobeClient) Run(ctxCase context.Context, cancelCase context.CancelFu
break break
} }
// If not DVR by FFmpeg, must be by other tools, only need to wait.
if !v.dvrByFFmpeg {
logger.Tf(ctx, "Waiting stream=%v to be DVR", v.streamURL)
}
// Wait for a while and retry. Use larger timeout for HLS. // Wait for a while and retry. Use larger timeout for HLS.
retryTimeout := 1 * time.Second retryTimeout := 1 * time.Second
if strings.Contains(v.streamURL, ".m3u8") { if strings.Contains(v.streamURL, ".m3u8") || v.dvrFile == "" {
retryTimeout = 3 * time.Second retryTimeout = 3 * time.Second
} }
@ -763,6 +816,10 @@ func (v *ffprobeClient) Run(ctxCase context.Context, cancelCase context.CancelFu
func (v *ffprobeClient) doDVR(ctx context.Context) error { func (v *ffprobeClient) doDVR(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx) ctx, cancel := context.WithCancel(ctx)
if !v.dvrByFFmpeg {
return nil
}
process := newBackendService() process := newBackendService()
process.name = *srsFFmpeg process.name = *srsFFmpeg
process.args = []string{ process.args = []string{
@ -1070,3 +1127,167 @@ func (v *ffprobeObject) Audio() *ffprobeObjectMedia {
} }
return nil return nil
} }
type HooksEvent interface {
HookAction() string
}
type HooksEventBase struct {
Action string `json:"action"`
}
func (v *HooksEventBase) HookAction() string {
return v.Action
}
type HooksEventOnDvr struct {
HooksEventBase
Stream string `json:"stream"`
StreamUrl string `json:"stream_url"`
StreamID string `json:"stream_id"`
CWD string `json:"cwd"`
File string `json:"file"`
TcUrl string `json:"tcUrl"`
App string `json:"app"`
Vhost string `json:"vhost"`
IP string `json:"ip"`
ClientIP string `json:"client_id"`
ServerID string `json:"server_id"`
}
type HooksService interface {
ServiceRunner
ServiceReadyQuerier
HooksAPI() int
HooksEvents() <-chan HooksEvent
}
type hooksService struct {
readyCtx context.Context
readyCancel context.CancelFunc
httpPort int
dispose func()
r0 error
hooksOnDvr chan HooksEvent
}
func NewHooksService(opts ...func(v *hooksService)) HooksService {
v := &hooksService{}
v.httpPort = allocator.Allocate()
v.dispose = func() {
allocator.Free(v.httpPort)
close(v.hooksOnDvr)
}
v.hooksOnDvr = make(chan HooksEvent, 64)
v.readyCtx, v.readyCancel = context.WithCancel(context.Background())
for _, opt := range opts {
opt(v)
}
return v
}
func (v *hooksService) ReadyCtx() context.Context {
return v.readyCtx
}
func (v *hooksService) HooksAPI() int {
return v.httpPort
}
func (v *hooksService) HooksEvents() <-chan HooksEvent {
return v.hooksOnDvr
}
func (v *hooksService) Run(ctx context.Context, cancel context.CancelFunc) error {
defer func() {
v.readyCancel()
v.dispose()
}()
handler := http.ServeMux{}
handler.HandleFunc("/api/v1/ping", func(w http.ResponseWriter, r *http.Request) {
ohttp.WriteData(ctx, w, r, "pong")
})
handler.HandleFunc("/api/v1/dvrs", func(w http.ResponseWriter, r *http.Request) {
b, err := ioutil.ReadAll(r.Body)
if err != nil {
ohttp.WriteError(ctx, w, r, err)
return
}
evt := HooksEventOnDvr{}
if err := json.Unmarshal(b, &evt); err != nil {
ohttp.WriteError(ctx, w, r, err)
return
}
select {
case <-ctx.Done():
case v.hooksOnDvr <- &evt:
}
logger.Tf(ctx, "Callback: Got on_dvr request %v", string(b))
ohttp.WriteData(ctx, w, r, nil)
})
server := &http.Server{Addr: fmt.Sprintf(":%v", v.httpPort), Handler: &handler}
var wg sync.WaitGroup
defer wg.Wait()
wg.Add(1)
go func() {
defer wg.Done()
logger.Tf(ctx, "Callback: Start hooks server, listen=%v", v.httpPort)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Wf(ctx, "Callback: Service listen=%v, err %v", v.httpPort, err)
v.r0 = errors.Wrapf(err, "server listen=%v", v.httpPort)
cancel()
return
}
logger.Tf(ctx, "Callback: Hooks done, listen=%v", v.httpPort)
}()
wg.Add(1)
go func() {
defer wg.Done()
<-ctx.Done()
go server.Shutdown(context.Background())
}()
wg.Add(1)
go func() {
defer wg.Done()
for ctx.Err() == nil {
time.Sleep(100 * time.Millisecond)
r := fmt.Sprintf("http://localhost:%v/api/v1/ping", v.httpPort)
res, err := http.Get(r)
if err != nil {
continue
}
defer res.Body.Close()
b, err := ioutil.ReadAll(res.Body)
if err != nil {
continue
}
logger.Tf(ctx, "Callback: API is ready, %v %v", r, string(b))
v.readyCancel()
return
}
}()
wg.Wait()
return v.r0
}

View file

@ -12,6 +12,7 @@ require (
github.com/pion/sdp/v3 v3.0.4 github.com/pion/sdp/v3 v3.0.4
github.com/pion/transport v0.12.2 github.com/pion/transport v0.12.2
github.com/pion/webrtc/v3 v3.0.13 github.com/pion/webrtc/v3 v3.0.13
github.com/pkg/errors v0.9.1
github.com/yapingcat/gomedia/codec v0.0.0-20220617074658-94762898dc25 github.com/yapingcat/gomedia/codec v0.0.0-20220617074658-94762898dc25
github.com/yapingcat/gomedia/mpeg2 v0.0.0-20220617074658-94762898dc25 github.com/yapingcat/gomedia/mpeg2 v0.0.0-20220617074658-94762898dc25
) )

View file

@ -11,9 +11,7 @@ type InvalidStartLineError string
func (err InvalidStartLineError) Syntax() bool { return true } func (err InvalidStartLineError) Syntax() bool { return true }
func (err InvalidStartLineError) Malformed() bool { return false } func (err InvalidStartLineError) Malformed() bool { return false }
func (err InvalidStartLineError) Broken() bool { return true } func (err InvalidStartLineError) Broken() bool { return true }
func (err InvalidStartLineError) Error() string { func (err InvalidStartLineError) Error() string { return "parser.InvalidStartLineError: " + string(err) }
return "parser.InvalidStartLineError: " + string(err)
}
type InvalidMessageFormat string type InvalidMessageFormat string

View file

@ -0,0 +1,87 @@
// The MIT License (MIT)
//
// Copyright (c) 2013-2017 Oryx(ossrs)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// The oryx http package, the response parse service.
package http
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
// Read http api by HTTP GET and parse the code/data.
func ApiRequest(url string) (code int, body []byte, err error) {
if body, err = apiGet(url); err != nil {
return
}
if code, _, err = apiParse(url, body); err != nil {
return
}
return
}
// Read http api by HTTP GET.
func apiGet(url string) (body []byte, err error) {
var resp *http.Response
if resp, err = http.Get(url); err != nil {
err = fmt.Errorf("api get failed, url=%v, err is %v", url, err)
return
}
defer resp.Body.Close()
if body, err = ioutil.ReadAll(resp.Body); err != nil {
err = fmt.Errorf("api read failed, url=%v, err is %v", url, err)
return
}
return
}
// Parse the standard response {code:int,data:object}.
func apiParse(url string, body []byte) (code int, data interface{}, err error) {
obj := make(map[string]interface{})
if err = json.Unmarshal(body, &obj); err != nil {
err = fmt.Errorf("api parse failed, url=%v, body=%v, err is %v", url, string(body), err)
return
}
if value, ok := obj["code"]; !ok {
err = fmt.Errorf("api no code, url=%v, body=%v", url, string(body))
return
} else if value, ok := value.(float64); !ok {
err = fmt.Errorf("api code not number, code=%v, url=%v, body=%v", value, url, string(body))
return
} else {
code = int(value)
}
data, _ = obj["data"]
if code != 0 {
err = fmt.Errorf("api error, code=%v, url=%v, body=%v, data=%v", code, url, string(body), data)
return
}
return
}

View file

@ -0,0 +1,274 @@
// The MIT License (MIT)
//
// Copyright (c) 2013-2017 Oryx(ossrs)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
// The oryx http package provides standard request and response in json.
// Error, when error, use this handler.
// CplxError, for complex error, use this handler.
// Data, when no error, use this handler.
// SystemError, application level error code.
// SetHeader, for direclty response the raw stream.
// The standard server response:
// code, an int error code.
// data, specifies the data.
// The api for simple api:
// WriteVersion, to directly response the version.
// WriteData, to directly write the data in json.
// WriteError, to directly write the error.
// WriteCplxError, to directly write the complex error.
// The global variables:
// oh.Server, to set the response header["Server"].
package http
import (
"encoding/json"
"fmt"
ol "github.com/ossrs/go-oryx-lib/logger"
"net/http"
"os"
"strconv"
"strings"
)
// header["Content-Type"] in response.
const (
HttpJson = "application/json"
HttpJavaScript = "application/javascript"
)
// header["Server"] in response.
var Server = "Oryx"
// system int error.
type SystemError int
func (v SystemError) Error() string {
return fmt.Sprintf("System error=%d", int(v))
}
// system conplex error.
type SystemComplexError struct {
// the system error code.
Code SystemError `json:"code"`
// the description for this error.
Message string `json:"data"`
}
func (v SystemComplexError) Error() string {
return fmt.Sprintf("%v, %v", v.Code.Error(), v.Message)
}
// application level, with code.
type AppError interface {
Code() int
error
}
// HTTP Status Code
type HTTPStatus interface {
Status() int
}
// http standard error response.
// @remark for not SystemError, we will use logger.E to print it.
// @remark user can use WriteError() for simple api.
func Error(ctx ol.Context, err error) http.Handler {
// for complex error, use code instead.
if v, ok := err.(SystemComplexError); ok {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jsonHandler(ctx, FilterCplxSystemError(ctx, w, r, v)).ServeHTTP(w, r)
})
}
// for int error, use code instead.
if v, ok := err.(SystemError); ok {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jsonHandler(ctx, FilterSystemError(ctx, w, r, v)).ServeHTTP(w, r)
})
}
// for application error, use code instead.
if v, ok := err.(AppError); ok {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jsonHandler(ctx, FilterAppError(ctx, w, r, v)).ServeHTTP(w, r)
})
}
// unknown error, log and response detail
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
SetHeader(w)
w.Header().Set("Content-Type", HttpJson)
status := http.StatusInternalServerError
if v, ok := err.(HTTPStatus); ok {
status = v.Status()
}
http.Error(w, FilterError(ctx, w, r, err), status)
})
}
// Wrapper for complex error use Error(ctx, SystemComplexError{})
// @remark user can use WriteCplxError() for simple api.
func CplxError(ctx ol.Context, code SystemError, message string) http.Handler {
return Error(ctx, SystemComplexError{code, message})
}
// http normal response.
// @remark user can use nil v to response success, which data is null.
// @remark user can use WriteData() for simple api.
func Data(ctx ol.Context, v interface{}) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
jsonHandler(ctx, FilterData(ctx, w, r, v)).ServeHTTP(w, r)
})
}
// set http header, for directly use the w,
// for example, user want to directly write raw text.
func SetHeader(w http.ResponseWriter) {
w.Header().Set("Server", Server)
}
// response json directly.
func jsonHandler(ctx ol.Context, rv interface{}) http.Handler {
var err error
var b []byte
if b, err = json.Marshal(rv); err != nil {
return Error(ctx, err)
}
status := http.StatusOK
if v, ok := rv.(HTTPStatus); ok {
status = v.Status()
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
SetHeader(w)
q := r.URL.Query()
if cb := q.Get("callback"); cb != "" {
w.Header().Set("Content-Type", HttpJavaScript)
if status != http.StatusOK {
w.WriteHeader(status)
}
// TODO: Handle error.
fmt.Fprintf(w, "%s(%s)", cb, string(b))
} else {
w.Header().Set("Content-Type", HttpJson)
if status != http.StatusOK {
w.WriteHeader(status)
}
// TODO: Handle error.
w.Write(b)
}
})
}
// response the standard version info:
// {code, server, data} where server is the server pid, and data is below object:
// {major, minor, revision, extra, version, signature}
// @param version in {major.minor.revision-extra}, where -extra is optional,
// for example: 1.0.0 or 1.0.0-0 or 1.0.0-1
func WriteVersion(w http.ResponseWriter, r *http.Request, version string) {
var major, minor, revision, extra int
versions := strings.Split(version, "-")
if len(versions) > 1 {
extra, _ = strconv.Atoi(versions[1])
}
versions = strings.Split(versions[0], ".")
if len(versions) > 0 {
major, _ = strconv.Atoi(versions[0])
}
if len(versions) > 1 {
minor, _ = strconv.Atoi(versions[1])
}
if len(versions) > 2 {
revision, _ = strconv.Atoi(versions[2])
}
Data(nil, map[string]interface{}{
"major": major,
"minor": minor,
"revision": revision,
"extra": extra,
"version": version,
"signature": Server,
}).ServeHTTP(w, r)
}
// Directly write json data, a wrapper for Data().
// @remark user can use Data() for group of complex apis.
func WriteData(ctx ol.Context, w http.ResponseWriter, r *http.Request, v interface{}) {
Data(ctx, v).ServeHTTP(w, r)
}
// Directly write success json response, same to WriteData(ctx, w, r, nil).
func Success(ctx ol.Context, w http.ResponseWriter, r *http.Request) {
WriteData(ctx, w, r, nil)
}
// Directly write error, a wrapper for Error().
// @remark user can use Error() for group of complex apis.
func WriteError(ctx ol.Context, w http.ResponseWriter, r *http.Request, err error) {
Error(ctx, err).ServeHTTP(w, r)
}
// Directly write complex error, a wrappter for CplxError().
// @remark user can use CplxError() for group of complex apis.
func WriteCplxError(ctx ol.Context, w http.ResponseWriter, r *http.Request, code SystemError, message string) {
CplxError(ctx, code, message).ServeHTTP(w, r)
}
// for hijack to define the response structure.
// user can redefine these functions for special response.
var FilterCplxSystemError = func(ctx ol.Context, w http.ResponseWriter, r *http.Request, o SystemComplexError) interface{} {
ol.Ef(ctx, "Serve %v failed, err is %+v", r.URL, o)
return o
}
var FilterSystemError = func(ctx ol.Context, w http.ResponseWriter, r *http.Request, o SystemError) interface{} {
ol.Ef(ctx, "Serve %v failed, err is %+v", r.URL, o)
return map[string]int{"code": int(o)}
}
var FilterAppError = func(ctx ol.Context, w http.ResponseWriter, r *http.Request, err AppError) interface{} {
ol.Ef(ctx, "Serve %v failed, err is %+v", r.URL, err)
return map[string]interface{}{"code": err.Code(), "data": err.Error()}
}
var FilterError = func(ctx ol.Context, w http.ResponseWriter, r *http.Request, err error) string {
ol.Ef(ctx, "Serve %v failed, err is %+v", r.URL, err)
return err.Error()
}
var FilterData = func(ctx ol.Context, w http.ResponseWriter, r *http.Request, o interface{}) interface{} {
rv := map[string]interface{}{
"code": 0,
"server": os.Getpid(),
"data": o,
}
// for string, directly use it without convert,
// for the type covert by golang maybe modify the content.
if v, ok := o.(string); ok {
rv["data"] = v
}
return rv
}

View file

@ -10,3 +10,4 @@ func isTerminal(fd int) bool {
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios) _, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
return err == nil return err == nil
} }

View file

@ -10,3 +10,4 @@ func isTerminal(fd int) bool {
_, err := unix.IoctlGetTermios(fd, ioctlReadTermios) _, err := unix.IoctlGetTermios(fd, ioctlReadTermios)
return err == nil return err == nil
} }

View file

@ -11,8 +11,8 @@ import (
"sync" "sync"
"time" "time"
"github.com/mgutz/ansi"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/mgutz/ansi"
"golang.org/x/crypto/ssh/terminal" "golang.org/x/crypto/ssh/terminal"
) )

View file

@ -33,6 +33,7 @@ github.com/ossrs/go-oryx-lib/amf0
github.com/ossrs/go-oryx-lib/avc github.com/ossrs/go-oryx-lib/avc
github.com/ossrs/go-oryx-lib/errors github.com/ossrs/go-oryx-lib/errors
github.com/ossrs/go-oryx-lib/flv github.com/ossrs/go-oryx-lib/flv
github.com/ossrs/go-oryx-lib/http
github.com/ossrs/go-oryx-lib/logger github.com/ossrs/go-oryx-lib/logger
github.com/ossrs/go-oryx-lib/rtmp github.com/ossrs/go-oryx-lib/rtmp
# github.com/pion/datachannel v1.4.21 # github.com/pion/datachannel v1.4.21
@ -116,6 +117,7 @@ github.com/pion/webrtc/v3/pkg/media/oggreader
github.com/pion/webrtc/v3/pkg/media/oggwriter github.com/pion/webrtc/v3/pkg/media/oggwriter
github.com/pion/webrtc/v3/pkg/rtcerr github.com/pion/webrtc/v3/pkg/rtcerr
# github.com/pkg/errors v0.9.1 # github.com/pkg/errors v0.9.1
## explicit
github.com/pkg/errors github.com/pkg/errors
# github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b # github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b
github.com/satori/go.uuid github.com/satori/go.uuid

View file

@ -1515,6 +1515,7 @@ vhost http.remux.srs.com {
vhost hooks.callback.srs.com { vhost hooks.callback.srs.com {
http_hooks { http_hooks {
# whether the http hooks enable. # whether the http hooks enable.
# Overwrite by env SRS_VHOST_HTTP_HOOKS_ENABLED for all vhosts.
# default off. # default off.
enabled on; enabled on;
# when client(encoder) publish to vhost/app/stream, call the hook, # when client(encoder) publish to vhost/app/stream, call the hook,
@ -1533,6 +1534,7 @@ vhost hooks.callback.srs.com {
# on_publish http://xxx/api0 http://xxx/api1 http://xxx/apiN # on_publish http://xxx/api0 http://xxx/api1 http://xxx/apiN
# @remark For SRS4, the HTTPS url is supported, for example: # @remark For SRS4, the HTTPS url is supported, for example:
# on_publish https://xxx/api0 https://xxx/api1 https://xxx/apiN # on_publish https://xxx/api0 https://xxx/api1 https://xxx/apiN
# Overwrite by env SRS_VHOST_HTTP_HOOKS_ON_PUBLISH for all vhosts.
on_publish http://127.0.0.1:8085/api/v1/streams http://localhost:8085/api/v1/streams; on_publish http://127.0.0.1:8085/api/v1/streams http://localhost:8085/api/v1/streams;
# when client(encoder) stop publish to vhost/app/stream, call the hook, # when client(encoder) stop publish to vhost/app/stream, call the hook,
# the request in the POST data string is a object encode by json: # the request in the POST data string is a object encode by json:
@ -1550,6 +1552,7 @@ vhost hooks.callback.srs.com {
# on_unpublish http://xxx/api0 http://xxx/api1 http://xxx/apiN # on_unpublish http://xxx/api0 http://xxx/api1 http://xxx/apiN
# @remark For SRS4, the HTTPS url is supported, for example: # @remark For SRS4, the HTTPS url is supported, for example:
# on_unpublish https://xxx/api0 https://xxx/api1 https://xxx/apiN # on_unpublish https://xxx/api0 https://xxx/api1 https://xxx/apiN
# Overwrite by env SRS_VHOST_HTTP_HOOKS_ON_UNPUBLISH for all vhosts.
on_unpublish http://127.0.0.1:8085/api/v1/streams http://localhost:8085/api/v1/streams; on_unpublish http://127.0.0.1:8085/api/v1/streams http://localhost:8085/api/v1/streams;
# when client start to play vhost/app/stream, call the hook, # when client start to play vhost/app/stream, call the hook,
# the request in the POST data string is a object encode by json: # the request in the POST data string is a object encode by json:
@ -1568,6 +1571,7 @@ vhost hooks.callback.srs.com {
# on_play http://xxx/api0 http://xxx/api1 http://xxx/apiN # on_play http://xxx/api0 http://xxx/api1 http://xxx/apiN
# @remark For SRS4, the HTTPS url is supported, for example: # @remark For SRS4, the HTTPS url is supported, for example:
# on_play https://xxx/api0 https://xxx/api1 https://xxx/apiN # on_play https://xxx/api0 https://xxx/api1 https://xxx/apiN
# Overwrite by env SRS_VHOST_HTTP_HOOKS_ON_PLAY for all vhosts.
on_play http://127.0.0.1:8085/api/v1/sessions http://localhost:8085/api/v1/sessions; on_play http://127.0.0.1:8085/api/v1/sessions http://localhost:8085/api/v1/sessions;
# when client stop to play vhost/app/stream, call the hook, # when client stop to play vhost/app/stream, call the hook,
# the request in the POST data string is a object encode by json: # the request in the POST data string is a object encode by json:
@ -1585,6 +1589,7 @@ vhost hooks.callback.srs.com {
# on_stop http://xxx/api0 http://xxx/api1 http://xxx/apiN # on_stop http://xxx/api0 http://xxx/api1 http://xxx/apiN
# @remark For SRS4, the HTTPS url is supported, for example: # @remark For SRS4, the HTTPS url is supported, for example:
# on_stop https://xxx/api0 https://xxx/api1 https://xxx/apiN # on_stop https://xxx/api0 https://xxx/api1 https://xxx/apiN
# Overwrite by env SRS_VHOST_HTTP_HOOKS_ON_STOP for all vhosts.
on_stop http://127.0.0.1:8085/api/v1/sessions http://localhost:8085/api/v1/sessions; on_stop http://127.0.0.1:8085/api/v1/sessions http://localhost:8085/api/v1/sessions;
# when srs reap a dvr file, call the hook, # when srs reap a dvr file, call the hook,
# the request in the POST data string is a object encode by json: # the request in the POST data string is a object encode by json:
@ -1600,6 +1605,7 @@ vhost hooks.callback.srs.com {
# if valid, the hook must return HTTP code 200(Status OK) and response # if valid, the hook must return HTTP code 200(Status OK) and response
# an int value specifies the error code(0 corresponding to success): # an int value specifies the error code(0 corresponding to success):
# 0 # 0
# Overwrite by env SRS_VHOST_HTTP_HOOKS_ON_DVR for all vhosts.
on_dvr http://127.0.0.1:8085/api/v1/dvrs http://localhost:8085/api/v1/dvrs; on_dvr http://127.0.0.1:8085/api/v1/dvrs http://localhost:8085/api/v1/dvrs;
# when srs reap a ts file of hls, call the hook, # when srs reap a ts file of hls, call the hook,
# the request in the POST data string is a object encode by json: # the request in the POST data string is a object encode by json:
@ -1620,6 +1626,7 @@ vhost hooks.callback.srs.com {
# if valid, the hook must return HTTP code 200(Status OK) and response # if valid, the hook must return HTTP code 200(Status OK) and response
# an int value specifies the error code(0 corresponding to success): # an int value specifies the error code(0 corresponding to success):
# 0 # 0
# Overwrite by env SRS_VHOST_HTTP_HOOKS_ON_HLS for all vhosts.
on_hls http://127.0.0.1:8085/api/v1/hls http://localhost:8085/api/v1/hls; on_hls http://127.0.0.1:8085/api/v1/hls http://localhost:8085/api/v1/hls;
# when srs reap a ts file of hls, call this hook, # when srs reap a ts file of hls, call this hook,
# used to push file to cdn network, by get the ts file from cdn network. # used to push file to cdn network, by get the ts file from cdn network.
@ -1631,6 +1638,7 @@ vhost hooks.callback.srs.com {
# [ts_url], replace with the ts url. # [ts_url], replace with the ts url.
# ignore any return data of server. # ignore any return data of server.
# @remark random select a url to report, not report all. # @remark random select a url to report, not report all.
# Overwrite by env SRS_VHOST_HTTP_HOOKS_ON_HLS_NOTIFY for all vhosts.
on_hls_notify http://127.0.0.1:8085/api/v1/hls/[server_id]/[app]/[stream]/[ts_url][param]; on_hls_notify http://127.0.0.1:8085/api/v1/hls/[server_id]/[app]/[stream]/[ts_url][param];
} }
} }

View file

@ -27,7 +27,8 @@ The changelog for SRS.
## SRS 5.0 Changelog ## SRS 5.0 Changelog
* v5.0, 2023-01-05, FFmpeg: Support build with FFmpeg native opus. v5.0.131 (#3140) * v5.0, 2023-01-06, DVR: Support blackbox test based on hooks. v5.0.132
* v5.0, 2023-01-06, FFmpeg: Support build with FFmpeg native opus. v5.0.131 (#3140)
* v5.0, 2023-01-05, CORS: Refine HTTP CORS headers. v5.0.130 * v5.0, 2023-01-05, CORS: Refine HTTP CORS headers. v5.0.130
* v5.0, 2023-01-03, Add blackbox test for HLS and MP3 codec. v5.0.129 * v5.0, 2023-01-03, Add blackbox test for HLS and MP3 codec. v5.0.129
* v5.0, 2023-01-02, Merge [#3355](https://github.com/ossrs/srs/pull/3355): Test: Support blackbox test by FFmpeg. v5.0.128 * v5.0, 2023-01-02, Merge [#3355](https://github.com/ossrs/srs/pull/3355): Test: Support blackbox test by FFmpeg. v5.0.128

View file

@ -68,6 +68,16 @@ const char* _srs_version = "XCORE-" RTMP_SIG_SRS_SERVER;
#define SRS_OVERWRITE_BY_ENV_MILLISECONDS(key) if (!srs_getenv(key).empty()) return (srs_utime_t)(::atoi(srs_getenv(key).c_str()) * SRS_UTIME_MILLISECONDS) #define SRS_OVERWRITE_BY_ENV_MILLISECONDS(key) if (!srs_getenv(key).empty()) return (srs_utime_t)(::atoi(srs_getenv(key).c_str()) * SRS_UTIME_MILLISECONDS)
#define SRS_OVERWRITE_BY_ENV_FLOAT_SECONDS(key) if (!srs_getenv(key).empty()) return srs_utime_t(::atof(srs_getenv(key).c_str()) * SRS_UTIME_SECONDS) #define SRS_OVERWRITE_BY_ENV_FLOAT_SECONDS(key) if (!srs_getenv(key).empty()) return srs_utime_t(::atof(srs_getenv(key).c_str()) * SRS_UTIME_SECONDS)
#define SRS_OVERWRITE_BY_ENV_FLOAT_MILLISECONDS(key) if (!srs_getenv(key).empty()) return srs_utime_t(::atof(srs_getenv(key).c_str()) * SRS_UTIME_MILLISECONDS) #define SRS_OVERWRITE_BY_ENV_FLOAT_MILLISECONDS(key) if (!srs_getenv(key).empty()) return srs_utime_t(::atof(srs_getenv(key).c_str()) * SRS_UTIME_MILLISECONDS)
#define SRS_OVERWRITE_BY_ENV_DIRECTIVE(key) { \
static SrsConfDirective* dir = NULL; \
if (!dir && !srs_getenv(key).empty()) { \
string v = srs_getenv(key); \
dir = new SrsConfDirective(); \
dir->name = key; \
dir->args.push_back(v); \
} \
if (dir) return dir; \
}
/** /**
* dumps the ingest/transcode-engine in @param dir to amf0 object @param engine. * dumps the ingest/transcode-engine in @param dir to amf0 object @param engine.
@ -5359,6 +5369,8 @@ SrsConfDirective* SrsConfig::get_vhost_http_hooks(string vhost)
bool SrsConfig::get_vhost_http_hooks_enabled(string vhost) bool SrsConfig::get_vhost_http_hooks_enabled(string vhost)
{ {
SRS_OVERWRITE_BY_ENV_BOOL("srs.vhost.http_hooks.enabled"); // SRS_VHOST_HTTP_HOOKS_ENABLED
static bool DEFAULT = false; static bool DEFAULT = false;
SrsConfDirective* conf = get_vhost(vhost); SrsConfDirective* conf = get_vhost(vhost);
@ -5371,6 +5383,8 @@ bool SrsConfig::get_vhost_http_hooks_enabled(string vhost)
bool SrsConfig::get_vhost_http_hooks_enabled(SrsConfDirective* vhost) bool SrsConfig::get_vhost_http_hooks_enabled(SrsConfDirective* vhost)
{ {
SRS_OVERWRITE_BY_ENV_BOOL("srs.vhost.http_hooks.enabled"); // SRS_VHOST_HTTP_HOOKS_ENABLED
static bool DEFAULT = false; static bool DEFAULT = false;
SrsConfDirective* conf = vhost->get("http_hooks"); SrsConfDirective* conf = vhost->get("http_hooks");
@ -5388,6 +5402,8 @@ bool SrsConfig::get_vhost_http_hooks_enabled(SrsConfDirective* vhost)
SrsConfDirective* SrsConfig::get_vhost_on_connect(string vhost) SrsConfDirective* SrsConfig::get_vhost_on_connect(string vhost)
{ {
SRS_OVERWRITE_BY_ENV_DIRECTIVE("srs.vhost.http_hooks.on_connect"); // SRS_VHOST_HTTP_HOOKS_ON_CONNECT
SrsConfDirective* conf = get_vhost_http_hooks(vhost); SrsConfDirective* conf = get_vhost_http_hooks(vhost);
if (!conf) { if (!conf) {
return NULL; return NULL;
@ -5398,6 +5414,8 @@ SrsConfDirective* SrsConfig::get_vhost_on_connect(string vhost)
SrsConfDirective* SrsConfig::get_vhost_on_close(string vhost) SrsConfDirective* SrsConfig::get_vhost_on_close(string vhost)
{ {
SRS_OVERWRITE_BY_ENV_DIRECTIVE("srs.vhost.http_hooks.on_close"); // SRS_VHOST_HTTP_HOOKS_ON_CLOSE
SrsConfDirective* conf = get_vhost_http_hooks(vhost); SrsConfDirective* conf = get_vhost_http_hooks(vhost);
if (!conf) { if (!conf) {
return NULL; return NULL;
@ -5408,6 +5426,8 @@ SrsConfDirective* SrsConfig::get_vhost_on_close(string vhost)
SrsConfDirective* SrsConfig::get_vhost_on_publish(string vhost) SrsConfDirective* SrsConfig::get_vhost_on_publish(string vhost)
{ {
SRS_OVERWRITE_BY_ENV_DIRECTIVE("srs.vhost.http_hooks.on_publish"); // SRS_VHOST_HTTP_HOOKS_ON_PUBLISH
SrsConfDirective* conf = get_vhost_http_hooks(vhost); SrsConfDirective* conf = get_vhost_http_hooks(vhost);
if (!conf) { if (!conf) {
return NULL; return NULL;
@ -5418,6 +5438,8 @@ SrsConfDirective* SrsConfig::get_vhost_on_publish(string vhost)
SrsConfDirective* SrsConfig::get_vhost_on_unpublish(string vhost) SrsConfDirective* SrsConfig::get_vhost_on_unpublish(string vhost)
{ {
SRS_OVERWRITE_BY_ENV_DIRECTIVE("srs.vhost.http_hooks.on_unpublish"); // SRS_VHOST_HTTP_HOOKS_ON_UNPUBLISH
SrsConfDirective* conf = get_vhost_http_hooks(vhost); SrsConfDirective* conf = get_vhost_http_hooks(vhost);
if (!conf) { if (!conf) {
return NULL; return NULL;
@ -5428,6 +5450,8 @@ SrsConfDirective* SrsConfig::get_vhost_on_unpublish(string vhost)
SrsConfDirective* SrsConfig::get_vhost_on_play(string vhost) SrsConfDirective* SrsConfig::get_vhost_on_play(string vhost)
{ {
SRS_OVERWRITE_BY_ENV_DIRECTIVE("srs.vhost.http_hooks.on_play"); // SRS_VHOST_HTTP_HOOKS_ON_PLAY
SrsConfDirective* conf = get_vhost_http_hooks(vhost); SrsConfDirective* conf = get_vhost_http_hooks(vhost);
if (!conf) { if (!conf) {
return NULL; return NULL;
@ -5438,6 +5462,8 @@ SrsConfDirective* SrsConfig::get_vhost_on_play(string vhost)
SrsConfDirective* SrsConfig::get_vhost_on_stop(string vhost) SrsConfDirective* SrsConfig::get_vhost_on_stop(string vhost)
{ {
SRS_OVERWRITE_BY_ENV_DIRECTIVE("srs.vhost.http_hooks.on_stop"); // SRS_VHOST_HTTP_HOOKS_ON_STOP
SrsConfDirective* conf = get_vhost_http_hooks(vhost); SrsConfDirective* conf = get_vhost_http_hooks(vhost);
if (!conf) { if (!conf) {
return NULL; return NULL;
@ -5448,6 +5474,8 @@ SrsConfDirective* SrsConfig::get_vhost_on_stop(string vhost)
SrsConfDirective* SrsConfig::get_vhost_on_dvr(string vhost) SrsConfDirective* SrsConfig::get_vhost_on_dvr(string vhost)
{ {
SRS_OVERWRITE_BY_ENV_DIRECTIVE("srs.vhost.http_hooks.on_dvr"); // SRS_VHOST_HTTP_HOOKS_ON_DVR
SrsConfDirective* conf = get_vhost_http_hooks(vhost); SrsConfDirective* conf = get_vhost_http_hooks(vhost);
if (!conf) { if (!conf) {
return NULL; return NULL;
@ -5458,6 +5486,8 @@ SrsConfDirective* SrsConfig::get_vhost_on_dvr(string vhost)
SrsConfDirective* SrsConfig::get_vhost_on_hls(string vhost) SrsConfDirective* SrsConfig::get_vhost_on_hls(string vhost)
{ {
SRS_OVERWRITE_BY_ENV_DIRECTIVE("srs.vhost.http_hooks.on_hls"); // SRS_VHOST_HTTP_HOOKS_ON_HLS
SrsConfDirective* conf = get_vhost_http_hooks(vhost); SrsConfDirective* conf = get_vhost_http_hooks(vhost);
if (!conf) { if (!conf) {
return NULL; return NULL;
@ -5468,6 +5498,8 @@ SrsConfDirective* SrsConfig::get_vhost_on_hls(string vhost)
SrsConfDirective* SrsConfig::get_vhost_on_hls_notify(string vhost) SrsConfDirective* SrsConfig::get_vhost_on_hls_notify(string vhost)
{ {
SRS_OVERWRITE_BY_ENV_DIRECTIVE("srs.vhost.http_hooks.on_hls_notify"); // SRS_VHOST_HTTP_HOOKS_ON_HLS_NOTIFY
SrsConfDirective* conf = get_vhost_http_hooks(vhost); SrsConfDirective* conf = get_vhost_http_hooks(vhost);
if (!conf) { if (!conf) {
return NULL; return NULL;

View file

@ -9,6 +9,6 @@
#define VERSION_MAJOR 5 #define VERSION_MAJOR 5
#define VERSION_MINOR 0 #define VERSION_MINOR 0
#define VERSION_REVISION 131 #define VERSION_REVISION 132
#endif #endif

View file

@ -3937,8 +3937,6 @@ VOID TEST(ConfigMainTest, SrtServerTlpktDrop)
VOID TEST(ConfigEnvTest, CheckEnvValuesGlobal) VOID TEST(ConfigEnvTest, CheckEnvValuesGlobal)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4035,8 +4033,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesGlobal)
VOID TEST(ConfigEnvTest, CheckEnvValuesthreads) VOID TEST(ConfigEnvTest, CheckEnvValuesthreads)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4047,8 +4043,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesthreads)
VOID TEST(ConfigEnvTest, CheckEnvValuesRtmp) VOID TEST(ConfigEnvTest, CheckEnvValuesRtmp)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4082,8 +4076,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesRtmp)
VOID TEST(ConfigEnvTest, CheckEnvValuesHttpApi) VOID TEST(ConfigEnvTest, CheckEnvValuesHttpApi)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4126,8 +4118,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesHttpApi)
VOID TEST(ConfigEnvTest, CheckEnvValuesHttpServer) VOID TEST(ConfigEnvTest, CheckEnvValuesHttpServer)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4163,8 +4153,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesHttpServer)
VOID TEST(ConfigEnvTest, CheckEnvValuesSrtServer) VOID TEST(ConfigEnvTest, CheckEnvValuesSrtServer)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4220,8 +4208,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesSrtServer)
VOID TEST(ConfigEnvTest, CheckEnvValuesVhostSrt) VOID TEST(ConfigEnvTest, CheckEnvValuesVhostSrt)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4235,8 +4221,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesVhostSrt)
VOID TEST(ConfigEnvTest, CheckEnvValuesRtcServer) VOID TEST(ConfigEnvTest, CheckEnvValuesRtcServer)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4303,8 +4287,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesRtcServer)
VOID TEST(ConfigEnvTest, CheckEnvValuesVhostRtc) VOID TEST(ConfigEnvTest, CheckEnvValuesVhostRtc)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4362,8 +4344,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesVhostRtc)
VOID TEST(ConfigEnvTest, CheckEnvValuesVhostPlay) VOID TEST(ConfigEnvTest, CheckEnvValuesVhostPlay)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4433,8 +4413,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesVhostPlay)
VOID TEST(ConfigEnvTest, CheckEnvValuesVhostPublish) VOID TEST(ConfigEnvTest, CheckEnvValuesVhostPublish)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4460,8 +4438,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesVhostPublish)
VOID TEST(ConfigEnvTest, CheckEnvValuesCircuitBreaker) VOID TEST(ConfigEnvTest, CheckEnvValuesCircuitBreaker)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4490,8 +4466,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesCircuitBreaker)
VOID TEST(ConfigEnvTest, CheckEnvValuesTencentcloudCls) VOID TEST(ConfigEnvTest, CheckEnvValuesTencentcloudCls)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4539,8 +4513,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesTencentcloudCls)
VOID TEST(ConfigEnvTest, CheckEnvValuesTencentcloudApm) VOID TEST(ConfigEnvTest, CheckEnvValuesTencentcloudApm)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4566,8 +4538,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesTencentcloudApm)
VOID TEST(ConfigEnvTest, CheckEnvValuesExporter) VOID TEST(ConfigEnvTest, CheckEnvValuesExporter)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4587,8 +4557,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesExporter)
VOID TEST(ConfigEnvTest, CheckEnvValuesHeartbeat) VOID TEST(ConfigEnvTest, CheckEnvValuesHeartbeat)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4611,8 +4579,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesHeartbeat)
VOID TEST(ConfigEnvTest, CheckEnvValuesScope) VOID TEST(ConfigEnvTest, CheckEnvValuesScope)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4646,8 +4612,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesScope)
VOID TEST(ConfigEnvTest, CheckEnvValuesHttpStatic) VOID TEST(ConfigEnvTest, CheckEnvValuesHttpStatic)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4664,8 +4628,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesHttpStatic)
VOID TEST(ConfigEnvTest, CheckEnvValuesHttpRemux) VOID TEST(ConfigEnvTest, CheckEnvValuesHttpRemux)
{ {
srs_error_t err;
MockSrsConfig conf; MockSrsConfig conf;
if (true) { if (true) {
@ -4722,8 +4684,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesHttpRemux)
VOID TEST(ConfigEnvTest, CheckEnvValuesDash) VOID TEST(ConfigEnvTest, CheckEnvValuesDash)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4776,8 +4736,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesHds)
VOID TEST(ConfigEnvTest, CheckEnvValuesDvr) VOID TEST(ConfigEnvTest, CheckEnvValuesDvr)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4806,8 +4764,6 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesDvr)
VOID TEST(ConfigEnvTest, CheckEnvValuesHls) VOID TEST(ConfigEnvTest, CheckEnvValuesHls)
{ {
srs_error_t err;
if (true) { if (true) {
MockSrsConfig conf; MockSrsConfig conf;
@ -4887,3 +4843,70 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesHls)
EXPECT_FALSE(conf.get_vhost_hls_dts_directly("__defaultVhost__")); EXPECT_FALSE(conf.get_vhost_hls_dts_directly("__defaultVhost__"));
} }
} }
VOID TEST(ConfigEnvTest, CheckEnvValuesHooks)
{
MockSrsConfig conf;
if (true) {
SrsSetEnvConfig(hooks, "SRS_VHOST_HTTP_HOOKS_ENABLED", "on");
EXPECT_TRUE(conf.get_vhost_http_hooks_enabled("__defaultVhost__"));
}
if (true) {
SrsSetEnvConfig(hooks, "SRS_VHOST_HTTP_HOOKS_ON_PUBLISH", "http://server/api/publish");
SrsConfDirective* dir = conf.get_vhost_on_publish("__defaultVhost__");
ASSERT_TRUE(dir != NULL);
ASSERT_TRUE((int)dir->args.size() == 1);
ASSERT_STREQ("http://server/api/publish", dir->arg0().c_str());
}
if (true) {
SrsSetEnvConfig(hooks, "SRS_VHOST_HTTP_HOOKS_ON_UNPUBLISH", "http://server/api/unpublish");
SrsConfDirective* dir = conf.get_vhost_on_unpublish("__defaultVhost__");
ASSERT_TRUE(dir != NULL);
ASSERT_TRUE((int)dir->args.size() == 1);
ASSERT_STREQ("http://server/api/unpublish", dir->arg0().c_str());
}
if (true) {
SrsSetEnvConfig(hooks, "SRS_VHOST_HTTP_HOOKS_ON_PLAY", "http://server/api/play");
SrsConfDirective* dir = conf.get_vhost_on_play("__defaultVhost__");
ASSERT_TRUE(dir != NULL);
ASSERT_TRUE((int)dir->args.size() == 1);
ASSERT_STREQ("http://server/api/play", dir->arg0().c_str());
}
if (true) {
SrsSetEnvConfig(hooks, "SRS_VHOST_HTTP_HOOKS_ON_STOP", "http://server/api/stop");
SrsConfDirective* dir = conf.get_vhost_on_stop("__defaultVhost__");
ASSERT_TRUE(dir != NULL);
ASSERT_TRUE((int)dir->args.size() == 1);
ASSERT_STREQ("http://server/api/stop", dir->arg0().c_str());
}
if (true) {
SrsSetEnvConfig(hooks, "SRS_VHOST_HTTP_HOOKS_ON_DVR", "http://server/api/dvr");
SrsConfDirective* dir = conf.get_vhost_on_dvr("__defaultVhost__");
ASSERT_TRUE(dir != NULL);
ASSERT_TRUE((int)dir->args.size() == 1);
ASSERT_STREQ("http://server/api/dvr", dir->arg0().c_str());
}
if (true) {
SrsSetEnvConfig(hooks, "SRS_VHOST_HTTP_HOOKS_ON_HLS", "http://server/api/hls");
SrsConfDirective* dir = conf.get_vhost_on_hls("__defaultVhost__");
ASSERT_TRUE(dir != NULL);
ASSERT_TRUE((int)dir->args.size() == 1);
ASSERT_STREQ("http://server/api/hls", dir->arg0().c_str());
}
if (true) {
SrsSetEnvConfig(hooks, "SRS_VHOST_HTTP_HOOKS_ON_HLS_NOTIFY", "http://server/api/hls_notify");
SrsConfDirective* dir = conf.get_vhost_on_hls_notify("__defaultVhost__");
ASSERT_TRUE(dir != NULL);
ASSERT_TRUE((int)dir->args.size() == 1);
ASSERT_STREQ("http://server/api/hls_notify", dir->arg0().c_str());
}
}