From 3a1bbc8a7143c26862275fe9c031fc7dee830780 Mon Sep 17 00:00:00 2001 From: panda <542638787@qq.com> Date: Sat, 7 Oct 2023 10:50:48 +0800 Subject: [PATCH 1/7] vlive support stream function --- platform/utils.go | 3 + platform/virtual-live-stream.go | 80 ++++++++++++++++---- ui/src/pages/ScenarioVLive.js | 130 +++++++++++++++++++++++++++++++- ui/src/resources/locale.json | 4 +- 4 files changed, 199 insertions(+), 18 deletions(-) diff --git a/platform/utils.go b/platform/utils.go index a676d1fb..bc84bab3 100644 --- a/platform/utils.go +++ b/platform/utils.go @@ -295,6 +295,9 @@ const ( SRS_BEIAN = "SRS_BEIAN" SRS_HTTPS = "SRS_HTTPS" SRS_HTTPS_DOMAIN = "SRS_HTTPS_DOMAIN" + + SRS_SOURCE_TYPE_FILE = "file" + SRS_SOURCE_TYPE_STREAM = "stream" ) // Tencent cloud consts. diff --git a/platform/virtual-live-stream.go b/platform/virtual-live-stream.go index 106ebb37..ec7b7261 100644 --- a/platform/virtual-live-stream.go +++ b/platform/virtual-live-stream.go @@ -11,6 +11,7 @@ import ( "fmt" "io" "net/http" + "net/url" "os" "os/exec" "path" @@ -216,6 +217,45 @@ func (v *VLiveWorker) Handle(ctx context.Context, handler *http.ServeMux) error } }) + ep = "/terraform/v1/ffmpeg/vlive/streamUrl/" + logger.Tf(ctx, "Handle %v", ep) + handler.HandleFunc(ep, func(w http.ResponseWriter, r *http.Request) { + if err := func() error { + q := r.URL.Query() + qUrl := q.Get("url") + u, err := url.Parse(qUrl) + if err != nil { + return errors.Wrapf(err, "parse %v", qUrl) + } + // check url if valid rtmp or http-flv or https-flv or hls live url + if u.Scheme != "rtmp" && u.Scheme != "http" && u.Scheme != "https" { + return errors.Errorf("invalid url scheme %v", u.Scheme) + } + if u.Scheme == "http" || u.Scheme == "https" { + if u.Path == "" { + return errors.Errorf("url path %v empty", u.Path) + } + if !strings.HasSuffix(u.Path, ".flv") && !strings.HasSuffix(u.Path, ".m3u8") { + return errors.Errorf("invalid url path suffix %v", u.Path) + } + } + targetUUID := uuid.NewString() + ohttp.WriteData(ctx, w, r, &struct { + Name string `json:"name"` + UUID string `json:"uuid"` + Target string `json:"target"` + }{ + Name: path.Base(u.Path), + UUID: targetUUID, + Target: qUrl, + }) + logger.Tf(ctx, "vLive stream url ok, url=%v, uuid=%v", qUrl, targetUUID) + return nil + }(); err != nil { + ohttp.WriteError(ctx, w, r, err) + } + }) + ep = "/terraform/v1/ffmpeg/vlive/server/" logger.Tf(ctx, "Handle %v", ep) handler.HandleFunc(ep, func(w http.ResponseWriter, r *http.Request) { @@ -355,6 +395,7 @@ func (v *VLiveWorker) Handle(ctx context.Context, handler *http.ServeMux) error Size int64 `json:"size"` UUID string `json:"uuid"` Target string `json:"target"` + Type string `json:"type"` } var token, platform string @@ -381,7 +422,9 @@ func (v *VLiveWorker) Handle(ctx context.Context, handler *http.ServeMux) error // Always cleanup the files in upload. var tempFiles []string for _, f := range files { - tempFiles = append(tempFiles, f.Target) + if f.Type != SRS_SOURCE_TYPE_STREAM { + tempFiles = append(tempFiles, f.Target) + } } defer func() { for _, tempFile := range tempFiles { @@ -397,11 +440,13 @@ func (v *VLiveWorker) Handle(ctx context.Context, handler *http.ServeMux) error if f.Target == "" { return errors.New("no target") } - if _, err := os.Stat(f.Target); err != nil { - return errors.Wrapf(err, "no file %v", f.Target) - } - if !strings.HasPrefix(f.Target, dirUploadPath) { - return errors.Errorf("invalid target %v", f.Target) + if f.Type != SRS_SOURCE_TYPE_STREAM { + if _, err := os.Stat(f.Target); err != nil { + return errors.Wrapf(err, "no file %v", f.Target) + } + if !strings.HasPrefix(f.Target, dirUploadPath) { + return errors.Errorf("invalid target %v", f.Target) + } } } @@ -467,11 +512,15 @@ func (v *VLiveWorker) Handle(ctx context.Context, handler *http.ServeMux) error parsedFile := &VLiveSourceFile{ Name: file.Name, Size: uint64(file.Size), UUID: file.UUID, - Target: path.Join(dirVLivePath, fmt.Sprintf("%v%v", file.UUID, path.Ext(file.Target))), + Target: file.Target, + Type: file.Type, Format: &format.Format, Video: matchVideo, Audio: matchAudio, } - if err = os.Rename(file.Target, parsedFile.Target); err != nil { - return errors.Wrapf(err, "rename %v to %v", file.Target, parsedFile.Target) + if file.Type != SRS_SOURCE_TYPE_STREAM { + parsedFile.Target = path.Join(dirVLivePath, fmt.Sprintf("%v%v", file.UUID, path.Ext(file.Target))) + if err = os.Rename(file.Target, parsedFile.Target); err != nil { + return errors.Wrapf(err, "rename %v to %v", file.Target, parsedFile.Target) + } } parsedFiles = append(parsedFiles, parsedFile) @@ -490,8 +539,10 @@ func (v *VLiveWorker) Handle(ctx context.Context, handler *http.ServeMux) error // Remove old files. for _, f := range confObj.Files { - if _, err := os.Stat(f.Target); err == nil { - os.Remove(f.Target) + if f.Type != SRS_SOURCE_TYPE_STREAM { + if _, err := os.Stat(f.Target); err == nil { + os.Remove(f.Target) + } } } confObj.Files = parsedFiles @@ -696,6 +747,7 @@ type VLiveSourceFile struct { Size uint64 `json:"size"` UUID string `json:"uuid"` Target string `json:"target"` + Type string `json:"type"` Format *VLiveFileFormat `json:"format"` Video *VLiveFileVideo `json:"video"` Audio *VLiveFileAudio `json:"audio"` @@ -936,9 +988,11 @@ func (v *VLiveTask) doVLive(ctx context.Context, input *VLiveSourceFile) error { outputURL := fmt.Sprintf("%v%v", outputServer, v.config.Secret) // Start FFmpeg process. - args := []string{ - "-stream_loop", "-1", "-re", "-i", input.Target, "-c", "copy", "-f", "flv", outputURL, + args := []string{} + if input.Type != SRS_SOURCE_TYPE_STREAM { + args = append(args, "-stream_loop", "-1") } + args = append(args, "-re", "-i", input.Target, "-c", "copy", "-f", "flv", outputURL) cmd := exec.CommandContext(ctx, "ffmpeg", args...) stderr, err := cmd.StderrPipe() diff --git a/ui/src/pages/ScenarioVLive.js b/ui/src/pages/ScenarioVLive.js index 5ee88a58..79a541be 100644 --- a/ui/src/pages/ScenarioVLive.js +++ b/ui/src/pages/ScenarioVLive.js @@ -642,10 +642,15 @@ function VLiveFileList({files, onChangeFiles}) { function ChooseVideoSourceCn({platform, vLiveFiles, setVLiveFiles}) { const [checkType, setCheckType] = React.useState('upload'); + React.useEffect(() => { + if (vLiveFiles?.length && vLiveFiles[0].type === 'stream') { + setCheckType('stream'); + } + }, [vLiveFiles]); return (<> 视频源 - * 虚拟直播就是将视频源(文件)转换成直播流 + * 虚拟直播就是将视频源(文件/流)转换成直播流 setCheckType('upload')} /> @@ -668,11 +673,29 @@ function ChooseVideoSourceCn({platform, vLiveFiles, setVLiveFiles}) { } + + + setCheckType('stream')} + />   + * 流必须是 rtmp/http-flv/hls 格式 + + {checkType === 'stream' && + + + + } + ); } function ChooseVideoSourceEn({platform, vLiveFiles, setVLiveFiles}) { const [checkType, setCheckType] = React.useState('upload'); + React.useEffect(() => { + if (vLiveFiles?.length && vLiveFiles[0].type === 'stream') { + setCheckType('stream'); + } + }, [vLiveFiles]); return (<> Live Stream Source @@ -699,9 +722,100 @@ function ChooseVideoSourceEn({platform, vLiveFiles, setVLiveFiles}) { } + + + setCheckType('stream')} + />   + * The stream must be in rtmp/http-flv/hls format. + + {checkType === 'stream' && + + + + } + ); } +function VLiveStreamSelectorCn({platform, vLiveFiles, setVLiveFiles}) { + const handleError = useErrorHandler(); + const [inputStream, setInputStream] = React.useState(''); + + const checkStreamUrl = function() { + if (!inputStream) return alert('请输入流地址'); + // check stream url if valid. start with rtmp/http-flv/hls. + if (!inputStream.startsWith('rtmp://') && !inputStream.startsWith('http://') && !inputStream.startsWith('https://')) return alert('流地址必须是 rtmp/http-flv/hls 格式'); + const token = Token.load(); + axios.post(`/terraform/v1/ffmpeg/vlive/streamUrl?url=${inputStream}`).then(res => { + console.log(`检查流地址成功,${JSON.stringify(res.data.data)}`); + const streamObj = res.data.data; + const files = [{name: streamObj.name, size: 0, uuid: streamObj.uuid, target: streamObj.target, type: "stream"}]; + axios.post('/terraform/v1/ffmpeg/vlive/source', { + ...token, platform, files, + }).then(res => { + console.log(`更新虚拟直播源为流地址成功,${JSON.stringify(res.data.data)}`); + setVLiveFiles(res.data.data.files); + }).catch(handleError); + }).catch(handleError); + } + + return (<> + + {!vLiveFiles?.length && <> + + + setInputStream(e.target.value)} /> + + + + + + } + {vLiveFiles?.length && setVLiveFiles(null)}/>} + + ); +} + +function VLiveStreamSelectorEn({platform, vLiveFiles, setVLiveFiles}) { + const handleError = useErrorHandler(); + const [inputStream, setInputStream] = React.useState(''); + + const checkStreamUrl = function() { + if (!inputStream) return alert('Please input stream URL'); + // check stream url if valid. start with rtmp/http-flv/hls. + if (!inputStream.startsWith('rtmp://') && !inputStream.startsWith('http://') && !inputStream.startsWith('https://')) return alert('The stream must be in rtmp/http-flv/hls format.'); + const token = Token.load(); + axios.post(`/terraform/v1/ffmpeg/vlive/streamUrl?url=${inputStream}`).then(res => { + console.log(`Check stream url ok,${JSON.stringify(res.data.data)}`); + const streamObj = res.data.data; + const files = [{name: streamObj.name, size: 0, uuid: streamObj.uuid, target: streamObj.target, type: "stream"}]; + axios.post('/terraform/v1/ffmpeg/vlive/source', { + ...token, platform, files, + }).then(res => { + console.log(`Setup the virtual live stream ok,${JSON.stringify(res.data.data)}`); + setVLiveFiles(res.data.data.files); + }).catch(handleError); + }).catch(handleError); + } + + return (<> + + {!vLiveFiles?.length && <> + + + setInputStream(e.target.value)} /> + + + + + + } + {vLiveFiles?.length && setVLiveFiles(null)}/>} + + ) +} + function VLiveFileSelectorCn({platform, vLiveFiles, setVLiveFiles}) { const handleError = useErrorHandler(); const [inputFile, setInputFile] = React.useState(''); @@ -838,8 +952,18 @@ function VLiveFileFormatInfo({file}) { const f = file; if (!f?.format) return <>; return <> - {Number(f?.size/1024/1024).toFixed(1)}MB   - {Number(f?.format?.duration).toFixed(0)}s   + {f?.type !== 'stream' && + <> + File   + {Number(f?.size/1024/1024).toFixed(1)}MB   + {Number(f?.format?.duration).toFixed(0)}s   + + } + {f?.type === 'stream' && + <> + Stream   + + } {Number(f?.format?.bit_rate/1000).toFixed(1)}Kbps ; } diff --git a/ui/src/resources/locale.json b/ui/src/resources/locale.json index b574b317..95349292 100644 --- a/ui/src/resources/locale.json +++ b/ui/src/resources/locale.json @@ -207,7 +207,7 @@ "submit": "提交", "setOk": "设置成功", "upload": "上传文件", - "changeFiles": "更换文件" + "changeFiles": "更换文件/流" } } }, @@ -419,7 +419,7 @@ "submit": "Submit", "setOk": "Setup OK", "upload": "Upload File", - "changeFiles": "Change File" + "changeFiles": "Change File/Stream" } } } From b3b5a0ad9fd2356b79124dd294e6a3fb564c4237 Mon Sep 17 00:00:00 2001 From: panda <542638787@qq.com> Date: Sat, 7 Oct 2023 14:52:49 +0800 Subject: [PATCH 2/7] support rtsp, distinguish file type, add timeout for ffprobe --- platform/utils.go | 9 +++++--- platform/virtual-live-stream.go | 38 +++++++++++++++++---------------- ui/src/pages/ScenarioVLive.js | 30 +++++++++++++++----------- 3 files changed, 44 insertions(+), 33 deletions(-) diff --git a/platform/utils.go b/platform/utils.go index bc84bab3..ed53f997 100644 --- a/platform/utils.go +++ b/platform/utils.go @@ -295,9 +295,6 @@ const ( SRS_BEIAN = "SRS_BEIAN" SRS_HTTPS = "SRS_HTTPS" SRS_HTTPS_DOMAIN = "SRS_HTTPS_DOMAIN" - - SRS_SOURCE_TYPE_FILE = "file" - SRS_SOURCE_TYPE_STREAM = "stream" ) // Tencent cloud consts. @@ -306,6 +303,12 @@ const ( TENCENT_CLOUD_VOD_ENDPOINT = "vod.tencentcloudapi.com" ) +// SrsVLiveSourceType defines the source type of vLive. +type SrsVLiveSourceType string +const SrsVLiveSourceTypeUpload SrsVLiveSourceType = "upload" +const SrsVLiveSourceTypeFile SrsVLiveSourceType = "file" +const SrsVLiveSourceTypeStream SrsVLiveSourceType = "stream" + // For vLive upload directory. var dirUploadPath = path.Join(".", "upload") var dirVLivePath = path.Join(".", "vlive") diff --git a/platform/virtual-live-stream.go b/platform/virtual-live-stream.go index ec7b7261..d2bff790 100644 --- a/platform/virtual-live-stream.go +++ b/platform/virtual-live-stream.go @@ -227,15 +227,15 @@ func (v *VLiveWorker) Handle(ctx context.Context, handler *http.ServeMux) error if err != nil { return errors.Wrapf(err, "parse %v", qUrl) } - // check url if valid rtmp or http-flv or https-flv or hls live url - if u.Scheme != "rtmp" && u.Scheme != "http" && u.Scheme != "https" { + // check url if valid rtmp or rtsp or http-flv or https-flv or hls live url + if u.Scheme != "rtmp" && u.Scheme != "rtsp" && u.Scheme != "http" && u.Scheme != "https" { return errors.Errorf("invalid url scheme %v", u.Scheme) } if u.Scheme == "http" || u.Scheme == "https" { if u.Path == "" { return errors.Errorf("url path %v empty", u.Path) } - if !strings.HasSuffix(u.Path, ".flv") && !strings.HasSuffix(u.Path, ".m3u8") { + if !strings.HasSuffix(u.Path, ".flv") && !strings.HasSuffix(u.Path, ".m3u8") && !strings.HasSuffix(u.Path, ".ts") { return errors.Errorf("invalid url path suffix %v", u.Path) } } @@ -395,7 +395,7 @@ func (v *VLiveWorker) Handle(ctx context.Context, handler *http.ServeMux) error Size int64 `json:"size"` UUID string `json:"uuid"` Target string `json:"target"` - Type string `json:"type"` + Type SrsVLiveSourceType `json:"type"` } var token, platform string @@ -422,7 +422,7 @@ func (v *VLiveWorker) Handle(ctx context.Context, handler *http.ServeMux) error // Always cleanup the files in upload. var tempFiles []string for _, f := range files { - if f.Type != SRS_SOURCE_TYPE_STREAM { + if f.Type != SrsVLiveSourceTypeStream { tempFiles = append(tempFiles, f.Target) } } @@ -440,7 +440,7 @@ func (v *VLiveWorker) Handle(ctx context.Context, handler *http.ServeMux) error if f.Target == "" { return errors.New("no target") } - if f.Type != SRS_SOURCE_TYPE_STREAM { + if f.Type != SrsVLiveSourceTypeStream { if _, err := os.Stat(f.Target); err != nil { return errors.Wrapf(err, "no file %v", f.Target) } @@ -465,7 +465,9 @@ func (v *VLiveWorker) Handle(ctx context.Context, handler *http.ServeMux) error // Parse file information and move file from dirUploadPath to dirVLivePath. for _, file := range files { // Probe file information. - stdout, err := exec.CommandContext(ctx, "ffprobe", + toCtx, toCancelFunc := context.WithTimeout(ctx, 15*time.Second) + defer toCancelFunc() + stdout, err := exec.CommandContext(toCtx, "ffprobe", "-show_error", "-show_private_data", "-v", "quiet", "-find_stream_info", "-print_format", "json", "-show_format", "-show_streams", file.Target, ).Output() @@ -516,7 +518,7 @@ func (v *VLiveWorker) Handle(ctx context.Context, handler *http.ServeMux) error Type: file.Type, Format: &format.Format, Video: matchVideo, Audio: matchAudio, } - if file.Type != SRS_SOURCE_TYPE_STREAM { + if file.Type != SrsVLiveSourceTypeStream { parsedFile.Target = path.Join(dirVLivePath, fmt.Sprintf("%v%v", file.UUID, path.Ext(file.Target))) if err = os.Rename(file.Target, parsedFile.Target); err != nil { return errors.Wrapf(err, "rename %v to %v", file.Target, parsedFile.Target) @@ -539,7 +541,7 @@ func (v *VLiveWorker) Handle(ctx context.Context, handler *http.ServeMux) error // Remove old files. for _, f := range confObj.Files { - if f.Type != SRS_SOURCE_TYPE_STREAM { + if f.Type != SrsVLiveSourceTypeStream { if _, err := os.Stat(f.Target); err == nil { os.Remove(f.Target) } @@ -743,14 +745,14 @@ func (v *VLiveFileAudio) String() string { } type VLiveSourceFile struct { - Name string `json:"name"` - Size uint64 `json:"size"` - UUID string `json:"uuid"` - Target string `json:"target"` - Type string `json:"type"` - Format *VLiveFileFormat `json:"format"` - Video *VLiveFileVideo `json:"video"` - Audio *VLiveFileAudio `json:"audio"` + Name string `json:"name"` + Size uint64 `json:"size"` + UUID string `json:"uuid"` + Target string `json:"target"` + Type SrsVLiveSourceType `json:"type"` + Format *VLiveFileFormat `json:"format"` + Video *VLiveFileVideo `json:"video"` + Audio *VLiveFileAudio `json:"audio"` } func (v *VLiveSourceFile) String() string { @@ -989,7 +991,7 @@ func (v *VLiveTask) doVLive(ctx context.Context, input *VLiveSourceFile) error { // Start FFmpeg process. args := []string{} - if input.Type != SRS_SOURCE_TYPE_STREAM { + if input.Type != SrsVLiveSourceTypeStream { args = append(args, "-stream_loop", "-1") } args = append(args, "-re", "-i", input.Target, "-c", "copy", "-f", "flv", outputURL) diff --git a/ui/src/pages/ScenarioVLive.js b/ui/src/pages/ScenarioVLive.js index 79a541be..ad9739eb 100644 --- a/ui/src/pages/ScenarioVLive.js +++ b/ui/src/pages/ScenarioVLive.js @@ -643,8 +643,11 @@ function VLiveFileList({files, onChangeFiles}) { function ChooseVideoSourceCn({platform, vLiveFiles, setVLiveFiles}) { const [checkType, setCheckType] = React.useState('upload'); React.useEffect(() => { - if (vLiveFiles?.length && vLiveFiles[0].type === 'stream') { - setCheckType('stream'); + if (vLiveFiles?.length) { + const type = vLiveFiles[0].type; + if (type === 'upload' || type === 'server' || type === 'stream') { + setCheckType(type); + } } }, [vLiveFiles]); return (<> @@ -692,8 +695,11 @@ function ChooseVideoSourceCn({platform, vLiveFiles, setVLiveFiles}) { function ChooseVideoSourceEn({platform, vLiveFiles, setVLiveFiles}) { const [checkType, setCheckType] = React.useState('upload'); React.useEffect(() => { - if (vLiveFiles?.length && vLiveFiles[0].type === 'stream') { - setCheckType('stream'); + if (vLiveFiles?.length) { + const type = vLiveFiles[0].type; + if (type === 'upload' || type === 'server' || type === 'stream') { + setCheckType(type); + } } }, [vLiveFiles]); return (<> @@ -744,8 +750,8 @@ function VLiveStreamSelectorCn({platform, vLiveFiles, setVLiveFiles}) { const checkStreamUrl = function() { if (!inputStream) return alert('请输入流地址'); - // check stream url if valid. start with rtmp/http-flv/hls. - if (!inputStream.startsWith('rtmp://') && !inputStream.startsWith('http://') && !inputStream.startsWith('https://')) return alert('流地址必须是 rtmp/http-flv/hls 格式'); + // check stream url if valid. start with rtmp/rtsp/http-flv/hls. + if (!inputStream.startsWith('rtmp://') && !inputStream.startsWith('rtsp://') && !inputStream.startsWith('http://') && !inputStream.startsWith('https://')) return alert('流地址必须是 rtmp/rtsp/http-flv/hls 格式'); const token = Token.load(); axios.post(`/terraform/v1/ffmpeg/vlive/streamUrl?url=${inputStream}`).then(res => { console.log(`检查流地址成功,${JSON.stringify(res.data.data)}`); @@ -783,8 +789,8 @@ function VLiveStreamSelectorEn({platform, vLiveFiles, setVLiveFiles}) { const checkStreamUrl = function() { if (!inputStream) return alert('Please input stream URL'); - // check stream url if valid. start with rtmp/http-flv/hls. - if (!inputStream.startsWith('rtmp://') && !inputStream.startsWith('http://') && !inputStream.startsWith('https://')) return alert('The stream must be in rtmp/http-flv/hls format.'); + // check stream url if valid. start with rtmp/rtsp/http-flv/hls. + if (!inputStream.startsWith('rtmp://') && !inputStream.startsWith('rtsp://') && !inputStream.startsWith('http://') && !inputStream.startsWith('https://')) return alert('The stream must be in rtmp/rtsp/http-flv/hls format.'); const token = Token.load(); axios.post(`/terraform/v1/ffmpeg/vlive/streamUrl?url=${inputStream}`).then(res => { console.log(`Check stream url ok,${JSON.stringify(res.data.data)}`); @@ -831,7 +837,7 @@ function VLiveFileSelectorCn({platform, vLiveFiles, setVLiveFiles}) { axios.post(`/terraform/v1/ffmpeg/vlive/server?file=${inputFile}`).then(res => { console.log(`检查服务器文件成功,${JSON.stringify(res.data.data)}`); const localFileObj = res.data.data; - const files = [{name: localFileObj.name, size: localFileObj.size, uuid: localFileObj.uuid, target: localFileObj.target}]; + const files = [{name: localFileObj.name, size: localFileObj.size, uuid: localFileObj.uuid, target: localFileObj.target, type: "file"}]; axios.post('/terraform/v1/ffmpeg/vlive/source', { ...token, platform, files, }).then(res => { @@ -873,7 +879,7 @@ function VLiveFileSelectorEn({platform, vLiveFiles, setVLiveFiles}) { axios.post(`/terraform/v1/ffmpeg/vlive/server?file=${inputFile}`).then(res => { console.log(`Check server file ok,${JSON.stringify(res.data.data)}`); const localFileObj = res.data.data; - const files = [{name: localFileObj.name, size: localFileObj.size, uuid: localFileObj.uuid, target: localFileObj.target}]; + const files = [{name: localFileObj.name, size: localFileObj.size, uuid: localFileObj.uuid, target: localFileObj.target, type: "file"}]; axios.post('/terraform/v1/ffmpeg/vlive/source', { ...token, platform, files, }).then(res => { @@ -908,7 +914,7 @@ function VLiveFileUploaderCn({platform, vLiveFiles, setVLiveFiles}) { const token = Token.load(); axios.post('/terraform/v1/ffmpeg/vlive/source', { ...token, platform, files: files.map(f => { - return {name: f.name, size: f.size, uuid: f.uuid, target: f.target}; + return {name: f.name, size: f.size, uuid: f.uuid, target: f.target, type: "upload"}; }), }).then(res => { console.log(`虚拟直播文件源设置成功, ${JSON.stringify(res.data.data)}`); @@ -932,7 +938,7 @@ function VLiveFileUploaderEn({platform, vLiveFiles, setVLiveFiles}) { const token = Token.load(); axios.post('/terraform/v1/ffmpeg/vlive/source', { ...token, platform, files: files.map(f => { - return {name: f.name, size: f.size, uuid: f.uuid, target: f.target}; + return {name: f.name, size: f.size, uuid: f.uuid, target: f.target, type: "upload"}; }), }).then(res => { console.log(`Set file source ok, ${JSON.stringify(res.data.data)}`); From e46c32a71f148d71ca15da79019388c6a4144a73 Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 7 Oct 2023 15:32:27 +0800 Subject: [PATCH 3/7] Refine UI. --- ui/src/pages/ScenarioVLive.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/ui/src/pages/ScenarioVLive.js b/ui/src/pages/ScenarioVLive.js index ad9739eb..84479f3e 100644 --- a/ui/src/pages/ScenarioVLive.js +++ b/ui/src/pages/ScenarioVLive.js @@ -678,10 +678,10 @@ function ChooseVideoSourceCn({platform, vLiveFiles, setVLiveFiles}) { - setCheckType('stream')} />   - * 流必须是 rtmp/http-flv/hls 格式 + * 流地址支持 rtmp, http, https, 或 rtsp 等格式 {checkType === 'stream' && @@ -730,10 +730,10 @@ function ChooseVideoSourceEn({platform, vLiveFiles, setVLiveFiles}) { - setCheckType('stream')} />   - * The stream must be in rtmp/http-flv/hls format. + * The stream URL should start with rtmp, http, https, or rtsp. {checkType === 'stream' && @@ -750,8 +750,10 @@ function VLiveStreamSelectorCn({platform, vLiveFiles, setVLiveFiles}) { const checkStreamUrl = function() { if (!inputStream) return alert('请输入流地址'); - // check stream url if valid. start with rtmp/rtsp/http-flv/hls. - if (!inputStream.startsWith('rtmp://') && !inputStream.startsWith('rtsp://') && !inputStream.startsWith('http://') && !inputStream.startsWith('https://')) return alert('流地址必须是 rtmp/rtsp/http-flv/hls 格式'); + const isHTTP = inputStream.startsWith('http://') || inputStream.startsWith('https://'); + if (!inputStream.startsWith('rtmp://') && !inputStream.startsWith('rtsp://') && !isHTTP) return alert('流地址必须是 rtmp/http/https/rtsp 格式'); + if (isHTTP && inputStream.indexOf('.flv') < 0 && inputStream.indexOf('.m3u8') < 0) return alert('HTTP流必须是 http-flv或hls 格式'); + const token = Token.load(); axios.post(`/terraform/v1/ffmpeg/vlive/streamUrl?url=${inputStream}`).then(res => { console.log(`检查流地址成功,${JSON.stringify(res.data.data)}`); @@ -789,8 +791,10 @@ function VLiveStreamSelectorEn({platform, vLiveFiles, setVLiveFiles}) { const checkStreamUrl = function() { if (!inputStream) return alert('Please input stream URL'); - // check stream url if valid. start with rtmp/rtsp/http-flv/hls. - if (!inputStream.startsWith('rtmp://') && !inputStream.startsWith('rtsp://') && !inputStream.startsWith('http://') && !inputStream.startsWith('https://')) return alert('The stream must be in rtmp/rtsp/http-flv/hls format.'); + const isHTTP = inputStream.startsWith('http://') || inputStream.startsWith('https://'); + if (!inputStream.startsWith('rtmp://') && !inputStream.startsWith('rtsp://') && !isHTTP) return alert('The stream must be rtmp/http/https/rtsp'); + if (isHTTP && inputStream.indexOf('.flv') < 0 && inputStream.indexOf('.m3u8') < 0) return alert('The HTTP stream must be http-flv/hls'); + const token = Token.load(); axios.post(`/terraform/v1/ffmpeg/vlive/streamUrl?url=${inputStream}`).then(res => { console.log(`Check stream url ok,${JSON.stringify(res.data.data)}`); From 003fb8b90eda335e6970abe7dc479982d962d10d Mon Sep 17 00:00:00 2001 From: winlin Date: Sat, 7 Oct 2023 15:34:30 +0800 Subject: [PATCH 4/7] Refine UI. --- ui/src/pages/ScenarioVLive.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/pages/ScenarioVLive.js b/ui/src/pages/ScenarioVLive.js index 84479f3e..fc1d2868 100644 --- a/ui/src/pages/ScenarioVLive.js +++ b/ui/src/pages/ScenarioVLive.js @@ -651,7 +651,7 @@ function ChooseVideoSourceCn({platform, vLiveFiles, setVLiveFiles}) { } }, [vLiveFiles]); return (<> - + 视频源 * 虚拟直播就是将视频源(文件/流)转换成直播流 - + Live Stream Source * Virtual live streaming is the process of converting a video source (file) into a live stream. Date: Tue, 10 Oct 2023 11:40:06 +0800 Subject: [PATCH 5/7] add test case for vlive stream --- test/scenario_test.go | 196 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 196 insertions(+) diff --git a/test/scenario_test.go b/test/scenario_test.go index ad947014..443d797e 100644 --- a/test/scenario_test.go +++ b/test/scenario_test.go @@ -16,6 +16,202 @@ import ( "github.com/ossrs/go-oryx-lib/logger" ) +func TestApi_PublishVliveStreamUrl(t *testing.T) { + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + defer cancel() + + if *noMediaTest { + return + } + + var r0, r1, r2, r3, r4, r5 error + defer func(ctx context.Context) { + if err := filterTestError(ctx.Err(), r0, r1, r2, r3, r4, r5); err != nil { + t.Errorf("Fail for err %+v", err) + } else { + logger.Tf(ctx, "test done") + } + }(ctx) + + var pubSecret string + if err := apiRequest(ctx, "/terraform/v1/hooks/srs/secret/query", nil, &struct { + Publish *string `json:"publish"` + }{ + Publish: &pubSecret, + }); err != nil { + r0 = err + return + } + + var wg sync.WaitGroup + defer wg.Wait() + // Start FFmpeg to publish stream. + streamID := fmt.Sprintf("stream-%v-%v", os.Getpid(), rand.Int()) + streamURL := fmt.Sprintf("%v/live/%v?secret=%v", *endpointRTMP, streamID, pubSecret) + ffmpeg := NewFFmpeg(func(v *ffmpegClient) { + v.args = []string{ + "-re", "-stream_loop", "-1", "-i", *srsInputFile, "-c", "copy", + "-f", "flv", streamURL, + } + }) + wg.Add(1) + go func() { + defer wg.Done() + r1 = ffmpeg.Run(ctx, cancel) + }() + + defer cancel() + select { + case <-ctx.Done(): + return + case <-ffmpeg.ReadyCtx().Done(): + } + + // Use the publish stream url as the vlive input. + res := struct { + Name string `json:"name"` + Target string `json:"target"` + UUID string `json:"uuid"` + Size int64 `json:"size"` + Type string `json:"type"` + }{} + if err := apiRequest(ctx, "/terraform/v1/ffmpeg/vlive/streamUrl?url="+streamURL, nil, &res); err != nil { + r0 = errors.Wrapf(err, "request ffmpeg vlive streamUrl failed") + return + } + + // Use the publish stream url as the vlive source. + res.Size = 0 + res.Type = "stream" + codec := struct { + UUID string `json:"uuid"` + Audio struct { + CodecName string `json:"codec_name"` + Channels int `json:"channels"` + SampleRate string `json:"sample_rate"` + } `json:"audio"` + Video struct { + CodecName string `json:"codec_name"` + Profile string `json:"profile"` + Width int `json:"width"` + Height int `json:"height"` + } `json:"video"` + }{} + if err := apiRequest(ctx, "/terraform/v1/ffmpeg/vlive/source", &struct { + Platform string `json:"platform"` + Files []interface{} `json:"files"` + }{ + Platform: "bilibili", + Files: []interface{}{res}, + }, &struct { + Files []interface{} `json:"files"` + }{ + Files: []interface{}{&codec}, + }); err != nil { + r0 = errors.Wrapf(err, "request ffmpeg vlive source failed") + return + } + + if err := func() error { + if codec.UUID != res.UUID { + return errors.Errorf("invalid codec uuid=%v, %v", codec.UUID, res.UUID) + } + if codec.Audio.CodecName != "aac" || codec.Audio.Channels != 2 || codec.Audio.SampleRate != "44100" { + return errors.Errorf("invalid codec audio=%v", codec.Audio) + } + if codec.Video.CodecName != "h264" || codec.Video.Profile != "High" || codec.Video.Width != 768 || codec.Video.Height != 320 { + return errors.Errorf("invalid codec video=%v", codec.Video) + } + return nil + }(); err != nil { + r0 = errors.Wrapf(err, "request ffmpeg vlive source failed") + return + } + + // Start virtual live streaming. + type VLiveConfig struct { + Platform string `json:"platform"` + Server string `json:"server"` + Secret string `json:"secret"` + Enabled bool `json:"enabled"` + Custom bool `json:"custom"` + Label string `json:"label"` + Files interface{} `json:"files"` + Action string `json:"action"` + } + conf := make(map[string]*VLiveConfig) + if err := apiRequest(ctx, "/terraform/v1/ffmpeg/vlive/secret", nil, &conf); err != nil { + r0 = errors.Wrapf(err, "request ffmpeg vlive secret failed") + return + } + + bilibili, ok := conf["bilibili"] + if !ok || bilibili == nil { + r0 = errors.Errorf("invalid bilibili secret") + return + } + bilibili.Action = "update" + bilibili.Custom = true + + // Restore the state of enabled. + backup := *bilibili + defer func() { + logger.Tf(ctx, "restore config %v", backup) + + if backup.Server == "" { + backup.Server = bilibili.Server + } + if backup.Secret == "" { + backup.Secret = bilibili.Secret + } + + // The ctx has already been cancelled by test case, which will cause the request failed. + ctx := context.Background() + apiRequest(ctx, "/terraform/v1/ffmpeg/vlive/secret", backup, nil) + }() + + publishStreamID := fmt.Sprintf("publish-stream-%v-%v", os.Getpid(), rand.Int()) + bilibili.Secret = fmt.Sprintf("%v?secret=%v", publishStreamID, pubSecret) + bilibili.Server = "rtmp://localhost/live/" + bilibili.Enabled = true + if err := apiRequest(ctx, "/terraform/v1/ffmpeg/vlive/secret", &bilibili, nil); err != nil { + r0 = errors.Wrapf(err, "request ffmpeg vlive secret failed") + return + } + + // Start FFprobe to detect and verify stream. + duration := time.Duration(*srsFFprobeDuration) * time.Millisecond + ffprobe := NewFFprobe(func(v *ffprobeClient) { + v.dvrFile = fmt.Sprintf("srs-ffprobe-%v.flv", publishStreamID) + v.streamURL = fmt.Sprintf("%v/live/%v.flv", *endpointHTTP, publishStreamID) + v.duration, v.timeout = duration, time.Duration(*srsFFprobeTimeout)*time.Millisecond + }) + wg.Add(1) + go func() { + defer wg.Done() + r2 = ffprobe.Run(ctx, cancel) + }() + + // Fast quit for probe done. + select { + case <-ctx.Done(): + case <-ffprobe.ProbeDoneCtx().Done(): + 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, m.String(), str) + } +} + func TestApi_PublishVLivePlayFlv(t *testing.T) { ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) defer cancel() From 8f5592f9ae0da0aa50174b77e6d6f94ae475793d Mon Sep 17 00:00:00 2001 From: winlin Date: Tue, 10 Oct 2023 19:56:02 +0800 Subject: [PATCH 6/7] Add pullrequest action without secrets. --- .github/workflows/pullrequest.yml | 417 ++++++++++++++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 .github/workflows/pullrequest.yml diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml new file mode 100644 index 00000000..cd639654 --- /dev/null +++ b/.github/workflows/pullrequest.yml @@ -0,0 +1,417 @@ +name: Test Dev Environment + +on: [pull_request] + +jobs: + envs: + name: envs + steps: + ################################################################################################################## + # Git checkout + - name: Checkout repository + uses: actions/checkout@v3 + # The github.ref is, for example, refs/tags/v5.0.145 or refs/tags/v5.0-r8 + # Generate variables like: + # SRS_TAG=v1.0.52 + # SRS_MAJOR=1 + # @see https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-environment-variable + - name: Generate varaiables + run: | + SRS_TAG=$(bash scripts/version.sh) + echo "SRS_TAG=$SRS_TAG" >> $GITHUB_ENV + SRS_MAJOR=$(echo $SRS_TAG| awk -F '.' '{print $1}' |sed 's/v//g') + echo "SRS_MAJOR=$SRS_MAJOR" >> $GITHUB_ENV + echo "SRS_TAG:$SRS_TAG, SRS_MAJOR:$SRS_MAJOR" + # Map a step output to a job output, see https://docs.github.com/en/actions/using-jobs/defining-outputs-for-jobs + outputs: + SRS_TAG: ${{ env.SRS_TAG }} + SRS_MAJOR: ${{ env.SRS_MAJOR }} + runs-on: ubuntu-20.04 + + run-test: + name: Run UTest + runs-on: ubuntu-20.04 + needs: + - envs + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Download test file + run: | + curl --location --output test/source.200kbps.768x320.flv \ + https://github.com/ossrs/srs/raw/develop/trunk/doc/source.200kbps.768x320.flv + - name: Setup the npm node + uses: actions/setup-node@v3 + with: + node-version: '18' + cache: 'npm' + cache-dependency-path: | + ui/package-lock.json + - name: Setup the Go + uses: actions/setup-go@v3 + with: + go-version: '>=1.16.0' + - name: Install tools + run: | + docker run --rm -v /usr/bin:/g ossrs/srs:tools \ + cp /usr/local/bin/ffmpeg /usr/local/bin/ffprobe /g/ + ffmpeg -version + - name: Test by jest and Go + run: | + make -j && make test -j + + build-platform-image: + name: Build platform image + runs-on: ubuntu-20.04 + needs: + - envs + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Build image for platform + run: | + docker build -t platform:latest -f Dockerfile . + docker images + docker save -o platform.tar platform:latest + - uses: actions/upload-artifact@v3 + with: + name: platform-cache + path: platform.tar + retention-days: 1 + + test-zh-image: + name: Test ZH image + needs: + - envs + - build-platform-image + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Download test file + run: | + curl --location --output test/source.200kbps.768x320.flv \ + https://github.com/ossrs/srs/raw/develop/trunk/doc/source.200kbps.768x320.flv + - name: Install tools + run: | + docker run --rm -v /usr/bin:/g ossrs/srs:tools \ + cp /usr/local/bin/ffmpeg /usr/local/bin/ffprobe /g/ + ffmpeg -version + - uses: actions/download-artifact@v3 + with: + name: platform-cache + - name: Run test for platform image + run: | + docker load -i platform.tar + docker tag platform ossrs/srs-stack:5 + docker run --rm -d -p 2022:2022 -p 2443:2443 -p 1935:1935 \ + -p 8080:8080 -p 8000:8000/udp -p 10080:10080/udp --name srs-stack \ + -v /data:/data -e REACT_APP_LOCALE=zh ossrs/srs-stack:5 + - name: Check and Test service + run: | + # We will handle the error by ourselves. + set +e + + # Record all logs. + docker logs -f srs-stack >docker.log 2>&1 & pid_docker=$! + + echo "Wait for service ready." && + make -j -C test && + bash scripts/tools/secret.sh --output test/.env && + ./test/srs-stack.test -test.v -endpoint http://localhost:2022 \ + -srs-log=true -wait-ready=true -init-password=true -init-self-signed-cert=true \ + -check-api-secret=true -test.run TestApi_Empty && + + echo "Make upload writable." && + sudo chmod 777 /data/upload && + + echo "Test HTTP service." && + bash scripts/tools/secret.sh --output test/.env && + ./test/srs-stack.test -test.v -endpoint http://localhost:2022 \ + -srs-log=true -wait-ready=true -init-password=false -init-self-signed-cert=false \ + -check-api-secret=true -no-media-test && + + echo "Test HTTPS service." && + bash scripts/tools/secret.sh --output test/.env && + ./test/srs-stack.test -test.v -endpoint https://localhost:2443 \ + -srs-log=true -wait-ready=true -init-password=false -init-self-signed-cert=false \ + -check-api-secret=true -no-media-test && + + echo "Run media test with retry" && + bash scripts/tools/secret.sh --output test/.env && + ./scripts/tools/failed-retry.sh 1 ./test/srs-stack.test -test.v -endpoint http://localhost:2022 \ + -srs-log=true -wait-ready=true -init-password=false -init-self-signed-cert=false \ + -check-api-secret=true -test.run TestApi_Publish* + ret=$?; echo "Test result: $ret" + + echo "Stop service" + docker stop srs-stack + kill $pid_docker 2>/dev/null + echo "Log of docker.log" && cat docker.log + + exit $ret + runs-on: ubuntu-20.04 + + test-en-image: + name: Test EN image + needs: + - envs + - build-platform-image + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Download test file + run: | + curl --location --output test/source.200kbps.768x320.flv \ + https://github.com/ossrs/srs/raw/develop/trunk/doc/source.200kbps.768x320.flv + - name: Install tools + run: | + docker run --rm -v /usr/bin:/g ossrs/srs:tools \ + cp /usr/local/bin/ffmpeg /usr/local/bin/ffprobe /g/ + ffmpeg -version + - uses: actions/download-artifact@v3 + with: + name: platform-cache + - name: Run test for platform image + run: | + docker load -i platform.tar + docker tag platform ossrs/srs-stack:5 + docker run --rm -d -p 2022:2022 -p 2443:2443 -p 1935:1935 \ + -p 8080:8080 -p 8000:8000/udp -p 10080:10080/udp --name srs-stack \ + -v /data:/data -e REACT_APP_LOCALE=en ossrs/srs-stack:5 + - name: Check and Test service + run: | + # We will handle the error by ourselves. + set +e + + # Record all logs. + docker logs -f srs-stack >docker.log 2>&1 & pid_docker=$! + + echo "Wait for service ready." && + make -j -C test && + bash scripts/tools/secret.sh --output test/.env && + ./test/srs-stack.test -test.v -endpoint http://localhost:2022 \ + -srs-log=true -wait-ready=true -init-password=true -init-self-signed-cert=true \ + -check-api-secret=true -test.run TestApi_Empty && + + echo "Make upload writable." && + sudo chmod 777 /data/upload && + + echo "Test HTTP service." && + bash scripts/tools/secret.sh --output test/.env && + ./test/srs-stack.test -test.v -endpoint http://localhost:2022 \ + -srs-log=true -wait-ready=true -init-password=false -init-self-signed-cert=false \ + -check-api-secret=true -no-media-test && + + echo "Test HTTPS service." && + bash scripts/tools/secret.sh --output test/.env && + ./test/srs-stack.test -test.v -endpoint https://localhost:2443 \ + -srs-log=true -wait-ready=true -init-password=false -init-self-signed-cert=false \ + -check-api-secret=true -no-media-test && + + echo "Run media test with retry" && + bash scripts/tools/secret.sh --output test/.env && + ./scripts/tools/failed-retry.sh 1 ./test/srs-stack.test -test.v -endpoint http://localhost:2022 \ + -srs-log=true -wait-ready=true -init-password=false -init-self-signed-cert=false \ + -check-api-secret=true -test.run TestApi_Publish* + ret=$?; echo "Test result: $ret" + + echo "Stop service" + docker stop srs-stack + kill $pid_docker 2>/dev/null + echo "Log of docker.log" && cat docker.log + + exit $ret + runs-on: ubuntu-20.04 + + test-zh-installer: + name: Test ZH installer + runs-on: ubuntu-20.04 + needs: + - envs + - build-platform-image + steps: + - name: Covert output to env + run: | + echo "SRS_TAG=${{ needs.envs.outputs.SRS_TAG }}" >> $GITHUB_ENV + echo "SRS_MAJOR=${{ needs.envs.outputs.SRS_MAJOR }}" >> $GITHUB_ENV + - name: Checkout repository + uses: actions/checkout@v3 + - name: Download test file + run: | + curl --location --output test/source.200kbps.768x320.flv \ + https://github.com/ossrs/srs/raw/develop/trunk/doc/source.200kbps.768x320.flv + - name: Install tools + run: | + docker run --rm -v /usr/bin:/g ossrs/srs:tools \ + cp /usr/local/bin/ffmpeg /usr/local/bin/ffprobe /g/ + ffmpeg -version + - name: Start Nginx service + run: | + sudo systemctl start nginx + sudo systemctl status nginx + - uses: actions/download-artifact@v3 + with: + name: platform-cache + - name: Load platform image + run: | + docker load -i platform.tar + docker tag platform:latest ossrs/srs-stack:$SRS_TAG + docker tag platform:latest registry.cn-hangzhou.aliyuncs.com/ossrs/srs-stack:$SRS_TAG + docker images + - name: Build package + run: | + bash scripts/setup-ubuntu/build.sh --language zh --version $SRS_TAG \ + --output $(pwd)/build --extract + du -sh $(pwd)/build/* + - name: Install package + run: | + sudo bash build/srs_stack/scripts/setup-ubuntu/install.sh --verbose + echo "" && echo "/usr/local/srs-stack/" && du -sh /usr/local/srs-stack/* + echo "" && ls -lha /data /data/config + - name: Check and Test service + run: | + # We will handle the error by ourselves. + set +e + + # Record all logs. + journalctl -u srs-stack -f >journalctl.log 2>&1 & pid_journalctl=$! + + echo "Wait for service ready." && + make -j -C test && + bash scripts/tools/secret.sh --output test/.env && + ./test/srs-stack.test -test.v -endpoint http://localhost:2022 \ + -srs-log=true -wait-ready=true -init-password=true -init-self-signed-cert=true \ + -check-api-secret=true -test.run TestApi_Empty && + + echo "Make upload writable." && + sudo chmod 777 /data/upload && + + echo "Test HTTP service." && + bash scripts/tools/secret.sh --output test/.env && + ./test/srs-stack.test -test.v -endpoint http://localhost:2022 \ + -srs-log=true -wait-ready=true -init-password=false -init-self-signed-cert=false \ + -check-api-secret=true -no-media-test && + + echo "Test HTTPS service." && + bash scripts/tools/secret.sh --output test/.env && + ./test/srs-stack.test -test.v -endpoint https://localhost:2443 \ + -srs-log=true -wait-ready=true -init-password=false -init-self-signed-cert=false \ + -check-api-secret=true -no-media-test && + + echo "Run media test with retry" && + bash scripts/tools/secret.sh --output test/.env && + ./scripts/tools/failed-retry.sh 1 ./test/srs-stack.test -test.v -endpoint http://localhost:2022 \ + -srs-log=true -wait-ready=true -init-password=false -init-self-signed-cert=false \ + -check-api-secret=true -test.run TestApi_Publish* + ret=$?; echo "Test result: $ret" + + echo "Stop service" + sudo systemctl stop srs-stack + kill $pid_journalctl 2>/dev/null + echo "Log of journalctl.log" && cat journalctl.log + + exit $ret + + test-en-installer: + name: Test EN installer + runs-on: ubuntu-20.04 + needs: + - envs + - build-platform-image + steps: + - name: Covert output to env + run: | + echo "SRS_TAG=${{ needs.envs.outputs.SRS_TAG }}" >> $GITHUB_ENV + echo "SRS_MAJOR=${{ needs.envs.outputs.SRS_MAJOR }}" >> $GITHUB_ENV + - name: Checkout repository + uses: actions/checkout@v3 + - name: Download test file + run: | + curl --location --output test/source.200kbps.768x320.flv \ + https://github.com/ossrs/srs/raw/develop/trunk/doc/source.200kbps.768x320.flv + - name: Install tools + run: | + docker run --rm -v /usr/bin:/g ossrs/srs:tools \ + cp /usr/local/bin/ffmpeg /usr/local/bin/ffprobe /g/ + ffmpeg -version + - name: Start Nginx service + run: | + sudo systemctl start nginx + sudo systemctl status nginx + - uses: actions/download-artifact@v3 + with: + name: platform-cache + - name: Load platform image + run: | + docker load -i platform.tar + docker tag platform:latest ossrs/srs-stack:$SRS_TAG + docker tag platform:latest registry.cn-hangzhou.aliyuncs.com/ossrs/srs-stack:$SRS_TAG + docker images + - name: Build package + run: | + bash scripts/setup-ubuntu/build.sh --language en --version $SRS_TAG \ + --output $(pwd)/build --extract + du -sh $(pwd)/build/* + - name: Install package + run: | + sudo bash build/srs_stack/scripts/setup-ubuntu/install.sh --verbose + echo "" && echo "/usr/local/srs-stack/" && du -sh /usr/local/srs-stack/* + echo "" && ls -lha /data /data/config + - name: Check and Test service + run: | + # We will handle the error by ourselves. + set +e + + # Record all logs. + journalctl -u srs-stack -f >journalctl.log 2>&1 & pid_journalctl=$! + + echo "Wait for service ready." && + make -j -C test && + bash scripts/tools/secret.sh --output test/.env && + ./test/srs-stack.test -test.v -endpoint http://localhost:2022 \ + -srs-log=true -wait-ready=true -init-password=true -init-self-signed-cert=true \ + -check-api-secret=true -test.run TestApi_Empty && + + echo "Make upload writable." && + sudo chmod 777 /data/upload && + + echo "Test HTTP service." && + bash scripts/tools/secret.sh --output test/.env && + ./test/srs-stack.test -test.v -endpoint http://localhost:2022 \ + -srs-log=true -wait-ready=true -init-password=false -init-self-signed-cert=false \ + -check-api-secret=true -no-media-test && + + echo "Test HTTPS service." && + bash scripts/tools/secret.sh --output test/.env && + ./test/srs-stack.test -test.v -endpoint https://localhost:2443 \ + -srs-log=true -wait-ready=true -init-password=false -init-self-signed-cert=false \ + -check-api-secret=true -no-media-test && + + echo "Run media test with retry" && + bash scripts/tools/secret.sh --output test/.env && + ./scripts/tools/failed-retry.sh 1 ./test/srs-stack.test -test.v -endpoint http://localhost:2022 \ + -srs-log=true -wait-ready=true -init-password=false -init-self-signed-cert=false \ + -check-api-secret=true -test.run TestApi_Publish* + ret=$?; echo "Test result: $ret" + + echo "Stop service" + sudo systemctl stop srs-stack + kill $pid_journalctl 2>/dev/null + echo "Log of journalctl.log" && cat journalctl.log + + exit $ret + + test-final: + name: test-final + runs-on: ubuntu-20.04 + needs: + - run-test + - test-zh-image + - test-en-image + - test-zh-installer + - test-en-installer + steps: + - uses: geekyeggo/delete-artifact@v2 + with: + name: platform-cache + - run: echo OK From bb7f63bcf944155e5add4854f10d1eec7780dff4 Mon Sep 17 00:00:00 2001 From: winlin Date: Tue, 10 Oct 2023 19:57:10 +0800 Subject: [PATCH 7/7] Add pullrequest action without secrets. --- .github/workflows/pullrequest.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pullrequest.yml b/.github/workflows/pullrequest.yml index cd639654..aaba9b49 100644 --- a/.github/workflows/pullrequest.yml +++ b/.github/workflows/pullrequest.yml @@ -1,4 +1,4 @@ -name: Test Dev Environment +name: Test Dev for PullRequest on: [pull_request]