mirror of
https://github.com/lkmio/lkm.git
synced 2025-10-05 23:26:51 +08:00
完善sink断开处理
This commit is contained in:
212
api.go
212
api.go
@@ -1,17 +1,19 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/yangjiechina/avformat/utils"
|
"github.com/yangjiechina/avformat/utils"
|
||||||
"github.com/yangjiechina/live-server/flv"
|
"github.com/yangjiechina/live-server/flv"
|
||||||
|
"github.com/yangjiechina/live-server/hls"
|
||||||
|
"github.com/yangjiechina/live-server/log"
|
||||||
"github.com/yangjiechina/live-server/rtc"
|
"github.com/yangjiechina/live-server/rtc"
|
||||||
"github.com/yangjiechina/live-server/stream"
|
"github.com/yangjiechina/live-server/stream"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -29,10 +31,51 @@ func init() {
|
|||||||
|
|
||||||
func startApiServer(addr string) {
|
func startApiServer(addr string) {
|
||||||
r := mux.NewRouter()
|
r := mux.NewRouter()
|
||||||
r.HandleFunc("/live/flv/{source}", onFLV)
|
/**
|
||||||
r.HandleFunc("/live/hls/{source}", onHLS)
|
http://host:port/xxx.flv
|
||||||
r.HandleFunc("/live/rtc/{source}", onRtc)
|
http://host:port/xxx.rtc
|
||||||
r.HandleFunc("/live/flv/ws/{source}", onWSFlv)
|
http://host:port/xxx.m3u8
|
||||||
|
http://host:port/xxx_0.ts
|
||||||
|
ws://host:port/xxx.flv
|
||||||
|
*/
|
||||||
|
r.HandleFunc("/live/{source}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
vars := mux.Vars(r)
|
||||||
|
source := vars["source"]
|
||||||
|
index := strings.LastIndex(source, ".")
|
||||||
|
if index < 0 || index == len(source)-1 {
|
||||||
|
log.Sugar.Errorf("bad request:%s. stream format must be passed at the end of the URL", r.URL.Path)
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceId := source[:index]
|
||||||
|
format := source[index+1:]
|
||||||
|
|
||||||
|
if "flv" == format {
|
||||||
|
//判断是否是websocket请求
|
||||||
|
ws := true
|
||||||
|
if !("upgrade" == strings.ToLower(r.Header.Get("Connection"))) {
|
||||||
|
ws = false
|
||||||
|
} else if !("websocket" == strings.ToLower(r.Header.Get("Upgrade"))) {
|
||||||
|
ws = false
|
||||||
|
} else if !("13" == r.Header.Get("Sec-Websocket-Version")) {
|
||||||
|
ws = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ws {
|
||||||
|
onWSFlv(sourceId, w, r)
|
||||||
|
} else {
|
||||||
|
onFLV(sourceId, w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if "m3u8" == format {
|
||||||
|
onHLS(sourceId, w, r)
|
||||||
|
} else if "ts" == format {
|
||||||
|
onTS(sourceId, w, r)
|
||||||
|
} else if "rtc" == format {
|
||||||
|
onRtc(sourceId, w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
r.HandleFunc("/rtc.html", func(writer http.ResponseWriter, request *http.Request) {
|
r.HandleFunc("/rtc.html", func(writer http.ResponseWriter, request *http.Request) {
|
||||||
http.ServeFile(writer, request, "./rtc.html")
|
http.ServeFile(writer, request, "./rtc.html")
|
||||||
@@ -54,40 +97,40 @@ func startApiServer(addr string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func onWSFlv(w http.ResponseWriter, r *http.Request) {
|
func onWSFlv(sourceId string, w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "video/x-flv")
|
|
||||||
w.Header().Set("Connection", "Keep-Alive")
|
|
||||||
w.Header().Set("Transfer-Encoding", "chunked")
|
|
||||||
|
|
||||||
conn, err := upgrader.Upgrade(w, r, nil)
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
log.Sugar.Errorf("websocket头检查失败 err:%s", err.Error())
|
||||||
}
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
vars := mux.Vars(r)
|
|
||||||
sourceId := vars["source"]
|
|
||||||
if index := strings.LastIndex(sourceId, "."); index > -1 {
|
|
||||||
sourceId = sourceId[:index]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tcpAddr, _ := net.ResolveTCPAddr("tcp", r.RemoteAddr)
|
tcpAddr, _ := net.ResolveTCPAddr("tcp", r.RemoteAddr)
|
||||||
sinkId := stream.GenerateSinkId(tcpAddr)
|
sinkId := stream.GenerateSinkId(tcpAddr)
|
||||||
sink := flv.NewFLVSink(sinkId, sourceId, flv.NewWSConn(conn))
|
sink := flv.NewFLVSink(sinkId, sourceId, flv.NewWSConn(conn))
|
||||||
|
|
||||||
go func() {
|
log.Sugar.Infof("ws-flv 连接 sink:%s", sink.PrintInfo())
|
||||||
|
|
||||||
sink.(*stream.SinkImpl).Play(sink, func() {
|
sink.(*stream.SinkImpl).Play(sink, func() {
|
||||||
//sink.(*stream.SinkImpl).PlayDone(sink, nil, nil)
|
|
||||||
}, func(state utils.HookState) {
|
}, func(state utils.HookState) {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
|
||||||
conn.Close()
|
conn.Close()
|
||||||
})
|
})
|
||||||
}()
|
|
||||||
|
|
||||||
|
netConn := conn.NetConn()
|
||||||
|
bytes := make([]byte, 64)
|
||||||
for {
|
for {
|
||||||
select {}
|
if _, err := netConn.Read(bytes); err != nil {
|
||||||
|
log.Sugar.Infof("ws-flv 断开连接 sink:%s", sink.PrintInfo())
|
||||||
|
sink.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func onFLV(w http.ResponseWriter, r *http.Request) {
|
func onFLV(sourceId string, w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "video/x-flv")
|
w.Header().Set("Content-Type", "video/x-flv")
|
||||||
w.Header().Set("Connection", "Keep-Alive")
|
w.Header().Set("Connection", "Keep-Alive")
|
||||||
w.Header().Set("Transfer-Encoding", "chunked")
|
w.Header().Set("Transfer-Encoding", "chunked")
|
||||||
@@ -97,58 +140,112 @@ func onFLV(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError)
|
http.Error(w, "webserver doesn't support hijacking", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
context_ := r.Context()
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
conn, _, err := hj.Hijack()
|
conn, _, err := hj.Hijack()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
vars := mux.Vars(r)
|
|
||||||
sourceId := vars["source"]
|
|
||||||
if index := strings.LastIndex(sourceId, "."); index > -1 {
|
|
||||||
sourceId = sourceId[:index]
|
|
||||||
}
|
|
||||||
|
|
||||||
tcpAddr, _ := net.ResolveTCPAddr("tcp", r.RemoteAddr)
|
tcpAddr, _ := net.ResolveTCPAddr("tcp", r.RemoteAddr)
|
||||||
sinkId := stream.GenerateSinkId(tcpAddr)
|
sinkId := stream.GenerateSinkId(tcpAddr)
|
||||||
sink := flv.NewFLVSink(sinkId, sourceId, conn)
|
sink := flv.NewFLVSink(sinkId, sourceId, conn)
|
||||||
|
|
||||||
go func(ctx context.Context) {
|
log.Sugar.Infof("http-flv 连接 sink:%s", sink.PrintInfo())
|
||||||
sink.(*stream.SinkImpl).Play(sink, func() {
|
sink.(*stream.SinkImpl).Play(sink, func() {
|
||||||
//sink.(*stream.SinkImpl).PlayDone(sink, nil, nil)
|
|
||||||
}, func(state utils.HookState) {
|
}, func(state utils.HookState) {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
|
||||||
conn.Close()
|
conn.Close()
|
||||||
})
|
})
|
||||||
}(context_)
|
|
||||||
|
bytes := make([]byte, 64)
|
||||||
|
for {
|
||||||
|
if _, err := conn.Read(bytes); err != nil {
|
||||||
|
log.Sugar.Infof("http-flv 断开连接 sink:%s", sink.PrintInfo())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func onHLS(w http.ResponseWriter, r *http.Request) {
|
func onTS(source string, w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
if !stream.AppConfig.Hls.Enable {
|
||||||
source := vars["source"]
|
log.Sugar.Warnf("处理m3u8请求失败 server未开启hls request:%s", r.URL.Path)
|
||||||
|
http.Error(w, "hls disable", http.StatusInternalServerError)
|
||||||
w.Header().Set("Content-Type", "application/vnd.apple.mpegurl")
|
return
|
||||||
|
|
||||||
//删除末尾的.ts/.m3u8, 请确保id中不存在.
|
|
||||||
//var sourceId string
|
|
||||||
//if index := strings.LastIndex(source, "."); index > -1 {
|
|
||||||
// sourceId = source[:index]
|
|
||||||
//}
|
|
||||||
//
|
|
||||||
//tcpAddr, _ := net.ResolveTCPAddr("tcp", r.RemoteAddr)
|
|
||||||
//sinkId := stream.GenerateSinkId(tcpAddr)
|
|
||||||
if strings.HasSuffix(source, ".m3u8") {
|
|
||||||
//查询是否存在hls流, 不存在-等生成后再响应m3u8文件. 存在-直接响应m3u8文件
|
|
||||||
http.ServeFile(w, r, "../tmp/"+source)
|
|
||||||
} else if strings.HasSuffix(source, ".ts") {
|
|
||||||
http.ServeFile(w, r, "../tmp/"+source)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
index := strings.LastIndex(source, "_")
|
||||||
|
if index < 0 || index == len(source)-1 {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
seq := source[index+1:]
|
||||||
|
sourceId := source[:index]
|
||||||
|
tsPath := stream.AppConfig.Hls.TSPath(sourceId, seq)
|
||||||
|
if _, err := os.Stat(tsPath); err != nil {
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//链路复用无法获取http断开回调
|
||||||
|
//Hijack需要自行解析http
|
||||||
|
http.ServeFile(w, r, tsPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func onRtc(w http.ResponseWriter, r *http.Request) {
|
func onHLS(sourceId string, w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !stream.AppConfig.Hls.Enable {
|
||||||
|
log.Sugar.Warnf("处理hls请求失败 server未开启hls request:%s", r.URL.Path)
|
||||||
|
http.Error(w, "hls disable", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/vnd.apple.mpegurl")
|
||||||
|
//m3u8和ts会一直刷新, 每个请求只hook一次.
|
||||||
|
tcpAddr, _ := net.ResolveTCPAddr("tcp", r.RemoteAddr)
|
||||||
|
sinkId := stream.GenerateSinkId(tcpAddr)
|
||||||
|
|
||||||
|
//hook成功后, 如果还没有m3u8文件,等生成m3u8文件
|
||||||
|
//后续直接返回当前m3u8文件
|
||||||
|
if stream.SinkManager.Exist(sinkId) {
|
||||||
|
http.ServeFile(w, r, stream.AppConfig.Hls.M3U8Path(sourceId))
|
||||||
|
} else {
|
||||||
|
context := r.Context()
|
||||||
|
done := make(chan int, 0)
|
||||||
|
|
||||||
|
sink := hls.NewM3U8Sink(sinkId, sourceId, func(m3u8 []byte) {
|
||||||
|
w.Write(m3u8)
|
||||||
|
done <- 0
|
||||||
|
})
|
||||||
|
|
||||||
|
hookState := utils.HookStateOK
|
||||||
|
sink.Play(sink, func() {
|
||||||
|
err := stream.SinkManager.Add(sink)
|
||||||
|
|
||||||
|
utils.Assert(err == nil)
|
||||||
|
}, func(state utils.HookState) {
|
||||||
|
log.Sugar.Warnf("hook播放事件失败 request:%s", r.URL.Path)
|
||||||
|
hookState = state
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
})
|
||||||
|
|
||||||
|
if utils.HookStateOK != hookState {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-context.Done():
|
||||||
|
log.Sugar.Infof("http m3u8连接断开")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func onRtc(sourceId string, w http.ResponseWriter, r *http.Request) {
|
||||||
v := struct {
|
v := struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
SDP string `json:"sdp"`
|
SDP string `json:"sdp"`
|
||||||
@@ -163,12 +260,12 @@ func onRtc(w http.ResponseWriter, r *http.Request) {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sinkId := stream.SinkId(123)
|
tcpAddr, _ := net.ResolveTCPAddr("tcp", r.RemoteAddr)
|
||||||
split := strings.Split(r.URL.Path, "/")
|
sinkId := stream.GenerateSinkId(tcpAddr)
|
||||||
|
|
||||||
group := sync.WaitGroup{}
|
group := sync.WaitGroup{}
|
||||||
group.Add(1)
|
group.Add(1)
|
||||||
sink := rtc.NewSink(sinkId, split[len(split)-1], v.SDP, func(sdp string) {
|
sink := rtc.NewSink(sinkId, sourceId, v.SDP, func(sdp string) {
|
||||||
response := struct {
|
response := struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
SDP string `json:"sdp"`
|
SDP string `json:"sdp"`
|
||||||
@@ -191,7 +288,10 @@ func onRtc(w http.ResponseWriter, r *http.Request) {
|
|||||||
sink.Play(sink, func() {
|
sink.Play(sink, func() {
|
||||||
|
|
||||||
}, func(state utils.HookState) {
|
}, func(state utils.HookState) {
|
||||||
|
w.WriteHeader(http.StatusForbidden)
|
||||||
|
|
||||||
group.Done()
|
group.Done()
|
||||||
})
|
})
|
||||||
|
|
||||||
group.Wait()
|
group.Wait()
|
||||||
}
|
}
|
||||||
|
@@ -2,37 +2,30 @@ package hls
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/yangjiechina/live-server/stream"
|
"github.com/yangjiechina/live-server/stream"
|
||||||
"net/http"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type sink struct {
|
type tsSink struct {
|
||||||
stream.SinkImpl
|
stream.SinkImpl
|
||||||
conn http.ResponseWriter
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSink(id stream.SinkId, sourceId string, w http.ResponseWriter) stream.ISink {
|
func NewTSSink(id stream.SinkId, sourceId string) stream.ISink {
|
||||||
return &sink{stream.SinkImpl{Id_: id, SourceId_: sourceId, Protocol_: stream.ProtocolHls}, w}
|
return &tsSink{stream.SinkImpl{Id_: id, SourceId_: sourceId, Protocol_: stream.ProtocolHls}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sink) Input(data []byte) error {
|
func (s *tsSink) Input(data []byte) error {
|
||||||
if s.conn != nil {
|
|
||||||
_, err := s.conn.Write(data)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type m3u8Sink struct {
|
type m3u8Sink struct {
|
||||||
stream.SinkImpl
|
stream.SinkImpl
|
||||||
|
cb func(m3u8 []byte)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *m3u8Sink) Input(data []byte) error {
|
func (s *m3u8Sink) Input(data []byte) error {
|
||||||
|
s.cb(data)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewM3U8Sink(id stream.SinkId, sourceId string, w http.ResponseWriter) stream.ISink {
|
func NewM3U8Sink(id stream.SinkId, sourceId string, cb func(m3u8 []byte)) stream.ISink {
|
||||||
return &m3u8Sink{stream.SinkImpl{Id_: id, SourceId_: sourceId, Protocol_: stream.ProtocolHls}}
|
return &m3u8Sink{stream.SinkImpl{Id_: id, SourceId_: sourceId, Protocol_: stream.ProtocolHls}, cb}
|
||||||
}
|
}
|
||||||
|
@@ -32,12 +32,14 @@ type transStream struct {
|
|||||||
duration int
|
duration int
|
||||||
m3u8File *os.File
|
m3u8File *os.File
|
||||||
playlistLength int
|
playlistLength int
|
||||||
|
|
||||||
|
m3u8Sinks map[stream.SinkId]stream.ISink
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTransStream 创建HLS传输流
|
// NewTransStream 创建HLS传输流
|
||||||
// @url url前缀
|
// @url url前缀
|
||||||
// @m3u8Name m3u8文件名
|
// @m3u8Name m3u8文件名
|
||||||
// @tsFormat ts文件格式, 例如: test_%d.ts
|
// @tsFormat ts文件格式, 例如: %d.ts
|
||||||
// @parentDir 保存切片的绝对路径. mu38和ts切片放在同一目录下, 目录地址使用parentDir+urlPrefix
|
// @parentDir 保存切片的绝对路径. mu38和ts切片放在同一目录下, 目录地址使用parentDir+urlPrefix
|
||||||
// @segmentDuration 单个切片时长
|
// @segmentDuration 单个切片时长
|
||||||
// @playlistLength 缓存多少个切片
|
// @playlistLength 缓存多少个切片
|
||||||
@@ -47,6 +49,7 @@ func NewTransStream(url, m3u8Name, tsFormat, dir string, segmentDuration, playli
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//创建m3u8文件
|
||||||
m3u8Path := fmt.Sprintf("%s/%s", dir, m3u8Name)
|
m3u8Path := fmt.Sprintf("%s/%s", dir, m3u8Name)
|
||||||
file, err := os.OpenFile(m3u8Path, os.O_CREATE|os.O_RDWR, 0666)
|
file, err := os.OpenFile(m3u8Path, os.O_CREATE|os.O_RDWR, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -62,6 +65,7 @@ func NewTransStream(url, m3u8Name, tsFormat, dir string, segmentDuration, playli
|
|||||||
playlistLength: playlistLength,
|
playlistLength: playlistLength,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//创建TS封装器
|
||||||
muxer := libmpeg.NewTSMuxer()
|
muxer := libmpeg.NewTSMuxer()
|
||||||
muxer.SetWriteHandler(stream_.onTSWrite)
|
muxer.SetWriteHandler(stream_.onTSWrite)
|
||||||
muxer.SetAllocHandler(stream_.onTSAlloc)
|
muxer.SetAllocHandler(stream_.onTSAlloc)
|
||||||
@@ -75,6 +79,8 @@ func NewTransStream(url, m3u8Name, tsFormat, dir string, segmentDuration, playli
|
|||||||
stream_.muxer = muxer
|
stream_.muxer = muxer
|
||||||
stream_.m3u8 = NewM3U8Writer(playlistLength)
|
stream_.m3u8 = NewM3U8Writer(playlistLength)
|
||||||
stream_.m3u8File = file
|
stream_.m3u8File = file
|
||||||
|
|
||||||
|
stream_.m3u8Sinks = make(map[stream.SinkId]stream.ISink, 24)
|
||||||
return stream_, nil
|
return stream_, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,10 +96,12 @@ func (t *transStream) Input(packet utils.AVPacket) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pts := packet.ConvertPts(90000)
|
||||||
|
dts := packet.ConvertDts(90000)
|
||||||
if utils.AVMediaTypeVideo == packet.MediaType() {
|
if utils.AVMediaTypeVideo == packet.MediaType() {
|
||||||
return t.muxer.Input(packet.Index(), packet.AnnexBPacketData(), packet.Pts()*90, packet.Dts()*90, packet.KeyFrame())
|
return t.muxer.Input(packet.Index(), packet.AnnexBPacketData(), pts, dts, packet.KeyFrame())
|
||||||
} else {
|
} else {
|
||||||
return t.muxer.Input(packet.Index(), packet.Data(), packet.Pts()*90, packet.Dts()*90, packet.KeyFrame())
|
return t.muxer.Input(packet.Index(), packet.Data(), pts, dts, packet.KeyFrame())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,9 +125,24 @@ func (t *transStream) AddTrack(stream utils.AVStream) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *transStream) WriteHeader() error {
|
func (t *transStream) WriteHeader() error {
|
||||||
|
t.Init()
|
||||||
|
|
||||||
return t.createSegment()
|
return t.createSegment()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *transStream) AddSink(sink stream.ISink) error {
|
||||||
|
if sink_, ok := sink.(*m3u8Sink); ok {
|
||||||
|
if t.m3u8.Size() > 0 {
|
||||||
|
return sink.Input([]byte(t.m3u8.ToString()))
|
||||||
|
} else {
|
||||||
|
t.m3u8Sinks[sink.Id()] = sink_
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return t.TransStreamImpl.AddSink(sink)
|
||||||
|
}
|
||||||
|
|
||||||
func (t *transStream) onTSWrite(data []byte) {
|
func (t *transStream) onTSWrite(data []byte) {
|
||||||
t.context.writeBufferSize += len(data)
|
t.context.writeBufferSize += len(data)
|
||||||
}
|
}
|
||||||
@@ -166,22 +189,33 @@ func (t *transStream) flushSegment() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//通知等待m3u8的sink
|
||||||
|
if len(t.m3u8Sinks) > 0 {
|
||||||
|
for _, sink := range t.m3u8Sinks {
|
||||||
|
sink.Input([]byte(m3u8Txt))
|
||||||
|
}
|
||||||
|
t.m3u8Sinks = make(map[stream.SinkId]stream.ISink, 0)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 创建一个新的ts切片
|
||||||
func (t *transStream) createSegment() error {
|
func (t *transStream) createSegment() error {
|
||||||
|
//保存上一个ts切片
|
||||||
if t.context.file != nil {
|
if t.context.file != nil {
|
||||||
err := t.flushSegment()
|
err := t.flushSegment()
|
||||||
t.context.segmentSeq++
|
t.context.segmentSeq++
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tsName := fmt.Sprintf(t.tsFormat, t.context.segmentSeq)
|
tsName := fmt.Sprintf(t.tsFormat, t.context.segmentSeq)
|
||||||
t.context.path = fmt.Sprintf("%s%s", t.dir, tsName)
|
//ts文件
|
||||||
|
t.context.path = fmt.Sprintf("%s/%s", t.dir, tsName)
|
||||||
|
//m3u8中的url
|
||||||
t.context.url = fmt.Sprintf("%s%s", t.url, tsName)
|
t.context.url = fmt.Sprintf("%s%s", t.url, tsName)
|
||||||
|
|
||||||
file, err := os.OpenFile(t.context.path, os.O_WRONLY|os.O_CREATE, 0666)
|
file, err := os.OpenFile(t.context.path, os.O_WRONLY|os.O_CREATE, 0666)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@@ -115,7 +115,7 @@ func (m *m3u8Writer) ToString() string {
|
|||||||
m.stringBuffer.WriteString("#EXT-X-TARGETDURATION:")
|
m.stringBuffer.WriteString("#EXT-X-TARGETDURATION:")
|
||||||
m.stringBuffer.WriteString(strconv.Itoa(m.targetDuration()))
|
m.stringBuffer.WriteString(strconv.Itoa(m.targetDuration()))
|
||||||
m.stringBuffer.WriteString("\r\n")
|
m.stringBuffer.WriteString("\r\n")
|
||||||
m.stringBuffer.WriteString("#ExtXMediaSequence:")
|
m.stringBuffer.WriteString("#Ext-X-MEDIA-SEQUENCE:")
|
||||||
m.stringBuffer.WriteString(strconv.Itoa(head[0].(Segment).sequence))
|
m.stringBuffer.WriteString(strconv.Itoa(head[0].(Segment).sequence))
|
||||||
m.stringBuffer.WriteString("\r\n")
|
m.stringBuffer.WriteString("\r\n")
|
||||||
|
|
||||||
|
9
main.go
9
main.go
@@ -25,10 +25,8 @@ func CreateTransStream(source stream.ISource, protocol stream.Protocol, streams
|
|||||||
return rtmp.NewTransStream(librtmp.ChunkSize)
|
return rtmp.NewTransStream(librtmp.ChunkSize)
|
||||||
} else if stream.ProtocolHls == protocol {
|
} else if stream.ProtocolHls == protocol {
|
||||||
id := source.Id()
|
id := source.Id()
|
||||||
m3u8Name := id + ".m3u8"
|
|
||||||
tsFormat := id + "_%d.ts"
|
|
||||||
|
|
||||||
transStream, err := hls.NewTransStream("", m3u8Name, tsFormat, "../tmp/", 2, 10)
|
transStream, err := hls.NewTransStream("", stream.AppConfig.Hls.M3U8Format(id), stream.AppConfig.Hls.TSFormat(id, "%d"), stream.AppConfig.Hls.Dir, stream.AppConfig.Hls.Duration, stream.AppConfig.Hls.PlaylistLength)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -60,6 +58,11 @@ func main() {
|
|||||||
stream.AppConfig.GOPCache = true
|
stream.AppConfig.GOPCache = true
|
||||||
stream.AppConfig.MergeWriteLatency = 350
|
stream.AppConfig.MergeWriteLatency = 350
|
||||||
|
|
||||||
|
stream.AppConfig.Hls.Enable = true
|
||||||
|
stream.AppConfig.Hls.Dir = "../tmp"
|
||||||
|
stream.AppConfig.Hls.Duration = 2
|
||||||
|
stream.AppConfig.Hls.PlaylistLength = 10
|
||||||
|
|
||||||
rtmpAddr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:1935")
|
rtmpAddr, err := net.ResolveTCPAddr("tcp", "0.0.0.0:1935")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
@@ -63,11 +63,11 @@ func (t *transStream) AddSink(sink_ stream.ISink) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if _, err := connection.AddTransceiverFromTrack(videoTrack, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly}); err != nil {
|
if _, err := connection.AddTransceiverFromTrack(videoTrack, webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly}); err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = connection.AddTrack(videoTrack); err != nil {
|
if _, err = connection.AddTrack(videoTrack); err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
rtcSink.addTrack(index, videoTrack)
|
rtcSink.addTrack(index, videoTrack)
|
||||||
@@ -80,14 +80,17 @@ func (t *transStream) AddSink(sink_ stream.ISink) error {
|
|||||||
complete := webrtc.GatheringCompletePromise(connection)
|
complete := webrtc.GatheringCompletePromise(connection)
|
||||||
answer, err := connection.CreateAnswer(nil)
|
answer, err := connection.CreateAnswer(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
} else if err = connection.SetLocalDescription(answer); err != nil {
|
} else if err = connection.SetLocalDescription(answer); err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
<-complete
|
<-complete
|
||||||
|
|
||||||
connection.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
connection.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
||||||
rtcSink.state = state
|
rtcSink.state = state
|
||||||
|
if webrtc.ICEConnectionStateDisconnected > state {
|
||||||
|
rtcSink.Close()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
rtcSink.peer = connection
|
rtcSink.peer = connection
|
||||||
|
@@ -9,6 +9,36 @@ type RtmpConfig struct {
|
|||||||
Addr string `json:"addr"`
|
Addr string `json:"addr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RecordConfig struct {
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
Format string `json:"format"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HlsConfig struct {
|
||||||
|
Enable bool
|
||||||
|
Dir string
|
||||||
|
Duration int
|
||||||
|
PlaylistLength int
|
||||||
|
}
|
||||||
|
|
||||||
|
// M3U8Path 根据sourceId返回m3u8的磁盘路径
|
||||||
|
func (c HlsConfig) M3U8Path(sourceId string) string {
|
||||||
|
return c.Dir + "/" + c.M3U8Format(sourceId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c HlsConfig) M3U8Format(sourceId string) string {
|
||||||
|
return sourceId + ".m3u8"
|
||||||
|
}
|
||||||
|
|
||||||
|
// TSPath 根据sourceId和ts文件名返回ts的磁盘路径
|
||||||
|
func (c HlsConfig) TSPath(sourceId string, tsSeq string) string {
|
||||||
|
return c.Dir + "/" + c.TSFormat(sourceId, tsSeq)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c HlsConfig) TSFormat(sourceId string, tsSeq string) string {
|
||||||
|
return sourceId + "_" + tsSeq + ".ts"
|
||||||
|
}
|
||||||
|
|
||||||
type HookConfig struct {
|
type HookConfig struct {
|
||||||
Time int
|
Time int
|
||||||
Enable bool `json:"enable"`
|
Enable bool `json:"enable"`
|
||||||
@@ -65,4 +95,7 @@ type AppConfig_ struct {
|
|||||||
MergeWriteLatency int `json:"mw_latency"`
|
MergeWriteLatency int `json:"mw_latency"`
|
||||||
Rtmp RtmpConfig
|
Rtmp RtmpConfig
|
||||||
Hook HookConfig
|
Hook HookConfig
|
||||||
|
|
||||||
|
Record RecordConfig
|
||||||
|
Hls HlsConfig
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
package stream
|
package stream
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/yangjiechina/avformat/utils"
|
"github.com/yangjiechina/avformat/utils"
|
||||||
"github.com/yangjiechina/live-server/log"
|
"github.com/yangjiechina/live-server/log"
|
||||||
"net"
|
"net"
|
||||||
@@ -43,7 +44,10 @@ type ISink interface {
|
|||||||
// DesiredVideoCodecId DescribeVideoCodecId 允许客户端拉取指定的视频流
|
// DesiredVideoCodecId DescribeVideoCodecId 允许客户端拉取指定的视频流
|
||||||
DesiredVideoCodecId() utils.AVCodecID
|
DesiredVideoCodecId() utils.AVCodecID
|
||||||
|
|
||||||
|
// Close 关闭释放Sink, 从传输流或等待队列中删除sink
|
||||||
Close()
|
Close()
|
||||||
|
|
||||||
|
PrintInfo() string
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateSinkId 根据网络地址生成SinkId IPV4使用一个uint64, IPV6使用String
|
// GenerateSinkId 根据网络地址生成SinkId IPV4使用一个uint64, IPV6使用String
|
||||||
@@ -184,6 +188,10 @@ func (s *SinkImpl) Close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SinkImpl) PrintInfo() string {
|
||||||
|
return fmt.Sprintf("%s-%v source:%s", s.ProtocolStr(), s.Id_, s.SourceId_)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SinkImpl) Play(sink ISink, success func(), failure func(state utils.HookState)) {
|
func (s *SinkImpl) Play(sink ISink, success func(), failure func(state utils.HookState)) {
|
||||||
f := func() {
|
f := func() {
|
||||||
source := SourceManager.Find(sink.SourceId())
|
source := SourceManager.Find(sink.SourceId())
|
||||||
|
@@ -91,7 +91,7 @@ func ExistSink(sourceId string, sinkId SinkId) bool {
|
|||||||
|
|
||||||
// ISinkManager 添加到TransStream的所有Sink
|
// ISinkManager 添加到TransStream的所有Sink
|
||||||
type ISinkManager interface {
|
type ISinkManager interface {
|
||||||
Add(source ISink) error
|
Add(sink ISink) error
|
||||||
|
|
||||||
Find(id SinkId) ISink
|
Find(id SinkId) ISink
|
||||||
|
|
||||||
@@ -110,10 +110,10 @@ type sinkManagerImpl struct {
|
|||||||
m sync.Map
|
m sync.Map
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sinkManagerImpl) Add(source ISink) error {
|
func (s *sinkManagerImpl) Add(sink ISink) error {
|
||||||
_, ok := s.m.LoadOrStore(source.Id(), source)
|
_, ok := s.m.LoadOrStore(sink.Id(), sink)
|
||||||
if ok {
|
if ok {
|
||||||
return fmt.Errorf("the source %s has been exist", source.Id())
|
return fmt.Errorf("the sink %s has been exist", sink.Id())
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@@ -132,13 +132,14 @@ type SourceImpl struct {
|
|||||||
|
|
||||||
TransDeMuxer stream.DeMuxer //负责从推流协议中解析出AVStream和AVPacket
|
TransDeMuxer stream.DeMuxer //负责从推流协议中解析出AVStream和AVPacket
|
||||||
recordSink ISink //每个Source唯一的一个录制流
|
recordSink ISink //每个Source唯一的一个录制流
|
||||||
|
hlsStream ITransStream //hls不等拉流,创建时直接生成
|
||||||
audioTranscoders []transcode.ITranscoder //音频解码器
|
audioTranscoders []transcode.ITranscoder //音频解码器
|
||||||
videoTranscoders []transcode.ITranscoder //视频解码器
|
videoTranscoders []transcode.ITranscoder //视频解码器
|
||||||
originStreams StreamManager //推流的音视频Streams
|
originStreams StreamManager //推流的音视频Streams
|
||||||
allStreams StreamManager //推流Streams+转码器获得的Streams
|
allStreams StreamManager //推流Streams+转码器获得的Streams
|
||||||
buffers []StreamBuffer
|
buffers []StreamBuffer
|
||||||
|
|
||||||
Input_ func(data []byte) //解决无法多态传递给子类的问题
|
Input_ func(data []byte) //解决多态无法传递给子类的问题
|
||||||
|
|
||||||
completed bool
|
completed bool
|
||||||
mutex sync.Mutex //只用作AddStream期间
|
mutex sync.Mutex //只用作AddStream期间
|
||||||
@@ -154,8 +155,6 @@ type SourceImpl struct {
|
|||||||
closeEvent chan byte
|
closeEvent chan byte
|
||||||
playingEventQueue chan ISink
|
playingEventQueue chan ISink
|
||||||
playingDoneEventQueue chan ISink
|
playingDoneEventQueue chan ISink
|
||||||
|
|
||||||
testTransStream ITransStream
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SourceImpl) Id() string {
|
func (s *SourceImpl) Id() string {
|
||||||
@@ -175,9 +174,17 @@ func (s *SourceImpl) Init() {
|
|||||||
if s.transStreams == nil {
|
if s.transStreams == nil {
|
||||||
s.transStreams = make(map[TransStreamId]ITransStream, 10)
|
s.transStreams = make(map[TransStreamId]ITransStream, 10)
|
||||||
}
|
}
|
||||||
//测试传输流
|
|
||||||
s.testTransStream = TransStreamFactory(s, ProtocolHls, nil)
|
//创建录制流
|
||||||
s.transStreams[0x100] = s.testTransStream
|
if AppConfig.Record.Enable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//创建HLS输出流
|
||||||
|
if AppConfig.Hls.Enable {
|
||||||
|
s.hlsStream = TransStreamFactory(s, ProtocolHls, nil)
|
||||||
|
s.transStreams[0x100] = s.hlsStream
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SourceImpl) LoopEvent() {
|
func (s *SourceImpl) LoopEvent() {
|
||||||
@@ -220,7 +227,7 @@ func IsSupportMux(protocol Protocol, audioCodecId, videoCodecId utils.AVCodecID)
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// 分发每路Stream的Buffer给传输流
|
// 分发每路StreamBuffer给传输流
|
||||||
// 按照时间戳升序发送
|
// 按照时间戳升序发送
|
||||||
func (s *SourceImpl) dispatchStreamBuffer(transStream ITransStream, streams []utils.AVStream) {
|
func (s *SourceImpl) dispatchStreamBuffer(transStream ITransStream, streams []utils.AVStream) {
|
||||||
size := len(streams)
|
size := len(streams)
|
||||||
@@ -447,12 +454,12 @@ func (s *SourceImpl) writeHeader() {
|
|||||||
s.AddSink(sink)
|
s.AddSink(sink)
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.testTransStream != nil {
|
if s.hlsStream != nil {
|
||||||
for _, stream_ := range s.originStreams.All() {
|
for _, stream_ := range s.originStreams.All() {
|
||||||
s.testTransStream.AddTrack(stream_)
|
s.hlsStream.AddTrack(stream_)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.testTransStream.WriteHeader()
|
s.hlsStream.WriteHeader()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user