Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

VLive: Support forward stream. v5.10.5 #140

Merged
merged 7 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions platform/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ const (
SRS_BEIAN = "SRS_BEIAN"
SRS_HTTPS = "SRS_HTTPS"
SRS_HTTPS_DOMAIN = "SRS_HTTPS_DOMAIN"

SRS_SOURCE_TYPE_FILE = "file"
panda1986 marked this conversation as resolved.
Show resolved Hide resolved
SRS_SOURCE_TYPE_STREAM = "stream"
)

// Tencent cloud consts.
Expand Down
80 changes: 67 additions & 13 deletions platform/virtual-live-stream.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path"
Expand Down Expand Up @@ -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" {
panda1986 marked this conversation as resolved.
Show resolved Hide resolved
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") {
panda1986 marked this conversation as resolved.
Show resolved Hide resolved
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) {
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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)
}
}
}

panda1986 marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -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()
Expand Down
130 changes: 127 additions & 3 deletions ui/src/pages/ScenarioVLive.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 (<>
<Form.Group className="mb-2">
<Form.Label>视频源</Form.Label>
<Form.Text> * 虚拟直播就是将视频源(文件)转换成直播流</Form.Text>
<Form.Text> * 虚拟直播就是将视频源(文件/流)转换成直播流</Form.Text>
<Form.Check type="radio" label="上传本地文件" id={'upload-' + platform} checked={checkType === 'upload'}
name={'chooseSource-' + platform} onChange={e => setCheckType('upload')}
/>
Expand All @@ -668,11 +673,29 @@ function ChooseVideoSourceCn({platform, vLiveFiles, setVLiveFiles}) {
</SrsErrorBoundary>
}
</Form.Group>
<Form.Group className="mb-3">
<InputGroup>
<Form.Check type="radio" label="指定流" id={'stream-' + platform} checked={checkType === 'stream'}
name={'chooseSource' + platform} onChange={e => setCheckType('stream')}
/> &nbsp;
<Form.Text> * 流必须是 rtmp/http-flv/hls 格式</Form.Text>
</InputGroup>
{checkType === 'stream' &&
<SrsErrorBoundary>
<VLiveStreamSelectorCn platform={platform} vLiveFiles={vLiveFiles} setVLiveFiles={setVLiveFiles}/>
</SrsErrorBoundary>
}
</Form.Group>
</>);
}

function ChooseVideoSourceEn({platform, vLiveFiles, setVLiveFiles}) {
const [checkType, setCheckType] = React.useState('upload');
React.useEffect(() => {
if (vLiveFiles?.length && vLiveFiles[0].type === 'stream') {
setCheckType('stream');
}
}, [vLiveFiles]);
return (<>
<Form.Group className="mb-2">
<Form.Label>Live Stream Source</Form.Label>
Expand All @@ -699,9 +722,100 @@ function ChooseVideoSourceEn({platform, vLiveFiles, setVLiveFiles}) {
</SrsErrorBoundary>
}
</Form.Group>
<Form.Group className="mb-3">
<InputGroup>
<Form.Check type="radio" label="Use stream" id={'stream-' + platform} checked={checkType === 'stream'}
name={'chooseSource' + platform} onChange={e => setCheckType('stream')}
/> &nbsp;
<Form.Text> * The stream must be in rtmp/http-flv/hls format.</Form.Text>
</InputGroup>
{checkType === 'stream' &&
<SrsErrorBoundary>
<VLiveStreamSelectorEn platform={platform} vLiveFiles={vLiveFiles} setVLiveFiles={setVLiveFiles}/>
</SrsErrorBoundary>
}
</Form.Group>
</>);
}

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 (<>
<Form.Control as="div">
{!vLiveFiles?.length && <>
<Row>
<Col>
<Form.Control type="text" value={inputStream} placeholder="请输入流地址" onChange={e => setInputStream(e.target.value)} />
</Col>
<Col xs="auto">
<Button variant="primary" onClick={checkStreamUrl}>确认</Button>
</Col>
</Row></>
}
{vLiveFiles?.length && <VLiveFileList files={vLiveFiles} onChangeFiles={(e) => setVLiveFiles(null)}/>}
</Form.Control>
</>);
}

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 (<>
<Form.Control as="div">
{!vLiveFiles?.length && <>
<Row>
<Col>
<Form.Control type="text" value={inputStream} placeholder="please input stream URL" onChange={e => setInputStream(e.target.value)} />
</Col>
<Col xs="auto">
<Button variant="primary" onClick={checkStreamUrl}>Submit</Button>
</Col>
</Row></>
}
{vLiveFiles?.length && <VLiveFileList files={vLiveFiles} onChangeFiles={(e) => setVLiveFiles(null)}/>}
</Form.Control>
</>)
}

function VLiveFileSelectorCn({platform, vLiveFiles, setVLiveFiles}) {
const handleError = useErrorHandler();
const [inputFile, setInputFile] = React.useState('');
Expand Down Expand Up @@ -838,8 +952,18 @@ function VLiveFileFormatInfo({file}) {
const f = file;
if (!f?.format) return <></>;
return <>
{Number(f?.size/1024/1024).toFixed(1)}MB &nbsp;
{Number(f?.format?.duration).toFixed(0)}s &nbsp;
{f?.type !== 'stream' &&
<>
File &nbsp;
{Number(f?.size/1024/1024).toFixed(1)}MB &nbsp;
{Number(f?.format?.duration).toFixed(0)}s &nbsp;
</>
}
{f?.type === 'stream' &&
<>
Stream &nbsp;
</>
}
{Number(f?.format?.bit_rate/1000).toFixed(1)}Kbps
</>;
}
Expand Down
4 changes: 2 additions & 2 deletions ui/src/resources/locale.json
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@
"submit": "提交",
"setOk": "设置成功",
"upload": "上传文件",
"changeFiles": "更换文件"
"changeFiles": "更换文件/流"
}
}
},
Expand Down Expand Up @@ -419,7 +419,7 @@
"submit": "Submit",
"setOk": "Setup OK",
"upload": "Upload File",
"changeFiles": "Change File"
"changeFiles": "Change File/Stream"
}
}
}
Expand Down