mirror of
https://github.com/lkmio/lkm.git
synced 2025-10-04 14:52:44 +08:00
feat: 支持开启和结束录制流
This commit is contained in:
47
api.go
47
api.go
@@ -99,6 +99,11 @@ func startApiServer(addr string) {
|
|||||||
handler.ServeHTTP(w, r)
|
handler.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 点播, 映射录制资源
|
||||||
|
// 放在最前面, 避免被后面的路由拦截
|
||||||
|
apiServer.router.PathPrefix("/record/").Handler(http.StripPrefix("/record/", http.FileServer(http.Dir(stream.AppConfig.Record.Dir))))
|
||||||
|
|
||||||
// {source}.flv和/{source}/{stream}.flv意味着, 推流id(路径)只能嵌套一层
|
// {source}.flv和/{source}/{stream}.flv意味着, 推流id(路径)只能嵌套一层
|
||||||
apiServer.router.HandleFunc("/{source}.flv", filterSourceID(apiServer.onFlv, ".flv"))
|
apiServer.router.HandleFunc("/{source}.flv", filterSourceID(apiServer.onFlv, ".flv"))
|
||||||
apiServer.router.HandleFunc("/{source}/{stream}.flv", filterSourceID(apiServer.onFlv, ".flv"))
|
apiServer.router.HandleFunc("/{source}/{stream}.flv", filterSourceID(apiServer.onFlv, ".flv"))
|
||||||
@@ -120,6 +125,8 @@ func startApiServer(addr string) {
|
|||||||
apiServer.router.HandleFunc("/api/v1/sink/list", withJsonParams(apiServer.OnSinkList, &IDS{})) // 查询某个推流源下,所有的拉流端列表
|
apiServer.router.HandleFunc("/api/v1/sink/list", withJsonParams(apiServer.OnSinkList, &IDS{})) // 查询某个推流源下,所有的拉流端列表
|
||||||
apiServer.router.HandleFunc("/api/v1/sink/close", withJsonParams(apiServer.OnSinkClose, &IDS{})) // 关闭拉流端
|
apiServer.router.HandleFunc("/api/v1/sink/close", withJsonParams(apiServer.OnSinkClose, &IDS{})) // 关闭拉流端
|
||||||
apiServer.router.HandleFunc("/api/v1/sink/add", withJsonParams(apiServer.OnSinkAdd, &GBOffer{})) // 级联/广播/JT转GB
|
apiServer.router.HandleFunc("/api/v1/sink/add", withJsonParams(apiServer.OnSinkAdd, &GBOffer{})) // 级联/广播/JT转GB
|
||||||
|
apiServer.router.HandleFunc("/api/v1/record/start", apiServer.OnRecordStart) // 开启录制
|
||||||
|
apiServer.router.HandleFunc("/api/v1/record/stop", apiServer.OnRecordStop) // 关闭录制
|
||||||
|
|
||||||
apiServer.router.HandleFunc("/api/v1/streams/statistics", nil) // 统计所有推拉流
|
apiServer.router.HandleFunc("/api/v1/streams/statistics", nil) // 统计所有推拉流
|
||||||
|
|
||||||
@@ -560,6 +567,11 @@ func (api *ApiServer) OnStreamInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
liveGBSUrls[streamName] = url
|
liveGBSUrls[streamName] = url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var recordStartTime string
|
||||||
|
if startTime := source.GetTransStreamPublisher().RecordStartTime(); !startTime.IsZero() {
|
||||||
|
recordStartTime = startTime.Format("2006-01-02 15:04:05")
|
||||||
|
}
|
||||||
|
|
||||||
statistics := source.GetBitrateStatistics()
|
statistics := source.GetBitrateStatistics()
|
||||||
response := struct {
|
response := struct {
|
||||||
AudioEnable bool `json:"AudioEnable"`
|
AudioEnable bool `json:"AudioEnable"`
|
||||||
@@ -583,7 +595,7 @@ func (api *ApiServer) OnStreamInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
RTPLostCount int `json:"RTPLostCount"`
|
RTPLostCount int `json:"RTPLostCount"`
|
||||||
RTPLostRate int `json:"RTPLostRate"`
|
RTPLostRate int `json:"RTPLostRate"`
|
||||||
RTSP string `json:"RTSP"`
|
RTSP string `json:"RTSP"`
|
||||||
RecordStartAt string `json:"RecordStartAt"`
|
RecordStartAt string `json:"RecordStartAt"` // 录制时间
|
||||||
RelaySize int `json:"RelaySize"`
|
RelaySize int `json:"RelaySize"`
|
||||||
SMSID string `json:"SMSID"`
|
SMSID string `json:"SMSID"`
|
||||||
SnapURL string `json:"SnapURL"`
|
SnapURL string `json:"SnapURL"`
|
||||||
@@ -621,7 +633,7 @@ func (api *ApiServer) OnStreamInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
RTPLostCount: 0,
|
RTPLostCount: 0,
|
||||||
RTPLostRate: 0,
|
RTPLostRate: 0,
|
||||||
RTSP: liveGBSUrls["rtsp"],
|
RTSP: liveGBSUrls["rtsp"],
|
||||||
RecordStartAt: "",
|
RecordStartAt: recordStartTime,
|
||||||
RelaySize: 0,
|
RelaySize: 0,
|
||||||
SMSID: "",
|
SMSID: "",
|
||||||
SnapURL: "",
|
SnapURL: "",
|
||||||
@@ -650,3 +662,34 @@ func (api *ApiServer) OnStreamInfo(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
httpResponseJson(w, &response)
|
httpResponseJson(w, &response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *ApiServer) OnRecordStart(w http.ResponseWriter, req *http.Request) {
|
||||||
|
streamId := req.FormValue("streamid")
|
||||||
|
source := stream.SourceManager.Find(streamId)
|
||||||
|
if source == nil {
|
||||||
|
log.Sugar.Errorf("OnRecordStart stream not found streamid %s", streamId)
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
} else if url, ok := source.GetTransStreamPublisher().StartRecord(); !ok {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
} else {
|
||||||
|
// 返回拉流地址
|
||||||
|
httpResponseJson(w, &struct {
|
||||||
|
DownloadURL string `json:"DownloadURL"`
|
||||||
|
}{
|
||||||
|
DownloadURL: url,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (api *ApiServer) OnRecordStop(w http.ResponseWriter, req *http.Request) {
|
||||||
|
streamId := req.FormValue("streamid")
|
||||||
|
source := stream.SourceManager.Find(streamId)
|
||||||
|
if source == nil {
|
||||||
|
log.Sugar.Errorf("OnRecordStop stream not found streamid %s", streamId)
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
} else if err := source.GetTransStreamPublisher().StopRecord(); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
httpResponseJson(w, err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -11,6 +11,7 @@ import (
|
|||||||
type FLVFileSink struct {
|
type FLVFileSink struct {
|
||||||
stream.BaseSink
|
stream.BaseSink
|
||||||
file *os.File
|
file *os.File
|
||||||
|
path string
|
||||||
fail bool
|
fail bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,22 +48,27 @@ func (f *FLVFileSink) Close() {
|
|||||||
f.file.Close()
|
f.file.Close()
|
||||||
f.file = nil
|
f.file = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if source := stream.SourceManager.Find(f.SourceID); source != nil {
|
||||||
|
stream.HookRecordEvent(source, f.path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFLVFileSink 创建FLV文件录制流Sink
|
// NewFLVFileSink 创建FLV文件录制流Sink
|
||||||
// 保存path: dir/sourceId/yyyy-MM-dd/HH-mm-ss.flv
|
// 保存path: dir/sourceId/yyyy-MM-dd/HH-mm-ss.flv
|
||||||
func NewFLVFileSink(sourceId string) (stream.Sink, string, error) {
|
func NewFLVFileSink(sourceId string) (stream.Sink, string, error) {
|
||||||
now := time.Now().Format("2006-01-02/15-04-05")
|
now := time.Now().Format("2006-01-02/15-04-05")
|
||||||
path := filepath.Join(stream.AppConfig.Record.Dir, sourceId, now+".flv")
|
path := filepath.Join(sourceId, now+".flv")
|
||||||
|
dirPath := filepath.Join(stream.AppConfig.Record.Dir, path)
|
||||||
|
|
||||||
// 创建目录
|
// 创建目录
|
||||||
dir := filepath.Dir(path)
|
dir := filepath.Dir(dirPath)
|
||||||
if err := os.MkdirAll(dir, 0666); err != nil {
|
if err := os.MkdirAll(dir, 0666); err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建flv文件
|
// 创建flv文件
|
||||||
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0666)
|
file, err := os.OpenFile(dirPath, os.O_CREATE|os.O_RDWR, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
}
|
}
|
||||||
|
@@ -351,3 +351,8 @@ func limitInt(min, max, value int) int {
|
|||||||
|
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GenerateRecordStreamPlayUrl 生成录制文件的播放url
|
||||||
|
func GenerateRecordStreamPlayUrl(recordFile string) string {
|
||||||
|
return fmt.Sprintf("http://%s:%d/record/%s", AppConfig.PublicIP, AppConfig.Http.Port, recordFile)
|
||||||
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package stream
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/lkmio/avformat"
|
"github.com/lkmio/avformat"
|
||||||
"github.com/lkmio/avformat/collections"
|
"github.com/lkmio/avformat/collections"
|
||||||
@@ -9,6 +10,9 @@ import (
|
|||||||
"github.com/lkmio/lkm/log"
|
"github.com/lkmio/lkm/log"
|
||||||
"github.com/lkmio/lkm/transcode"
|
"github.com/lkmio/lkm/transcode"
|
||||||
"github.com/lkmio/transport"
|
"github.com/lkmio/transport"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
@@ -66,6 +70,16 @@ type TransStreamPublisher interface {
|
|||||||
ExecuteSyncEvent(cb func())
|
ExecuteSyncEvent(cb func())
|
||||||
|
|
||||||
SetSourceID(id string)
|
SetSourceID(id string)
|
||||||
|
|
||||||
|
// StartRecord 开启录制
|
||||||
|
// 如果AppConfig已经开启了全局录制, 则无需手动开启, 返回false
|
||||||
|
StartRecord() (string, bool)
|
||||||
|
|
||||||
|
// StopRecord 停止录制
|
||||||
|
// 如果AppConfig已经开启了全局录制, 返回error
|
||||||
|
StopRecord() error
|
||||||
|
|
||||||
|
RecordStartTime() time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
type transStreamPublisher struct {
|
type transStreamPublisher struct {
|
||||||
@@ -78,6 +92,7 @@ type transStreamPublisher struct {
|
|||||||
|
|
||||||
recordSink Sink // 每个Source的录制流
|
recordSink Sink // 每个Source的录制流
|
||||||
recordFilePath string // 录制流文件路径
|
recordFilePath string // 录制流文件路径
|
||||||
|
recordStartTime time.Time // 开始录制时间
|
||||||
hlsStream TransStream // HLS传输流
|
hlsStream TransStream // HLS传输流
|
||||||
originTracks TrackManager // 推流的原始track
|
originTracks TrackManager // 推流的原始track
|
||||||
transcodeTracks map[utils.AVCodecID]*TranscodeTrack // 转码Track
|
transcodeTracks map[utils.AVCodecID]*TranscodeTrack // 转码Track
|
||||||
@@ -99,7 +114,18 @@ func (t *transStreamPublisher) Post(event *StreamEvent) {
|
|||||||
t.streamEvents.Post(event)
|
t.streamEvents.Post(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getGoroutineID() uint64 {
|
||||||
|
b := make([]byte, 64)
|
||||||
|
b = b[:runtime.Stack(b, false)]
|
||||||
|
b = bytes.TrimPrefix(b, []byte("goroutine "))
|
||||||
|
b = b[:bytes.IndexByte(b, ' ')]
|
||||||
|
n, _ := strconv.ParseUint(string(b), 10, 64)
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
func (t *transStreamPublisher) run() {
|
func (t *transStreamPublisher) run() {
|
||||||
|
log.Sugar.Infof("transStreamPublisher run goroutine id: %d", getGoroutineID())
|
||||||
|
|
||||||
t.streamEvents = NewNonBlockingChannel[*StreamEvent](256)
|
t.streamEvents = NewNonBlockingChannel[*StreamEvent](256)
|
||||||
t.mainContextEvents = make(chan func(), 256)
|
t.mainContextEvents = make(chan func(), 256)
|
||||||
|
|
||||||
@@ -165,6 +191,19 @@ func (t *transStreamPublisher) ExecuteSyncEvent(cb func()) {
|
|||||||
group.Wait()
|
group.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *transStreamPublisher) createRecordSink() bool {
|
||||||
|
sink, path, err := CreateRecordStream(t.source)
|
||||||
|
if err != nil {
|
||||||
|
log.Sugar.Errorf("创建录制sink失败 source: %s err: %s", t.source, err.Error())
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
t.recordSink = sink
|
||||||
|
t.recordFilePath = path
|
||||||
|
t.recordStartTime = time.Now()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (t *transStreamPublisher) CreateDefaultOutStreams() {
|
func (t *transStreamPublisher) CreateDefaultOutStreams() {
|
||||||
if t.transStreams == nil {
|
if t.transStreams == nil {
|
||||||
t.transStreams = make(map[TransStreamID]TransStream, 10)
|
t.transStreams = make(map[TransStreamID]TransStream, 10)
|
||||||
@@ -172,13 +211,7 @@ func (t *transStreamPublisher) CreateDefaultOutStreams() {
|
|||||||
|
|
||||||
// 创建录制流
|
// 创建录制流
|
||||||
if AppConfig.Record.Enable {
|
if AppConfig.Record.Enable {
|
||||||
sink, path, err := CreateRecordStream(t.source)
|
t.createRecordSink()
|
||||||
if err != nil {
|
|
||||||
log.Sugar.Errorf("创建录制sink失败 source: %s err: %s", t.source, err.Error())
|
|
||||||
} else {
|
|
||||||
t.recordSink = sink
|
|
||||||
t.recordFilePath = path
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建HLS输出流
|
// 创建HLS输出流
|
||||||
@@ -628,12 +661,12 @@ func (t *transStreamPublisher) clearSinkStreaming(sink Sink) {
|
|||||||
delete(transStreamSinks, sink.GetID())
|
delete(transStreamSinks, sink.GetID())
|
||||||
t.lastStreamEndTime = time.Now()
|
t.lastStreamEndTime = time.Now()
|
||||||
sink.StopStreaming(t.transStreams[sink.GetTransStreamID()])
|
sink.StopStreaming(t.transStreams[sink.GetTransStreamID()])
|
||||||
|
delete(t.sinks, sink.GetID())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *transStreamPublisher) doRemoveSink(sink Sink) bool {
|
func (t *transStreamPublisher) doRemoveSink(sink Sink) bool {
|
||||||
if _, ok := t.sinks[sink.GetID()]; ok {
|
if _, ok := t.sinks[sink.GetID()]; ok {
|
||||||
t.clearSinkStreaming(sink)
|
t.clearSinkStreaming(sink)
|
||||||
delete(t.sinks, sink.GetID())
|
|
||||||
|
|
||||||
t.sinkCount--
|
t.sinkCount--
|
||||||
log.Sugar.Infof("sink count: %d source: %s", t.sinkCount, t.source)
|
log.Sugar.Infof("sink count: %d source: %s", t.sinkCount, t.source)
|
||||||
@@ -873,6 +906,49 @@ func (t *transStreamPublisher) SetSourceID(id string) {
|
|||||||
t.source = id
|
t.source = id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *transStreamPublisher) StartRecord() (string, bool) {
|
||||||
|
if AppConfig.Record.Enable || t.recordSink != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
var ok bool
|
||||||
|
t.ExecuteSyncEvent(func() {
|
||||||
|
if t.recordSink == nil && t.createRecordSink() {
|
||||||
|
ok = t.doAddSink(t.recordSink, false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
var url string
|
||||||
|
if ok {
|
||||||
|
// 去掉反斜杠
|
||||||
|
url = GenerateRecordStreamPlayUrl(filepath.ToSlash(t.recordFilePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
return url, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *transStreamPublisher) StopRecord() error {
|
||||||
|
if AppConfig.Record.Enable {
|
||||||
|
return fmt.Errorf("录制常开")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.ExecuteSyncEvent(func() {
|
||||||
|
if t.recordSink != nil {
|
||||||
|
t.clearSinkStreaming(t.recordSink)
|
||||||
|
t.recordSink.Close()
|
||||||
|
t.recordSink = nil
|
||||||
|
t.recordFilePath = ""
|
||||||
|
t.recordStartTime = time.Time{}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *transStreamPublisher) RecordStartTime() time.Time {
|
||||||
|
return t.recordStartTime
|
||||||
|
}
|
||||||
|
|
||||||
func NewTransStreamPublisher(source string) TransStreamPublisher {
|
func NewTransStreamPublisher(source string) TransStreamPublisher {
|
||||||
return &transStreamPublisher{
|
return &transStreamPublisher{
|
||||||
transStreams: make(map[TransStreamID]TransStream),
|
transStreams: make(map[TransStreamID]TransStream),
|
||||||
|
Reference in New Issue
Block a user