mirror of
https://github.com/lkmio/lkm.git
synced 2025-09-26 19:21:14 +08:00
Compare commits
32 Commits
ece4663362
...
dev
Author | SHA1 | Date | |
---|---|---|---|
![]() |
6ff273c9e5 | ||
![]() |
fc5b8f5a1b | ||
![]() |
ef313b1ea4 | ||
![]() |
a5b7fc6f24 | ||
![]() |
69308c466b | ||
![]() |
7150525c20 | ||
![]() |
f526be83e9 | ||
![]() |
c5851a0e01 | ||
![]() |
6ce491445d | ||
![]() |
eeaeee14d5 | ||
![]() |
0ce4f3d607 | ||
![]() |
8339234df4 | ||
![]() |
3f23747da6 | ||
![]() |
42c38ad815 | ||
![]() |
f63ae846c8 | ||
![]() |
1e730b8ef6 | ||
![]() |
cf7041150a | ||
![]() |
d7ad1dc725 | ||
![]() |
ca52588bae | ||
![]() |
cac5e91471 | ||
![]() |
b57a9de773 | ||
![]() |
7806098ad6 | ||
![]() |
791f75c54c | ||
![]() |
4870830a6c | ||
![]() |
c6aba06199 | ||
![]() |
77d18481c0 | ||
![]() |
7fc147bc8a | ||
![]() |
1e51835b6b | ||
![]() |
525911fd9a | ||
![]() |
95925e2778 | ||
![]() |
22177deb15 | ||
![]() |
a2c372a367 |
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.19-alpine as builder
|
||||
FROM golang:1.20-alpine as builder
|
||||
|
||||
# 设置构建参数
|
||||
ARG GOOS=linux
|
||||
@@ -27,8 +27,6 @@ WORKDIR /build/lkm
|
||||
# 将代码复制到容器中
|
||||
COPY . .
|
||||
|
||||
COPY ./avformat /build/avformat
|
||||
|
||||
RUN go mod download && go mod tidy -v && go build -o lkm .
|
||||
|
||||
# 运行阶段指定scratch作为基础镜像
|
||||
|
28
README.md
28
README.md
@@ -1,33 +1,24 @@
|
||||
## 简介
|
||||
|
||||
基于GoLang实现的流媒体服务器,支持RTMP、GB28181、jt1078推流、jt1078转GB28181,输出rtmp/http-flv/ws-flv/webrtc/hls/rtsp等拉流协议。支持如下编码器和流协议:
|
||||
|
||||
| Codec\Stream | RTMP | FLV | HLS | RTC | RTSP |
|
||||
| ------------ | ---- | --- | --- | --- | ---- |
|
||||
| H264 | √ | √ | √ | √ | √ |
|
||||
| H265 | √ | √ | √ | -([有计划支持](https://linkingvision.com/webrtch265)) | √ |
|
||||
| G711A/U | √ | √ | - | √ | √ |
|
||||
| AAC | √ | √ | √ | - | √ |
|
||||
| OPUS | - | - | - | √ | - |
|
||||
基于GoLang实现的流媒体服务器,支持RTMP、GB28181、jt1078推流、jt1078转GB28181,输出rtmp/http-flv/ws-flv/webrtc/hls/rtsp等拉流协议,支持AAC/G711/G726/OPUS音频转码。
|
||||
|
||||
## 编译
|
||||
|
||||
在使用之前,建议先阅读[LKM启动配置文件参数说明](https://github.com/lkmio/lkm/wiki/Startup-Parameters)。如果你想修改源码,推荐阅读[LKM源码分析](https://github.com/lkmio/lkm/wiki/Source-Code-Analysis)。
|
||||
在使用之前,建议先阅读[LKM启动参数项说明](https://github.com/lkmio/lkm/wiki/Startup-Parameters)。如果你想修改源码,推荐阅读[LKM源码分析](https://github.com/lkmio/lkm/wiki/Source-Code-Analysis)。
|
||||
|
||||
### 源码编译
|
||||
|
||||
git clone https://github.com/lkmio/avformat.git
|
||||
git clone https://github.com/lkmio/lkm.git
|
||||
cd lkm
|
||||
go mod tidy
|
||||
go mod vendor
|
||||
go build
|
||||
|
||||
go build
|
||||
|
||||
### 开启音频转码
|
||||
-tags audio_transcode
|
||||
|
||||
### docker编译
|
||||
|
||||
./build_docker_images.sh GOOS=linux GOARCH=amd64
|
||||
|
||||
|
||||
支持修改`GOOS`和`GOARCH`参数来决定编译平台。默认编译制作`linx amd64`平台的镜像,如果宿主机有golang编译环境,则以宿主机平台为准。优先级如下:编译时指定平台 > 宿主机平台 > 默认平台。
|
||||
|
||||
### docker启动
|
||||
@@ -77,7 +68,10 @@ ffplay -i rtmp://127.0.0.1/34020000001320000001/34020000001310000001.session_id_
|
||||
|
||||
```
|
||||
|
||||
## 1078推流
|
||||
## JT1078推流
|
||||
|
||||
> 需自行安装信令服务, 告知设备推流到LKM的收流端口
|
||||
>
|
||||
> [JT1078转GB28181](https://github.com/lkmio/gb-cms)
|
||||
|
||||
|
||||
|
305
api.go
305
api.go
@@ -1,12 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/lkmio/avformat/utils"
|
||||
"github.com/lkmio/lkm/flv"
|
||||
"github.com/lkmio/lkm/gb28181"
|
||||
"github.com/lkmio/lkm/hls"
|
||||
"github.com/lkmio/lkm/log"
|
||||
"github.com/lkmio/lkm/rtc"
|
||||
@@ -18,7 +20,6 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -81,6 +82,28 @@ func startApiServer(addr string) {
|
||||
http://host:port/xxx_0.ts
|
||||
ws://host:port/xxx.flv
|
||||
*/
|
||||
|
||||
apiServer.router.Use(func(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// 添加 CORS 头以解决跨域问题
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS, PUT, DELETE")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||
|
||||
// 如果是OPTIONS请求,直接返回
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
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(路径)只能嵌套一层
|
||||
apiServer.router.HandleFunc("/{source}.flv", filterSourceID(apiServer.onFlv, ".flv"))
|
||||
apiServer.router.HandleFunc("/{source}/{stream}.flv", filterSourceID(apiServer.onFlv, ".flv"))
|
||||
@@ -102,19 +125,25 @@ func startApiServer(addr string) {
|
||||
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/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) // 统计所有推拉流
|
||||
|
||||
if stream.AppConfig.GB28181.Enable {
|
||||
apiServer.router.HandleFunc("/ws/v1/gb28181/talk", apiServer.OnGBTalk) // 对讲的主讲人WebSocket连接
|
||||
apiServer.router.HandleFunc("/api/v1/gb28181/source/create", withJsonParams(apiServer.OnGBOfferCreate, &SourceSDP{}))
|
||||
apiServer.router.HandleFunc("/api/v1/gb28181/answer/set", withJsonParams(apiServer.OnGBSourceConnect, &SourceSDP{})) // active拉流模式下, 设置对方的地址
|
||||
apiServer.router.HandleFunc("/ws/v1/gb28181/talk", apiServer.OnGBTalk) // 对讲的主讲人WebSocket连接
|
||||
apiServer.router.HandleFunc("/api/v1/control/ws-talk/{device}/{channel}", apiServer.OnLiveGBSTalk) // livegbs一对一对讲
|
||||
apiServer.router.HandleFunc("/api/v1/gb28181/source/create", withJsonParams(apiServer.OnGBOfferCreate, &SourceSDP{})) // 创建国标源
|
||||
apiServer.router.HandleFunc("/api/v1/gb28181/answer/set", withJsonParams(apiServer.OnGBSourceConnect, &SourceSDP{})) // 设置应答sdp, 如果是active模式拉流, 设置对方的地址. 下载文件设置文件大小
|
||||
apiServer.router.HandleFunc("/api/v1/gb28181/speed/set", withJsonParams(apiServer.OnGBSpeedSet, &SourceSDP{}))
|
||||
}
|
||||
|
||||
apiServer.router.HandleFunc("/api/v1/gc/force", func(writer http.ResponseWriter, request *http.Request) {
|
||||
runtime.GC()
|
||||
})
|
||||
|
||||
apiServer.router.HandleFunc("/api/v1/stream/info", apiServer.OnStreamInfo)
|
||||
|
||||
apiServer.router.PathPrefix("/web/").Handler(http.StripPrefix("/web/", http.FileServer(http.Dir("./web"))))
|
||||
|
||||
http.Handle("/", apiServer.router)
|
||||
@@ -134,13 +163,8 @@ func startApiServer(addr string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (api *ApiServer) generateSinkID(remoteAddr string) stream.SinkID {
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", remoteAddr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return stream.NetAddr2SinkID(tcpAddr)
|
||||
func (api *ApiServer) generateSinkID(_ string) stream.SinkID {
|
||||
return utils.RandStringBytes(18)
|
||||
}
|
||||
|
||||
func (api *ApiServer) onFlv(sourceId string, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -315,18 +339,30 @@ func (api *ApiServer) onRtc(sourceId string, w http.ResponseWriter, r *http.Requ
|
||||
}{}
|
||||
|
||||
data, err := io.ReadAll(r.Body)
|
||||
var liveGBSWF bool
|
||||
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("rtc拉流失败 err: %s remote: %s", err.Error(), r.RemoteAddr)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
} else if liveGBSWF = "livegbs" == r.URL.Query().Get("wf"); liveGBSWF {
|
||||
// 兼容livegbs前端播放webrtc
|
||||
offer, err := base64.StdEncoding.DecodeString(string(data))
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("rtc拉流失败 err: %s remote: %s", err.Error(), r.RemoteAddr)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
v.Type = "offer"
|
||||
v.SDP = string(offer)
|
||||
} else if err := json.Unmarshal(data, &v); err != nil {
|
||||
log.Sugar.Errorf("rtc拉流失败 err: %s remote: %s", err.Error(), r.RemoteAddr)
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
group := sync.WaitGroup{}
|
||||
group.Add(1)
|
||||
done := make(chan struct{})
|
||||
sink := rtc.NewSink(api.generateSinkID(r.RemoteAddr), sourceId, v.SDP, func(sdp string) {
|
||||
response := struct {
|
||||
Type string `json:"type"`
|
||||
@@ -336,15 +372,20 @@ func (api *ApiServer) onRtc(sourceId string, w http.ResponseWriter, r *http.Requ
|
||||
SDP: sdp,
|
||||
}
|
||||
|
||||
marshal, err := json.Marshal(response)
|
||||
var body []byte
|
||||
body, err = json.Marshal(response)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(marshal)
|
||||
if liveGBSWF {
|
||||
body = []byte(base64.StdEncoding.EncodeToString([]byte(sdp)))
|
||||
} else {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
group.Done()
|
||||
w.Write(body)
|
||||
close(done)
|
||||
})
|
||||
|
||||
log.Sugar.Infof("rtc拉流请求 source: %s sink: %s sdp:%v", sourceId, sink.String(), v.SDP)
|
||||
@@ -353,10 +394,17 @@ func (api *ApiServer) onRtc(sourceId string, w http.ResponseWriter, r *http.Requ
|
||||
if utils.HookStateOK != ok {
|
||||
log.Sugar.Warnf("rtc拉流失败 source: %s sink: %s", sourceId, sink.String())
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
group.Done()
|
||||
return
|
||||
}
|
||||
|
||||
group.Wait()
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
log.Sugar.Infof("rtc拉流请求取消 source: %s sink: %s", sourceId, stream.SinkID2String(sink.GetID()))
|
||||
sink.Close()
|
||||
break
|
||||
case <-done:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func (api *ApiServer) OnSourceList(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -461,3 +509,222 @@ func (api *ApiServer) OnSinkClose(v *IDS, w http.ResponseWriter, r *http.Request
|
||||
|
||||
httpResponseOK(w, nil)
|
||||
}
|
||||
|
||||
func (api *ApiServer) OnStreamInfo(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("streamid")
|
||||
source := stream.SourceManager.Find(id)
|
||||
if source == nil || source.IsClosed() {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
httpResponseJson(w, "stream not found")
|
||||
return
|
||||
} else if !source.IsCompleted() {
|
||||
// 在请求结束前, 每隔1秒检查track探测是否完成
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
for !source.IsClosed() && !source.IsCompleted() && r.Context().Err() == nil {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
break
|
||||
case <-r.Context().Done():
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracks := source.OriginTracks()
|
||||
if len(tracks) < 1 {
|
||||
return
|
||||
}
|
||||
|
||||
var deviceId string
|
||||
var channelId string
|
||||
split := strings.Split(id, "/")
|
||||
if len(split) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
deviceId = split[0]
|
||||
channelId = split[1]
|
||||
if len(split[1]) >= 20 {
|
||||
channelId = split[1][:20]
|
||||
}
|
||||
|
||||
var transport string
|
||||
if stream.SourceType28181 == source.GetType() {
|
||||
if gb28181.SetupUDP != source.(gb28181.GBSource).SetupType() {
|
||||
transport = "TCP"
|
||||
} else {
|
||||
transport = "UDP"
|
||||
}
|
||||
}
|
||||
|
||||
var token string
|
||||
cookie, err := r.Cookie("token")
|
||||
if err == nil {
|
||||
token = cookie.Value
|
||||
}
|
||||
|
||||
urls := stream.GetStreamPlayUrlsMap(id)
|
||||
liveGBSUrls := make(map[string]string)
|
||||
for streamName, url := range urls {
|
||||
url += "?stream_token=" + token
|
||||
|
||||
// 兼容livegbs前端播放webrtc
|
||||
if streamName == "rtc" {
|
||||
if strings.HasPrefix(url, "http") {
|
||||
url = strings.Replace(url, "http", "webrtc", 1)
|
||||
} else if strings.HasPrefix(url, "https") {
|
||||
url = strings.Replace(url, "https", "webrtcs", 1)
|
||||
}
|
||||
|
||||
url += "&wf=livegbs"
|
||||
}
|
||||
|
||||
liveGBSUrls[streamName] = url
|
||||
}
|
||||
|
||||
var recordStartTime string
|
||||
if startTime := source.GetTransStreamPublisher().RecordStartTime(); !startTime.IsZero() {
|
||||
recordStartTime = startTime.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
gbSource := Source2GBSource(source)
|
||||
var downloadInfo *DownloadInfo
|
||||
if gbSource != nil && InviteTypeDownload == gbSource.GetSessionName() {
|
||||
progress := gbSource.GetPlaybackProgress()
|
||||
gbSource.GetTransStreamPublisher()
|
||||
downloadInfo = &DownloadInfo{
|
||||
PlaybackDuration: gbSource.GetDuration(),
|
||||
PlaybackSpeed: gbSource.GetSpeed(),
|
||||
PlaybackFileSize: gbSource.GetFileSize(),
|
||||
PlaybackStartTime: gbSource.GetStartTime(),
|
||||
PlaybackEndTime: gbSource.GetEndTime(),
|
||||
PlaybackFileURL: gbSource.GetTransStreamPublisher().GetRecordStreamPlayUrl(),
|
||||
PlaybackProgress: progress,
|
||||
Progress: progress,
|
||||
}
|
||||
|
||||
}
|
||||
statistics := source.GetBitrateStatistics()
|
||||
response := struct {
|
||||
*DownloadInfo
|
||||
AudioEnable bool `json:"AudioEnable"`
|
||||
CDN string `json:"CDN"`
|
||||
CascadeSize int `json:"CascadeSize"`
|
||||
ChannelID string `json:"ChannelID"`
|
||||
ChannelName string `json:"ChannelName"`
|
||||
CloudRecord bool `json:"CloudRecord"`
|
||||
DecodeSize int `json:"DecodeSize"`
|
||||
DeviceID string `json:"DeviceID"`
|
||||
Duration int `json:"Duration"`
|
||||
FLV string `json:"FLV"`
|
||||
HLS string `json:"HLS"`
|
||||
InBitRate int `json:"InBitRate"`
|
||||
InBytes int `json:"InBytes"`
|
||||
NumOutputs int `json:"NumOutputs"`
|
||||
Ondemand bool `json:"Ondemand"`
|
||||
OutBytes int `json:"OutBytes"`
|
||||
RTMP string `json:"RTMP"`
|
||||
RTPCount int `json:"RTPCount"`
|
||||
RTPLostCount int `json:"RTPLostCount"`
|
||||
RTPLostRate int `json:"RTPLostRate"`
|
||||
RTSP string `json:"RTSP"`
|
||||
RecordStartAt string `json:"RecordStartAt"` // 录制时间
|
||||
RelaySize int `json:"RelaySize"`
|
||||
SMSID string `json:"SMSID"`
|
||||
SnapURL string `json:"SnapURL"`
|
||||
SourceAudioCodecName string `json:"SourceAudioCodecName"`
|
||||
SourceAudioSampleRate int `json:"SourceAudioSampleRate"`
|
||||
SourceVideoCodecName string `json:"SourceVideoCodecName"`
|
||||
SourceVideoFrameRate int `json:"SourceVideoFrameRate"`
|
||||
SourceVideoHeight int `json:"SourceVideoHeight"`
|
||||
SourceVideoWidth int `json:"SourceVideoWidth"`
|
||||
StartAt string `json:"StartAt"`
|
||||
StreamID string `json:"StreamID"`
|
||||
Transport string `json:"Transport"`
|
||||
VideoFrameCount int `json:"VideoFrameCount"`
|
||||
WEBRTC string `json:"WEBRTC"`
|
||||
WS_FLV string `json:"WS_FLV"`
|
||||
}{
|
||||
DownloadInfo: downloadInfo,
|
||||
AudioEnable: true,
|
||||
CDN: "",
|
||||
CascadeSize: 0,
|
||||
ChannelID: channelId,
|
||||
ChannelName: "",
|
||||
CloudRecord: false,
|
||||
DecodeSize: 0,
|
||||
DeviceID: deviceId,
|
||||
Duration: int(time.Since(source.CreateTime()).Seconds()),
|
||||
FLV: liveGBSUrls["flv"],
|
||||
HLS: liveGBSUrls["hls"],
|
||||
InBitRate: statistics.PreviousSecond() * 8 / 1024,
|
||||
InBytes: int(statistics.Total()),
|
||||
NumOutputs: source.GetTransStreamPublisher().SinkCount(),
|
||||
Ondemand: true,
|
||||
OutBytes: 0,
|
||||
RTMP: liveGBSUrls["rtmp"],
|
||||
RTPCount: 0,
|
||||
RTPLostCount: 0,
|
||||
RTPLostRate: 0,
|
||||
RTSP: liveGBSUrls["rtsp"],
|
||||
RecordStartAt: recordStartTime,
|
||||
RelaySize: 0,
|
||||
SMSID: "",
|
||||
SnapURL: "",
|
||||
SourceVideoFrameRate: 0,
|
||||
StartAt: source.CreateTime().Format("2006-01-02 15:04:05"),
|
||||
StreamID: id,
|
||||
Transport: transport,
|
||||
VideoFrameCount: 0,
|
||||
WEBRTC: liveGBSUrls["rtc"],
|
||||
WS_FLV: liveGBSUrls["ws_flv"],
|
||||
}
|
||||
|
||||
for _, track := range tracks {
|
||||
if utils.AVMediaTypeAudio == track.Stream.MediaType {
|
||||
response.SourceAudioCodecName = track.Stream.CodecID.String()
|
||||
response.SourceAudioSampleRate = track.Stream.AudioConfig.SampleRate
|
||||
} else if utils.AVMediaTypeVideo == track.Stream.MediaType {
|
||||
response.SourceVideoCodecName = track.Stream.CodecID.String()
|
||||
// response.SourceVideoFrameRate
|
||||
if track.Stream.CodecParameters != nil {
|
||||
response.SourceVideoWidth = track.Stream.CodecParameters.Width()
|
||||
response.SourceVideoHeight = track.Stream.CodecParameters.Height()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 ok := source.GetTransStreamPublisher().StartRecord(); !ok {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
} else {
|
||||
// 返回拉流地址
|
||||
httpResponseJson(w, &struct {
|
||||
DownloadURL string `json:"DownloadURL"`
|
||||
}{
|
||||
DownloadURL: source.GetTransStreamPublisher().GetRecordStreamPlayUrl(),
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
215
api_gb.go
215
api_gb.go
@@ -1,15 +1,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"github.com/gorilla/mux"
|
||||
audio_transcoder "github.com/lkmio/audio-transcoder"
|
||||
"github.com/lkmio/avformat/bufio"
|
||||
"github.com/lkmio/avformat/utils"
|
||||
"github.com/lkmio/lkm/gb28181"
|
||||
"github.com/lkmio/lkm/log"
|
||||
"github.com/lkmio/lkm/stream"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,11 +24,26 @@ const (
|
||||
)
|
||||
|
||||
type SDP struct {
|
||||
SessionName string `json:"session_name,omitempty"` // play/download/playback/talk/broadcast
|
||||
Addr string `json:"addr,omitempty"` // 连接地址
|
||||
SSRC string `json:"ssrc,omitempty"`
|
||||
Setup string `json:"setup,omitempty"` // active/passive
|
||||
Transport string `json:"transport,omitempty"` // tcp/udp
|
||||
SessionName string `json:"session_name,omitempty"` // play/download/playback/talk/broadcast
|
||||
Addr string `json:"addr,omitempty"` // 连接地址
|
||||
SSRC string `json:"ssrc,omitempty"`
|
||||
Setup string `json:"setup,omitempty"` // active/passive
|
||||
Transport string `json:"transport,omitempty"` // tcp/udp
|
||||
Speed float64 `json:"speed,omitempty"`
|
||||
StartTime int `json:"start_time,omitempty"`
|
||||
EndTime int `json:"end_time,omitempty"`
|
||||
FileSize int `json:"file_size,omitempty"`
|
||||
}
|
||||
|
||||
type DownloadInfo struct {
|
||||
PlaybackDuration int // 回放/下载时长
|
||||
PlaybackSpeed float64 // 回放/下载速度
|
||||
PlaybackFileURL string // 回放/下载文件URL
|
||||
PlaybackStartTime string // 回放/下载开始时间
|
||||
PlaybackEndTime string // 回放/下载结束时间
|
||||
PlaybackFileSize int // 回放/下载文件大小
|
||||
PlaybackProgress float64 // 1-下载完成
|
||||
Progress float64
|
||||
}
|
||||
|
||||
type SourceSDP struct {
|
||||
@@ -39,6 +57,18 @@ type GBOffer struct {
|
||||
TransStreamProtocol stream.TransStreamProtocol `json:"trans_stream_protocol,omitempty"`
|
||||
}
|
||||
|
||||
func Source2GBSource(source stream.Source) gb28181.GBSource {
|
||||
if gbSource, ok := source.(*gb28181.PassiveSource); ok {
|
||||
return gbSource
|
||||
} else if gbSource, ok := source.(*gb28181.ActiveSource); ok {
|
||||
return gbSource
|
||||
} else if gbSource, ok := source.(*gb28181.PassiveSource); ok {
|
||||
return gbSource
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (api *ApiServer) OnGBSourceCreate(v *SourceSDP, w http.ResponseWriter, r *http.Request) {
|
||||
log.Sugar.Infof("创建国标源: %v", v)
|
||||
|
||||
@@ -73,33 +103,30 @@ func (api *ApiServer) OnGBSourceCreate(v *SourceSDP, w http.ResponseWriter, r *h
|
||||
//udp收流
|
||||
}
|
||||
|
||||
if tcp && active {
|
||||
if !stream.AppConfig.GB28181.IsMultiPort() {
|
||||
err = fmt.Errorf("单端口模式下不能主动拉流")
|
||||
} else if !tcp {
|
||||
err = fmt.Errorf("UDP不能主动拉流")
|
||||
} else if !stream.AppConfig.GB28181.IsEnableTCP() {
|
||||
err = fmt.Errorf("未开启TCP收流服务,UDP不能主动拉流")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var ssrc string
|
||||
if v.SessionName == InviteTypeDownload || v.SessionName == InviteTypePlayback {
|
||||
if InviteTypeDownload == v.SessionName || InviteTypePlayback == v.SessionName {
|
||||
ssrc = gb28181.GetVodSSRC()
|
||||
} else {
|
||||
ssrc = gb28181.GetLiveSSRC()
|
||||
}
|
||||
|
||||
ssrcValue, _ := strconv.Atoi(ssrc)
|
||||
_, port, err := gb28181.NewGBSource(v.Source, uint32(ssrcValue), tcp, active)
|
||||
gbSource, port, err := gb28181.NewGBSource(v.Source, uint32(ssrcValue), tcp, active)
|
||||
if err != nil {
|
||||
return
|
||||
} else if InviteTypeDownload == v.SessionName {
|
||||
// 开启录制
|
||||
gbSource.GetTransStreamPublisher().StartRecord()
|
||||
}
|
||||
|
||||
startTime := time.Unix(int64(v.StartTime), 0).Format("2006-01-02T15:04:05")
|
||||
endTime := time.Unix(int64(v.EndTime), 0).Format("2006-01-02T15:04:05")
|
||||
gbSource.SetSessionName(v.SessionName)
|
||||
gbSource.SetStartTime(startTime)
|
||||
gbSource.SetEndTime(endTime)
|
||||
gbSource.SetSpeed(v.Speed)
|
||||
gbSource.SetDuration(v.EndTime - v.StartTime)
|
||||
|
||||
response.Addr = net.JoinHostPort(stream.AppConfig.PublicIP, strconv.Itoa(port))
|
||||
response.Urls = stream.GetStreamPlayUrls(v.Source)
|
||||
response.SSRC = ssrc
|
||||
@@ -109,13 +136,13 @@ func (api *ApiServer) OnGBSourceCreate(v *SourceSDP, w http.ResponseWriter, r *h
|
||||
}
|
||||
|
||||
func (api *ApiServer) OnGBSourceConnect(v *SourceSDP, w http.ResponseWriter, r *http.Request) {
|
||||
log.Sugar.Infof("设置国标主动拉流连接地址: %v", v)
|
||||
log.Sugar.Infof("设置国标应答: %v", v)
|
||||
|
||||
var err error
|
||||
// 响应错误消息
|
||||
defer func() {
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("设置国标主动拉流失败 err: %s", err.Error())
|
||||
log.Sugar.Errorf("设置国标应答失败 err: %s", err.Error())
|
||||
httpResponseError(w, err.Error())
|
||||
}
|
||||
}()
|
||||
@@ -123,22 +150,24 @@ func (api *ApiServer) OnGBSourceConnect(v *SourceSDP, w http.ResponseWriter, r *
|
||||
source := stream.SourceManager.Find(v.Source)
|
||||
if source == nil {
|
||||
err = fmt.Errorf("%s 源不存在", v.Source)
|
||||
return
|
||||
}
|
||||
} else if stream.SourceType28181 != source.GetType() {
|
||||
err = fmt.Errorf("%s 源不是28181类型", v.Source)
|
||||
} else if activeSource, ok := source.(*gb28181.ActiveSource); ok {
|
||||
activeSource.SetFileSize(v.FileSize)
|
||||
// 主动连接取流
|
||||
var addr *net.TCPAddr
|
||||
addr, err = net.ResolveTCPAddr("tcp", v.Addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
activeSource, ok := source.(*gb28181.ActiveSource)
|
||||
if !ok {
|
||||
err = fmt.Errorf("%s 源不是Active拉流类型", v.Source)
|
||||
return
|
||||
}
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", v.Addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err = activeSource.Connect(addr); err == nil {
|
||||
httpResponseOK(w, nil)
|
||||
if err = activeSource.Connect(addr); err == nil {
|
||||
httpResponseOK(w, nil)
|
||||
}
|
||||
} else if passiveSource, ok := source.(*gb28181.PassiveSource); ok {
|
||||
passiveSource.SetFileSize(v.FileSize)
|
||||
} else if udpSource, ok := source.(*gb28181.UDPSource); ok {
|
||||
udpSource.SetFileSize(v.FileSize)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +229,21 @@ func (api *ApiServer) AddForwardSink(protocol stream.TransStreamProtocol, transp
|
||||
httpResponseOK(w, &response)
|
||||
}
|
||||
|
||||
func (api *ApiServer) OnSinkAdd(v *GBOffer, w http.ResponseWriter, r *http.Request) {
|
||||
log.Sugar.Infof("添加sink: %v", *v)
|
||||
if stream.TransStreamGBCascaded != v.TransStreamProtocol && stream.TransStreamGBTalk != v.TransStreamProtocol && stream.TransStreamGBGateway != v.TransStreamProtocol {
|
||||
httpResponseError(w, "不支持的协议")
|
||||
return
|
||||
}
|
||||
|
||||
setup := gb28181.SetupTypeFromString(v.Setup)
|
||||
if v.AnswerSetup != "" {
|
||||
setup = gb28181.SetupTypeFromString(v.AnswerSetup)
|
||||
}
|
||||
|
||||
api.AddForwardSink(v.TransStreamProtocol, setup.TransportType(), v.Source, v.Addr, v.SSRC, v.SessionName, w, r)
|
||||
}
|
||||
|
||||
// OnGBTalk 国标广播/对讲流程:
|
||||
// 1. 浏览器使用WS携带source_id访问/api/v1/gb28181/talk, 如果source_id冲突, 直接断开ws连接
|
||||
// 2. WS链接建立后, 调用gb-cms接口/api/v1/broadcast/invite, 向设备发送广播请求
|
||||
@@ -218,9 +262,9 @@ func (api *ApiServer) OnGBTalk(w http.ResponseWriter, r *http.Request) {
|
||||
talkSource.Init()
|
||||
talkSource.SetUrlValues(r.Form)
|
||||
|
||||
_, state := stream.PreparePublishSource(talkSource, true)
|
||||
if utils.HookStateOK != state {
|
||||
log.Sugar.Errorf("对讲失败, source: %s", talkSource)
|
||||
_, err = stream.PreparePublishSource(talkSource, true)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("对讲失败, err: %s source: %s", err, talkSource)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
@@ -252,17 +296,88 @@ func (api *ApiServer) OnGBTalk(w http.ResponseWriter, r *http.Request) {
|
||||
talkSource.Close()
|
||||
}
|
||||
|
||||
func (api *ApiServer) OnSinkAdd(v *GBOffer, w http.ResponseWriter, r *http.Request) {
|
||||
log.Sugar.Infof("添加sink: %v", *v)
|
||||
if stream.TransStreamGBCascaded != v.TransStreamProtocol && stream.TransStreamGBTalk != v.TransStreamProtocol && stream.TransStreamGBGateway != v.TransStreamProtocol {
|
||||
httpResponseError(w, "不支持的协议")
|
||||
// OnLiveGBSTalk liveGBS前端对讲
|
||||
func (api *ApiServer) OnLiveGBSTalk(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
device := vars["device"]
|
||||
channel := vars["channel"]
|
||||
_ = r.URL.Query().Get("format")
|
||||
|
||||
conn, err := api.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("升级为websocket失败 err: %s", err.Error())
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
setup := gb28181.SetupTypeFromString(v.Setup)
|
||||
if v.AnswerSetup != "" {
|
||||
setup = gb28181.SetupTypeFromString(v.AnswerSetup)
|
||||
id := device + "/" + channel + ".broadcast"
|
||||
talkSource := gb28181.NewTalkSource(id, conn)
|
||||
talkSource.Init()
|
||||
talkSource.SetUrlValues(r.Form)
|
||||
|
||||
_, err = stream.PreparePublishSource(talkSource, true)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("对讲失败, err: %s source: %s", err, talkSource)
|
||||
conn.Close()
|
||||
return
|
||||
}
|
||||
|
||||
api.AddForwardSink(v.TransStreamProtocol, setup.TransportType(), v.Source, v.Addr, v.SSRC, v.SessionName, w, r)
|
||||
log.Sugar.Infof("ws对讲连接成功, source: %s", talkSource)
|
||||
|
||||
stream.LoopEvent(talkSource)
|
||||
|
||||
data := stream.UDPReceiveBufferPool.Get().([]byte)
|
||||
pcm := make([]byte, 32000)
|
||||
g711aPacket := make([]byte, stream.UDPReceiveBufferSize/2)
|
||||
|
||||
for {
|
||||
_, bytes, err := conn.ReadMessage()
|
||||
length := len(bytes)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("读取对讲音频包失败, source: %s err: %s", id, err.Error())
|
||||
break
|
||||
} else if length < 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 扩容
|
||||
if int(float64(len(bytes))*1.4) > len(pcm) {
|
||||
pcm = make([]byte, len(bytes)*2)
|
||||
}
|
||||
|
||||
// base64解密
|
||||
var pcmN int
|
||||
pcmN, err = base64.StdEncoding.Decode(pcm, bytes)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("base64解密失败, source: %s err: %s", id, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
for i := 0; i < pcmN; {
|
||||
// 控制每包大小
|
||||
n := bufio.MinInt(stream.UDPReceiveBufferSize, length-i)
|
||||
copy(data, pcm[:n])
|
||||
|
||||
// 编码成G711A
|
||||
audio_transcoder.EncodeAlawToBuffer(data, g711aPacket)
|
||||
|
||||
_, _ = talkSource.PublishSource.Input(g711aPacket[:n/2])
|
||||
i += n
|
||||
}
|
||||
}
|
||||
|
||||
talkSource.Close()
|
||||
}
|
||||
|
||||
func (api *ApiServer) OnGBSpeedSet(v *SourceSDP, w http.ResponseWriter, r *http.Request) {
|
||||
source := stream.SourceManager.Find(v.Source)
|
||||
if source == nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
httpResponseError(w, "stream not found")
|
||||
} else if stream.SourceType28181 != source.GetType() {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
httpResponseError(w, "stream type not support")
|
||||
} else if gbSource := Source2GBSource(source); gbSource != nil {
|
||||
gbSource.SetSpeed(v.Speed)
|
||||
}
|
||||
}
|
||||
|
40
bridge.go
40
bridge.go
@@ -10,22 +10,15 @@ import (
|
||||
|
||||
// 处理不同包不能相互引用的需求
|
||||
|
||||
func NewStreamEndInfo(source string, tracks []*stream.Track, streams map[stream.TransStreamID]stream.TransStream) *stream.StreamEndInfo {
|
||||
if len(tracks) < 1 || len(streams) < 1 {
|
||||
func NewStreamEndInfo(source string, originTracks []*stream.Track, streams map[stream.TransStreamID]stream.TransStream) *stream.StreamEndInfo {
|
||||
if len(streams) < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
info := stream.StreamEndInfo{
|
||||
ID: source,
|
||||
Timestamps: make(map[utils.AVCodecID][2]int64, len(tracks)),
|
||||
}
|
||||
|
||||
for _, track := range tracks {
|
||||
var timestamp [2]int64
|
||||
timestamp[0] = track.Dts + int64(track.FrameDuration)
|
||||
timestamp[1] = track.Pts + int64(track.FrameDuration)
|
||||
|
||||
info.Timestamps[track.Stream.CodecID] = timestamp
|
||||
ID: source,
|
||||
Timestamps: make(map[stream.TransStreamID]map[utils.AVCodecID][2]int64, len(streams)),
|
||||
OriginTracks: make(map[utils.AVCodecID]interface{}, len(originTracks)),
|
||||
}
|
||||
|
||||
for _, transStream := range streams {
|
||||
@@ -33,19 +26,32 @@ func NewStreamEndInfo(source string, tracks []*stream.Track, streams map[stream.
|
||||
if stream.TransStreamHls == transStream.GetProtocol() {
|
||||
if hls := transStream.(*hls.TransStream); hls.M3U8Writer.Size() > 0 {
|
||||
info.M3U8Writer = hls.M3U8Writer
|
||||
info.PlaylistFormat = hls.PlaylistFormatPtr
|
||||
info.PlaylistFormat = hls.PlaylistFormat
|
||||
}
|
||||
} else if stream.TransStreamRtsp == transStream.GetProtocol() {
|
||||
if rtsp := transStream.(*rtsp.TransStream); len(rtsp.Tracks) > 0 {
|
||||
info.RtspTracks = make(map[int]uint16, len(tracks))
|
||||
info.RtspTracks = make(map[utils.AVCodecID]uint16, 8)
|
||||
for _, track := range rtsp.RtspTracks {
|
||||
info.RtspTracks[int(track.CodecID)] = track.EndSeq
|
||||
info.RtspTracks[track.CodecID] = track.EndSeq
|
||||
}
|
||||
}
|
||||
} else if stream.TransStreamFlv == transStream.GetProtocol() {
|
||||
stream := transStream.(*flv.TransStream)
|
||||
info.FLVPrevTagSize = stream.Muxer.PrevTagSize()
|
||||
flv := transStream.(*flv.TransStream)
|
||||
info.FLVPrevTagSize = flv.Muxer.PrevTagSize()
|
||||
}
|
||||
|
||||
// 保存传输流最后的时间戳
|
||||
tracks := transStream.GetTracks()
|
||||
ts := make(map[utils.AVCodecID][2]int64, len(tracks))
|
||||
for _, track := range tracks {
|
||||
ts[track.Stream.CodecID] = [2]int64{track.Dts, track.Pts}
|
||||
}
|
||||
|
||||
info.Timestamps[transStream.GetID()] = ts
|
||||
}
|
||||
|
||||
for _, track := range originTracks {
|
||||
info.OriginTracks[track.Stream.CodecID] = nil
|
||||
}
|
||||
|
||||
return &info
|
||||
|
10
config.json
10
config.json
@@ -7,6 +7,7 @@
|
||||
"idle_timeout": 60,
|
||||
"receive_timeout":60,
|
||||
"debug": false,
|
||||
"media_port": [50000,60000],
|
||||
|
||||
"http": {
|
||||
"port": 8080
|
||||
@@ -26,9 +27,8 @@
|
||||
|
||||
"rtsp": {
|
||||
"enable": true,
|
||||
"port": [554,20000,30000],
|
||||
"password": "123456",
|
||||
"transport": "UDP|TCP"
|
||||
"port": 554,
|
||||
"password": "123456"
|
||||
},
|
||||
|
||||
"webrtc": {
|
||||
@@ -38,9 +38,7 @@
|
||||
},
|
||||
|
||||
"gb28181": {
|
||||
"enable": true,
|
||||
"port": [50000,60000],
|
||||
"transport": "UDP|TCP"
|
||||
"enable": true
|
||||
},
|
||||
|
||||
"jt1078": {
|
||||
|
@@ -18,7 +18,7 @@ type TransStream struct {
|
||||
flvExtraDataBlock []byte // metadata和sequence header
|
||||
}
|
||||
|
||||
func (t *TransStream) Input(packet *avformat.AVPacket, _ int) ([]*collections.ReferenceCounter[[]byte], int64, bool, error) {
|
||||
func (t *TransStream) Input(packet *avformat.AVPacket, index int) ([]*collections.ReferenceCounter[[]byte], int64, bool, error) {
|
||||
t.ClearOutStreamBuffer()
|
||||
|
||||
var flvTagSize int
|
||||
@@ -29,12 +29,19 @@ func (t *TransStream) Input(packet *avformat.AVPacket, _ int) ([]*collections.Re
|
||||
var keyBuffer bool
|
||||
var frameType int
|
||||
|
||||
dts = packet.ConvertDts(1000)
|
||||
pts = packet.ConvertPts(1000)
|
||||
duration := packet.GetDuration(1000)
|
||||
track := t.Tracks[index]
|
||||
dts = track.Dts
|
||||
pts = track.Pts
|
||||
track.Dts += duration
|
||||
track.Pts = track.Dts + packet.GetPtsDtsDelta(1000)
|
||||
|
||||
if utils.AVMediaTypeAudio == packet.MediaType {
|
||||
//log.Sugar.Infof("audio packet dts: %d, pts: %d data size: %d", dts, pts, len(packet.Data))
|
||||
data = packet.Data
|
||||
flvTagSize = flv.TagHeaderSize + t.Muxer.ComputeAudioDataHeaderSize() + len(packet.Data)
|
||||
} else if utils.AVMediaTypeVideo == packet.MediaType {
|
||||
//log.Sugar.Infof("video packet dts: %d, pts: %d", dts, pts)
|
||||
data = avformat.AnnexBPacket2AVCC(packet)
|
||||
flvTagSize = flv.TagHeaderSize + t.Muxer.ComputeVideoDataHeaderSize(uint32(pts-dts)) + len(data)
|
||||
if videoKey = packet.Key; videoKey {
|
||||
@@ -129,7 +136,7 @@ func (t *TransStream) ReadExtraData(_ int64) ([]*collections.ReferenceCounter[[]
|
||||
return []*collections.ReferenceCounter[[]byte]{collections.NewReferenceCounter(GetHttpFLVBlock(t.flvHeaderBlock)), collections.NewReferenceCounter(GetHttpFLVBlock(t.flvExtraDataBlock))}, 0, nil
|
||||
}
|
||||
|
||||
func (t *TransStream) ReadKeyFrameBuffer() ([]*collections.ReferenceCounter[[]byte], int64, error) {
|
||||
func (t *TransStream) ReadKeyFrameBuffer() ([]stream.TransStreamSegment, error) {
|
||||
t.ClearOutStreamBuffer()
|
||||
|
||||
// 发送当前内存池已有的合并写切片
|
||||
@@ -137,20 +144,42 @@ func (t *TransStream) ReadKeyFrameBuffer() ([]*collections.ReferenceCounter[[]by
|
||||
t.AppendOutStreamBuffer(segment)
|
||||
})
|
||||
|
||||
return t.OutBuffer[:t.OutBufferSize], 0, nil
|
||||
if t.OutBufferSize < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return []stream.TransStreamSegment{
|
||||
{
|
||||
Data: t.OutBuffer[:t.OutBufferSize],
|
||||
TS: 0,
|
||||
Key: true,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *TransStream) Close() ([]*collections.ReferenceCounter[[]byte], int64, error) {
|
||||
func (t *TransStream) Close() ([]stream.TransStreamSegment, error) {
|
||||
t.ClearOutStreamBuffer()
|
||||
|
||||
// 发送剩余的流
|
||||
var key bool
|
||||
var segment *collections.ReferenceCounter[[]byte]
|
||||
if !t.MWBuffer.IsNewSegment() {
|
||||
if segment, _ := t.flushSegment(); segment != nil {
|
||||
if segment, key = t.flushSegment(); segment != nil {
|
||||
t.AppendOutStreamBuffer(segment)
|
||||
}
|
||||
}
|
||||
|
||||
return t.OutBuffer[:t.OutBufferSize], 0, nil
|
||||
if t.OutBufferSize < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return []stream.TransStreamSegment{
|
||||
{
|
||||
Data: t.OutBuffer[:t.OutBufferSize],
|
||||
TS: 0,
|
||||
Key: key,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 保存为完整的http-flv切片
|
||||
|
@@ -1,10 +0,0 @@
|
||||
package gb28181
|
||||
|
||||
// Filter 关联Source
|
||||
type Filter interface {
|
||||
AddSource(ssrc uint32, source GBSource) bool
|
||||
|
||||
RemoveSource(ssrc uint32)
|
||||
|
||||
FindSource(ssrc uint32) GBSource
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
package gb28181
|
||||
|
||||
type singleFilter struct {
|
||||
source GBSource
|
||||
}
|
||||
|
||||
func (s *singleFilter) AddSource(ssrc uint32, source GBSource) bool {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
func (s *singleFilter) RemoveSource(ssrc uint32) {
|
||||
s.source = nil
|
||||
}
|
||||
|
||||
func (s *singleFilter) FindSource(ssrc uint32) GBSource {
|
||||
return s.source
|
||||
}
|
||||
|
||||
func NewSingleFilter(source GBSource) Filter {
|
||||
return &singleFilter{source: source}
|
||||
}
|
@@ -1,38 +0,0 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
type ssrcFilter struct {
|
||||
sources map[uint32]GBSource
|
||||
mute sync.RWMutex
|
||||
}
|
||||
|
||||
func (r *ssrcFilter) AddSource(ssrc uint32, source GBSource) bool {
|
||||
r.mute.Lock()
|
||||
defer r.mute.Unlock()
|
||||
|
||||
if _, ok := r.sources[ssrc]; !ok {
|
||||
r.sources[ssrc] = source
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *ssrcFilter) RemoveSource(ssrc uint32) {
|
||||
r.mute.Lock()
|
||||
defer r.mute.Unlock()
|
||||
delete(r.sources, ssrc)
|
||||
}
|
||||
|
||||
func (r *ssrcFilter) FindSource(ssrc uint32) GBSource {
|
||||
r.mute.RLock()
|
||||
defer r.mute.RUnlock()
|
||||
return r.sources[ssrc]
|
||||
}
|
||||
|
||||
func NewSSRCFilter(guestCount int) Filter {
|
||||
return &ssrcFilter{sources: make(map[uint32]GBSource, guestCount)}
|
||||
}
|
@@ -65,9 +65,9 @@ func (s *GBGateway) Input(packet *avformat.AVPacket, index int) ([]*collections.
|
||||
return result, 0, true, nil
|
||||
}
|
||||
|
||||
func (s *GBGateway) Close() ([]*collections.ReferenceCounter[[]byte], int64, error) {
|
||||
func (s *GBGateway) Close() ([]stream.TransStreamSegment, error) {
|
||||
s.rtpBuffer.Clear()
|
||||
return nil, 0, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func NewGBGateway(ssrc uint32) *GBGateway {
|
||||
|
@@ -24,7 +24,7 @@ import (
|
||||
func connectSource(source string, addr string) {
|
||||
v := &struct {
|
||||
Source string `json:"source"` //GetSourceID
|
||||
RemoteAddr string `json:"remote_addr"`
|
||||
RemoteAddr string `json:"addr"`
|
||||
}{
|
||||
Source: source,
|
||||
RemoteAddr: addr,
|
||||
@@ -35,7 +35,7 @@ func connectSource(source string, addr string) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("POST", "http://localhost:8080/v1/gb28181/source/connect", bytes.NewBuffer(marshal))
|
||||
request, err := http.NewRequest("POST", "http://localhost:8080/api/v1/gb28181/answer/set", bytes.NewBuffer(marshal))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -209,11 +209,12 @@ func TestPublish(t *testing.T) {
|
||||
//path := "../../source_files/rtp_ps_h264_G7221_0xBEBC204.raw"
|
||||
//var rawSsrc uint32 = 0xBEBC204
|
||||
|
||||
path := "../../source_files/rtp_ps_h264_G726_0xBEBC205.raw"
|
||||
//path := "../../source_files/rtp_ps_h264_G726_0xBEBC205.raw"
|
||||
path := "../../source_files/rtp_ps_err_parse.raw"
|
||||
var rawSsrc uint32 = 0xBEBC205
|
||||
|
||||
localAddr := "0.0.0.0:20001"
|
||||
id := "hls_mystream"
|
||||
id := "hls/mystream"
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
@@ -305,7 +306,7 @@ func TestPublish(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("active", func(t *testing.T) {
|
||||
ip, port, ssrc := createSource(id, "active", rawSsrc)
|
||||
_, _, ssrc := createSource(id, "active", rawSsrc)
|
||||
|
||||
addr, _ := net.ResolveTCPAddr("tcp", localAddr)
|
||||
server := transport.TCPServer{}
|
||||
@@ -317,6 +318,7 @@ func TestPublish(t *testing.T) {
|
||||
ctrDelay(packet[2:])
|
||||
}
|
||||
|
||||
server.Close()
|
||||
return nil
|
||||
}, nil, nil)
|
||||
|
||||
@@ -325,7 +327,9 @@ func TestPublish(t *testing.T) {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
connectSource(id, fmt.Sprintf("%s:%d", ip, port))
|
||||
server.Accept()
|
||||
connectSource(id, localAddr)
|
||||
select {}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -336,10 +340,10 @@ func TestDecode(t *testing.T) {
|
||||
panic(err2)
|
||||
}
|
||||
|
||||
source := NewPassiveSource()
|
||||
source.Init()
|
||||
filter := NewSingleFilter(source)
|
||||
session := NewTCPSession(nil, filter)
|
||||
source := &PassiveSource{
|
||||
decoder: transport.NewLengthFieldFrameDecoder(0xFFFF, 2),
|
||||
}
|
||||
|
||||
reader := bufio.NewBytesReader(file)
|
||||
|
||||
for {
|
||||
@@ -353,7 +357,7 @@ func TestDecode(t *testing.T) {
|
||||
break
|
||||
}
|
||||
|
||||
err2 = session.DecodeGBRTPOverTCPPacket(bytes, filter, nil)
|
||||
err2 = source.DecodeGBRTPOverTCPPacket(bytes)
|
||||
if err2 != nil {
|
||||
break
|
||||
}
|
||||
|
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/lkmio/transport"
|
||||
"github.com/pion/rtp"
|
||||
"math"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -23,7 +22,6 @@ const (
|
||||
SetupActive = SetupType(2)
|
||||
|
||||
PsProbeBufferSize = 1024 * 1024 * 2
|
||||
JitterBufferSize = 1024 * 1024
|
||||
)
|
||||
|
||||
func (s SetupType) TransportType() stream.TransportType {
|
||||
@@ -65,8 +63,6 @@ func SetupTypeFromString(setupType string) SetupType {
|
||||
|
||||
var (
|
||||
TransportManger transport.Manager
|
||||
SharedUDPServer *UDPServer
|
||||
SharedTCPServer *TCPServer
|
||||
)
|
||||
|
||||
// GBSource GB28181推流Source, 统一解析PS流、级联转发.
|
||||
@@ -75,23 +71,35 @@ type GBSource interface {
|
||||
|
||||
SetupType() SetupType
|
||||
|
||||
// PreparePublish 收到流时, 做一些初始化工作.
|
||||
PreparePublish(conn net.Conn, ssrc uint32, source GBSource)
|
||||
|
||||
SetConn(conn net.Conn)
|
||||
|
||||
SetSSRC(ssrc uint32)
|
||||
|
||||
SSRC() uint32
|
||||
|
||||
ProcessPacket(data []byte) error
|
||||
|
||||
SetTransport(transport transport.Transport)
|
||||
|
||||
GetDuration() int
|
||||
GetSpeed() float64
|
||||
GetSessionName() string
|
||||
GetStartTime() string
|
||||
GetEndTime() string
|
||||
GetFileSize() int
|
||||
GetPlaybackProgress() float64
|
||||
|
||||
SetDuration(duration int)
|
||||
SetSpeed(speed float64)
|
||||
SetSessionName(sessionName string)
|
||||
SetStartTime(startTime string)
|
||||
SetEndTime(endTime string)
|
||||
SetFileSize(fileSize int)
|
||||
}
|
||||
|
||||
type BaseGBSource struct {
|
||||
stream.PublishSource
|
||||
|
||||
transport transport.Transport
|
||||
probeBuffer *mpeg.PSProbeBuffer
|
||||
transport transport.Transport
|
||||
ssrc uint32
|
||||
|
||||
audioTimestamp int64
|
||||
@@ -101,18 +109,15 @@ type BaseGBSource struct {
|
||||
isSystemClock bool // 推流时间戳不正确, 是否使用系统时间.
|
||||
lastRtpTimestamp int64
|
||||
sameTimePackets [][]byte
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) Init() {
|
||||
source.TransDemuxer = mpeg.NewPSDemuxer(false)
|
||||
source.TransDemuxer.SetHandler(source)
|
||||
source.TransDemuxer.SetOnPreprocessPacketHandler(func(packet *avformat.AVPacket) {
|
||||
source.correctTimestamp(packet, packet.Dts, packet.Pts)
|
||||
})
|
||||
source.SetType(stream.SourceType28181)
|
||||
source.probeBuffer = mpeg.NewProbeBuffer(PsProbeBufferSize)
|
||||
source.PublishSource.Init()
|
||||
source.lastRtpTimestamp = -1
|
||||
sessionName string // play/playback/download...
|
||||
duration int // 回放/下载时长, 单位秒
|
||||
speed float64 // 回放/下载速度
|
||||
startTime string // 回放/下载开始时间
|
||||
endTime string // 回放/下载结束时间
|
||||
fileSize int // 回放/下载文件大小
|
||||
playbackProgress float64 // 1-下载完成
|
||||
playbackDataSize int // 已下载数据大小
|
||||
}
|
||||
|
||||
// ProcessPacket 输入rtp包, 处理PS流, 负责解析->封装->推流
|
||||
@@ -120,6 +125,18 @@ func (source *BaseGBSource) ProcessPacket(data []byte) error {
|
||||
packet := rtp.Packet{}
|
||||
_ = packet.Unmarshal(data)
|
||||
|
||||
// 收到第一包, 初始化
|
||||
if source.probeBuffer == nil {
|
||||
source.InitializePublish(packet.SSRC)
|
||||
}
|
||||
|
||||
// 统计下载的进度
|
||||
source.playbackDataSize += len(data)
|
||||
source.playbackProgress = float64(source.playbackDataSize) / float64(source.fileSize)
|
||||
if source.playbackProgress > 1 {
|
||||
source.playbackProgress = 1
|
||||
}
|
||||
|
||||
// 国标级联转发
|
||||
if source.GetTransStreamPublisher().GetForwardTransStream() != nil {
|
||||
if source.lastRtpTimestamp == -1 {
|
||||
@@ -228,30 +245,17 @@ func (source *BaseGBSource) correctTimestamp(packet *avformat.AVPacket, dts, pts
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) Close() {
|
||||
log.Sugar.Infof("GB28181推流结束 ssrc:%d %s", source.ssrc, source.PublishSource.String())
|
||||
|
||||
// 释放收流端口
|
||||
if source.transport != nil {
|
||||
source.transport.Close()
|
||||
source.transport = nil
|
||||
}
|
||||
|
||||
// 删除ssrc关联
|
||||
if !stream.AppConfig.GB28181.IsMultiPort() {
|
||||
if SharedTCPServer != nil {
|
||||
SharedTCPServer.filter.RemoveSource(source.ssrc)
|
||||
}
|
||||
|
||||
if SharedUDPServer != nil {
|
||||
SharedUDPServer.filter.RemoveSource(source.ssrc)
|
||||
}
|
||||
}
|
||||
log.Sugar.Infof("GB28181推流结束 ssrc: %d %s", source.ssrc, source.PublishSource.String())
|
||||
|
||||
source.PublishSource.Close()
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) SetConn(conn net.Conn) {
|
||||
source.Conn = conn
|
||||
// 加锁执行, 保证并发安全
|
||||
source.ExecuteWithDeleteLock(func() {
|
||||
if source.transport != nil {
|
||||
source.transport.Close()
|
||||
source.transport = nil
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) SetSSRC(ssrc uint32) {
|
||||
@@ -262,41 +266,100 @@ func (source *BaseGBSource) SSRC() uint32 {
|
||||
return source.ssrc
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) PreparePublish(conn net.Conn, ssrc uint32, source_ GBSource) {
|
||||
source.SetConn(conn)
|
||||
source.SetSSRC(ssrc)
|
||||
source.SetState(stream.SessionStateTransferring)
|
||||
func (source *BaseGBSource) InitializePublish(ssrc uint32) {
|
||||
if source.ssrc != ssrc {
|
||||
log.Sugar.Warnf("创建source的ssrc与实际推流的ssrc不一致, 创建的ssrc: %x 实际推流的ssrc: %x source: %s", source.ssrc, ssrc, source.GetID())
|
||||
}
|
||||
|
||||
// 初始化ps解复用器
|
||||
source.TransDemuxer.SetOnPreprocessPacketHandler(func(packet *avformat.AVPacket) {
|
||||
source.correctTimestamp(packet, packet.Dts, packet.Pts)
|
||||
})
|
||||
source.probeBuffer = mpeg.NewProbeBuffer(PsProbeBufferSize)
|
||||
source.lastRtpTimestamp = -1
|
||||
|
||||
source.ssrc = ssrc
|
||||
source.audioTimestamp = -1
|
||||
source.videoTimestamp = -1
|
||||
source.audioPacketCreatedTime = -1
|
||||
source.videoPacketCreatedTime = -1
|
||||
|
||||
if stream.AppConfig.Hooks.IsEnablePublishEvent() {
|
||||
go func() {
|
||||
if _, state := stream.HookPublishEvent(source_); utils.HookStateOK == state {
|
||||
return
|
||||
}
|
||||
|
||||
log.Sugar.Errorf("GB28181 推流失败 source:%s", source.GetID())
|
||||
if conn != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
p := stream.SourceManager.Find(source.GetID())
|
||||
if p == nil {
|
||||
log.Sugar.Errorf("GB28181推流失败, 未找到source: %s", source.GetID())
|
||||
source.Close()
|
||||
return
|
||||
}
|
||||
|
||||
stream.PreparePublishSourceWithAsync(p, false)
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) Init() {
|
||||
// 创建ps解复用器
|
||||
source.TransDemuxer = mpeg.NewPSDemuxer(false)
|
||||
source.TransDemuxer.SetHandler(source)
|
||||
source.PublishSource.Init()
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) SetTransport(transport transport.Transport) {
|
||||
source.transport = transport
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) GetSessionName() string {
|
||||
return source.sessionName
|
||||
}
|
||||
func (source *BaseGBSource) GetStartTime() string {
|
||||
return source.startTime
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) GetEndTime() string {
|
||||
return source.endTime
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) GetFileSize() int {
|
||||
return source.fileSize
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) GetPlaybackProgress() float64 {
|
||||
return source.playbackProgress
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) SetStartTime(startTime string) {
|
||||
source.startTime = startTime
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) SetEndTime(endTime string) {
|
||||
source.endTime = endTime
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) SetFileSize(fileSize int) {
|
||||
source.fileSize = fileSize
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) SetSessionName(sessionName string) {
|
||||
// 转小写
|
||||
source.sessionName = strings.ToLower(sessionName)
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) GetDuration() int {
|
||||
return source.duration
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) GetSpeed() float64 {
|
||||
return source.speed
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) SetDuration(duration int) {
|
||||
source.duration = duration
|
||||
}
|
||||
|
||||
func (source *BaseGBSource) SetSpeed(speed float64) {
|
||||
source.speed = speed
|
||||
}
|
||||
|
||||
// NewGBSource 创建国标推流源, 返回监听的收流端口
|
||||
func NewGBSource(id string, ssrc uint32, tcp bool, active bool) (GBSource, int, error) {
|
||||
if tcp {
|
||||
utils.Assert(stream.AppConfig.GB28181.IsEnableTCP())
|
||||
} else {
|
||||
utils.Assert(stream.AppConfig.GB28181.IsEnableUDP())
|
||||
}
|
||||
|
||||
if active {
|
||||
utils.Assert(tcp && stream.AppConfig.GB28181.IsEnableTCP() && stream.AppConfig.GB28181.IsMultiPort())
|
||||
}
|
||||
|
||||
var transportServer transport.Transport
|
||||
var source GBSource
|
||||
var port int
|
||||
var err error
|
||||
@@ -304,55 +367,46 @@ func NewGBSource(id string, ssrc uint32, tcp bool, active bool) (GBSource, int,
|
||||
if active {
|
||||
source, port, err = NewActiveSource()
|
||||
} else if tcp {
|
||||
transportServer, err = TransportManger.NewTCPServer()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
source = NewPassiveSource()
|
||||
transportServer.(*transport.TCPServer).SetHandler(source.(*PassiveSource))
|
||||
transportServer.(*transport.TCPServer).Accept()
|
||||
port = transportServer.ListenPort()
|
||||
} else {
|
||||
transportServer, err = TransportManger.NewUDPServer()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
source = NewUDPSource()
|
||||
transportServer.(*transport.UDPServer).SetHandler(source.(*UDPSource))
|
||||
transportServer.(*transport.UDPServer).Receive()
|
||||
port = transportServer.ListenPort()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// 单端口模式,绑定ssrc
|
||||
if !stream.AppConfig.GB28181.IsMultiPort() {
|
||||
var success bool
|
||||
if tcp {
|
||||
success = SharedTCPServer.filter.AddSource(ssrc, source)
|
||||
} else {
|
||||
success = SharedUDPServer.filter.AddSource(ssrc, source)
|
||||
}
|
||||
|
||||
if !success {
|
||||
return nil, 0, fmt.Errorf("ssrc conflict")
|
||||
}
|
||||
|
||||
port = stream.AppConfig.GB28181.Port[0]
|
||||
} else if !active {
|
||||
// 多端口模式, 创建收流Server
|
||||
if tcp {
|
||||
tcpServer, err := NewTCPServer(NewSingleFilter(source))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
port = tcpServer.tcp.ListenPort()
|
||||
source.(*PassiveSource).transport = tcpServer.tcp
|
||||
} else {
|
||||
server, err := NewUDPServer(NewSingleFilter(source))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
port = server.udp.ListenPort()
|
||||
source.(*UDPSource).transport = server.udp
|
||||
}
|
||||
}
|
||||
|
||||
source.SetType(stream.SourceType28181)
|
||||
source.SetID(id)
|
||||
source.SetSSRC(ssrc)
|
||||
source.Init()
|
||||
if _, state := stream.PreparePublishSource(source, false); utils.HookStateOK != state {
|
||||
return nil, 0, fmt.Errorf("error code %d", state)
|
||||
// 加锁保护一下, 防止初始化阶段, 调用关闭source接口, 发生并发安全问题
|
||||
source.ExecuteWithDeleteLock(func() {
|
||||
if err = stream.AddSource(source); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
source.SetTransport(transportServer)
|
||||
source.Init()
|
||||
})
|
||||
|
||||
// id冲突
|
||||
if err != nil {
|
||||
if transportServer != nil {
|
||||
transportServer.Close()
|
||||
}
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
stream.LoopEvent(source)
|
||||
|
@@ -1,24 +1,30 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"github.com/lkmio/lkm/stream"
|
||||
"github.com/lkmio/transport"
|
||||
"net"
|
||||
)
|
||||
|
||||
type ActiveSource struct {
|
||||
PassiveSource
|
||||
|
||||
*PassiveSource
|
||||
port int
|
||||
remoteAddr net.TCPAddr
|
||||
tcp *TCPClient
|
||||
}
|
||||
|
||||
func (a *ActiveSource) Connect(remoteAddr *net.TCPAddr) error {
|
||||
client, err := NewTCPClient(a.port, remoteAddr, a)
|
||||
client := &transport.TCPClient{}
|
||||
client.SetHandler(a.PassiveSource)
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", stream.ListenAddr(a.port))
|
||||
if err != nil {
|
||||
return err
|
||||
} else if _, err = client.Connect(addr, remoteAddr); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.tcp = client
|
||||
go client.Receive()
|
||||
a.transport = client
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -28,12 +34,23 @@ func (a *ActiveSource) SetupType() SetupType {
|
||||
|
||||
func NewActiveSource() (*ActiveSource, int, error) {
|
||||
var port int
|
||||
TransportManger.AllocPort(true, func(port_ uint16) error {
|
||||
err := TransportManger.AllocPort(true, func(port_ uint16) error {
|
||||
port = int(port_)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return &ActiveSource{
|
||||
PassiveSource: &PassiveSource{
|
||||
StreamServer: stream.StreamServer[GBSource]{
|
||||
SourceType: stream.SourceType28181,
|
||||
},
|
||||
decoder: transport.NewLengthFieldFrameDecoder(0xFFFF, 2),
|
||||
receiveBuffer: stream.TCPReceiveBufferPool.Get().([]byte),
|
||||
},
|
||||
port: port,
|
||||
}, port, nil
|
||||
}
|
||||
|
@@ -1,13 +1,105 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"github.com/lkmio/lkm/log"
|
||||
"github.com/lkmio/lkm/stream"
|
||||
"github.com/lkmio/transport"
|
||||
"net"
|
||||
)
|
||||
|
||||
type PassiveSource struct {
|
||||
stream.StreamServer[GBSource]
|
||||
BaseGBSource
|
||||
decoder *transport.LengthFieldFrameDecoder
|
||||
receiveBuffer []byte
|
||||
remoteAddr string
|
||||
}
|
||||
|
||||
func (p *PassiveSource) SetupType() SetupType {
|
||||
return SetupPassive
|
||||
}
|
||||
|
||||
func NewPassiveSource() *PassiveSource {
|
||||
return &PassiveSource{}
|
||||
func (p *PassiveSource) Close() {
|
||||
p.BaseGBSource.Close()
|
||||
stream.TCPReceiveBufferPool.Put(p.receiveBuffer[:cap(p.receiveBuffer)])
|
||||
}
|
||||
|
||||
func (p *PassiveSource) DecodeGBRTPOverTCPPacket(data []byte) error {
|
||||
length := len(data)
|
||||
for i := 0; i < length; {
|
||||
// 解析粘包数据
|
||||
n, bytes, err := p.decoder.Input(data[i:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i += n
|
||||
if bytes == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if err = p.ProcessPacket(bytes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PassiveSource) OnConnected(conn net.Conn) []byte {
|
||||
p.StreamServer.OnConnected(conn)
|
||||
|
||||
var ok bool
|
||||
p.ExecuteWithDeleteLock(func() {
|
||||
if p.IsClosed() {
|
||||
log.Sugar.Infof("source %s 已关闭, 拒绝新连接", p.GetID())
|
||||
} else if ok = p.PublishSource.Conn == nil; ok {
|
||||
// 一个推流一个端口, 默认第一个连接为有效连接, 关闭其他连接
|
||||
p.PublishSource.Conn = conn
|
||||
p.remoteAddr = conn.RemoteAddr().String()
|
||||
} else {
|
||||
log.Sugar.Infof("port %d 已连接, 关闭连接. source: %s", p.transport.ListenPort(), p.GetID())
|
||||
}
|
||||
})
|
||||
|
||||
if !ok {
|
||||
_ = conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.receiveBuffer
|
||||
}
|
||||
|
||||
func (p *PassiveSource) OnPacket(conn net.Conn, data []byte) []byte {
|
||||
p.StreamServer.OnPacket(conn, data)
|
||||
|
||||
err := p.DecodeGBRTPOverTCPPacket(data)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("解析rtp失败 err: %s conn: %s data: %s", err.Error(), conn.RemoteAddr().String(), hex.EncodeToString(data))
|
||||
_ = conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
return p.receiveBuffer
|
||||
}
|
||||
|
||||
func (p *PassiveSource) OnDisConnected(conn net.Conn, err error) {
|
||||
p.StreamServer.OnDisConnected(conn, err)
|
||||
|
||||
if conn.RemoteAddr().String() == p.remoteAddr {
|
||||
p.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func NewPassiveSource() *PassiveSource {
|
||||
source := &PassiveSource{
|
||||
StreamServer: stream.StreamServer[GBSource]{
|
||||
SourceType: stream.SourceType28181,
|
||||
},
|
||||
decoder: transport.NewLengthFieldFrameDecoder(0xFFFF, 2),
|
||||
receiveBuffer: stream.TCPReceiveBufferPool.Get().([]byte),
|
||||
}
|
||||
|
||||
return source
|
||||
}
|
||||
|
@@ -1,14 +1,16 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"github.com/lkmio/lkm/log"
|
||||
"github.com/lkmio/lkm/stream"
|
||||
"github.com/pion/rtp"
|
||||
"net"
|
||||
)
|
||||
|
||||
// UDPSource 国标UDP推流源
|
||||
type UDPSource struct {
|
||||
stream.StreamServer[interface{}]
|
||||
BaseGBSource
|
||||
|
||||
jitterBuffer *stream.JitterBuffer[*rtp.Packet]
|
||||
}
|
||||
|
||||
@@ -18,12 +20,12 @@ func (u *UDPSource) SetupType() SetupType {
|
||||
|
||||
// OnOrderedRtp 有序RTP包回调
|
||||
func (u *UDPSource) OnOrderedRtp(packet *rtp.Packet) {
|
||||
// 此时还在网络收流携程, 交给Source的主协程处理
|
||||
u.ProcessPacket(packet.Raw)
|
||||
_ = u.ProcessPacket(packet.Raw)
|
||||
// 处理完后, 归还buffer
|
||||
stream.UDPReceiveBufferPool.Put(packet.Raw[:cap(packet.Raw)])
|
||||
}
|
||||
|
||||
// InputRtpPacket 将RTP包排序后,交给Source的主协程处理
|
||||
// InputRtpPacket 将RTP包排序后,交给Source处理
|
||||
func (u *UDPSource) InputRtpPacket(pkt *rtp.Packet) error {
|
||||
block := stream.UDPReceiveBufferPool.Get().([]byte)
|
||||
copy(block, pkt.Raw)
|
||||
@@ -45,8 +47,31 @@ func (u *UDPSource) Close() {
|
||||
u.BaseGBSource.Close()
|
||||
}
|
||||
|
||||
func (u *UDPSource) OnPacket(conn net.Conn, data []byte) []byte {
|
||||
u.StreamServer.OnPacket(conn, data)
|
||||
|
||||
packet := rtp.Packet{}
|
||||
err := packet.Unmarshal(data)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("解析rtp失败 err:%s conn:%s", err.Error(), conn.RemoteAddr().String())
|
||||
return nil
|
||||
} else if u.Conn == nil {
|
||||
u.Conn = conn
|
||||
}
|
||||
|
||||
packet.Raw = data
|
||||
_ = u.InputRtpPacket(&packet)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewUDPSource() *UDPSource {
|
||||
return &UDPSource{
|
||||
source := &UDPSource{
|
||||
jitterBuffer: stream.NewJitterBuffer[*rtp.Packet](),
|
||||
}
|
||||
|
||||
source.StreamServer = stream.StreamServer[interface{}]{
|
||||
SourceType: stream.SourceType28181,
|
||||
}
|
||||
|
||||
return source
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ package gb28181
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -11,9 +10,8 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
ssrcCount uint32
|
||||
lock sync.Mutex
|
||||
SSRCFilters []Filter
|
||||
ssrcCount uint32
|
||||
lock sync.Mutex
|
||||
)
|
||||
|
||||
func NextSSRC() uint32 {
|
||||
@@ -23,19 +21,7 @@ func NextSSRC() uint32 {
|
||||
return ssrcCount
|
||||
}
|
||||
|
||||
func getUniqueSSRC(ssrc string, get func() string) string {
|
||||
atoi, err := strconv.Atoi(ssrc)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
v := uint32(atoi)
|
||||
for _, filter := range SSRCFilters {
|
||||
if filter.FindSource(v) != nil {
|
||||
ssrc = get()
|
||||
}
|
||||
}
|
||||
|
||||
func getUniqueSSRC(ssrc string, _ func() string) string {
|
||||
return ssrc
|
||||
}
|
||||
|
||||
|
@@ -33,7 +33,7 @@ func (d *Demuxer) Input(data []byte) (int, error) {
|
||||
for i := 0; i < length; {
|
||||
n := bufio.MinInt(length-i, 320)
|
||||
_, _ = d.DataPipeline.Write(data[i:i+n], 0, utils.AVMediaTypeAudio)
|
||||
pkt, _ := d.DataPipeline.Feat(0)
|
||||
pkt, _ := d.DataPipeline.Fetch(0)
|
||||
d.OnAudioPacket(0, utils.AVCodecIdPCMALAW, pkt, d.ts)
|
||||
d.ts += int64(n)
|
||||
i += n
|
||||
|
@@ -1,28 +0,0 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"github.com/lkmio/lkm/stream"
|
||||
"github.com/lkmio/transport"
|
||||
"net"
|
||||
)
|
||||
|
||||
// TCPClient GB28181TCP主动收流
|
||||
type TCPClient struct {
|
||||
TCPServer
|
||||
}
|
||||
|
||||
func NewTCPClient(listenPort int, remoteAddr *net.TCPAddr, source GBSource) (*TCPClient, error) {
|
||||
client := &TCPClient{
|
||||
TCPServer{filter: NewSingleFilter(source)},
|
||||
}
|
||||
tcp := transport.TCPClient{}
|
||||
tcp.SetHandler(client)
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", stream.ListenAddr(listenPort))
|
||||
if err != nil {
|
||||
return client, err
|
||||
}
|
||||
|
||||
_, err = tcp.Connect(addr, remoteAddr)
|
||||
return client, err
|
||||
}
|
@@ -1,96 +0,0 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"github.com/lkmio/lkm/log"
|
||||
"github.com/lkmio/lkm/stream"
|
||||
"github.com/lkmio/transport"
|
||||
"net"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// TCPServer GB28181TCP被动收流
|
||||
type TCPServer struct {
|
||||
stream.StreamServer[*TCPSession]
|
||||
tcp *transport.TCPServer
|
||||
filter Filter
|
||||
}
|
||||
|
||||
func (T *TCPServer) OnNewSession(conn net.Conn) *TCPSession {
|
||||
return NewTCPSession(conn, T.filter)
|
||||
}
|
||||
|
||||
func (T *TCPServer) OnCloseSession(session *TCPSession) {
|
||||
session.Close()
|
||||
|
||||
if session.source != nil {
|
||||
T.filter.RemoveSource(session.source.SSRC())
|
||||
}
|
||||
|
||||
if stream.AppConfig.GB28181.IsMultiPort() {
|
||||
T.tcp.Close()
|
||||
T.Handler = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (T *TCPServer) OnConnected(conn net.Conn) []byte {
|
||||
T.StreamServer.OnConnected(conn)
|
||||
return conn.(*transport.Conn).Data.(*TCPSession).receiveBuffer
|
||||
}
|
||||
|
||||
func (T *TCPServer) OnPacket(conn net.Conn, data []byte) []byte {
|
||||
T.StreamServer.OnPacket(conn, data)
|
||||
session := conn.(*transport.Conn).Data.(*TCPSession)
|
||||
|
||||
err := session.DecodeGBRTPOverTCPPacket(data, T.filter, conn)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("解析rtp失败 err: %s conn: %s data: %s", err.Error(), conn.RemoteAddr().String(), hex.EncodeToString(data))
|
||||
_ = conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
return session.receiveBuffer
|
||||
}
|
||||
|
||||
func NewTCPServer(filter Filter) (*TCPServer, error) {
|
||||
server := &TCPServer{
|
||||
filter: filter,
|
||||
}
|
||||
|
||||
var tcp *transport.TCPServer
|
||||
var err error
|
||||
if stream.AppConfig.GB28181.IsMultiPort() {
|
||||
tcp = &transport.TCPServer{}
|
||||
tcp, err = TransportManger.NewTCPServer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
} else {
|
||||
tcp = &transport.TCPServer{
|
||||
ReuseServer: transport.ReuseServer{
|
||||
EnableReuse: true,
|
||||
ConcurrentNumber: runtime.NumCPU(),
|
||||
},
|
||||
}
|
||||
|
||||
var gbAddr *net.TCPAddr
|
||||
gbAddr, err = net.ResolveTCPAddr("tcp", stream.ListenAddr(stream.AppConfig.GB28181.Port[0]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = tcp.Bind(gbAddr); err != nil {
|
||||
return server, err
|
||||
}
|
||||
}
|
||||
|
||||
tcp.SetHandler(server)
|
||||
tcp.Accept()
|
||||
server.tcp = tcp
|
||||
server.StreamServer = stream.StreamServer[*TCPSession]{
|
||||
SourceType: stream.SourceType28181,
|
||||
Handler: server,
|
||||
}
|
||||
return server, nil
|
||||
}
|
@@ -1,88 +0,0 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/lkmio/lkm/stream"
|
||||
"github.com/lkmio/transport"
|
||||
"github.com/pion/rtp"
|
||||
"net"
|
||||
)
|
||||
|
||||
// TCPSession 国标TCP主被动推流Session, 统一处理TCP粘包.
|
||||
type TCPSession struct {
|
||||
conn net.Conn
|
||||
source GBSource
|
||||
decoder *transport.LengthFieldFrameDecoder
|
||||
receiveBuffer []byte
|
||||
}
|
||||
|
||||
func (t *TCPSession) Init(source GBSource) {
|
||||
t.source = source
|
||||
}
|
||||
|
||||
func (t *TCPSession) Close() {
|
||||
t.conn = nil
|
||||
if t.source != nil {
|
||||
t.source.Close()
|
||||
t.source = nil
|
||||
}
|
||||
|
||||
stream.TCPReceiveBufferPool.Put(t.receiveBuffer[:cap(t.receiveBuffer)])
|
||||
}
|
||||
|
||||
func (t *TCPSession) DecodeGBRTPOverTCPPacket(data []byte, filter Filter, conn net.Conn) error {
|
||||
length := len(data)
|
||||
for i := 0; i < length; {
|
||||
// 解析粘包数据
|
||||
n, bytes, err := t.decoder.Input(data[i:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
i += n
|
||||
if bytes == nil {
|
||||
break
|
||||
}
|
||||
|
||||
// 单端口模式,ssrc匹配source
|
||||
if t.source == nil || stream.SessionStateHandshakeSuccess == t.source.State() {
|
||||
packet := rtp.Packet{}
|
||||
if err = packet.Unmarshal(bytes); err != nil {
|
||||
return err
|
||||
} else if t.source == nil {
|
||||
t.source = filter.FindSource(packet.SSRC)
|
||||
}
|
||||
|
||||
if t.source == nil {
|
||||
// ssrc 匹配不到Source
|
||||
return fmt.Errorf("gb28181推流失败 ssrc: %x 匹配不到source", packet.SSRC)
|
||||
}
|
||||
|
||||
if stream.SessionStateHandshakeSuccess == t.source.State() {
|
||||
t.source.PreparePublish(conn, packet.SSRC, t.source)
|
||||
}
|
||||
}
|
||||
|
||||
if err = t.source.ProcessPacket(bytes); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewTCPSession(conn net.Conn, filter Filter) *TCPSession {
|
||||
session := &TCPSession{
|
||||
conn: conn,
|
||||
// filter: filter,
|
||||
decoder: transport.NewLengthFieldFrameDecoder(0xFFFF, 2),
|
||||
receiveBuffer: stream.TCPReceiveBufferPool.Get().([]byte),
|
||||
}
|
||||
|
||||
// 多端口收流, Source已知, 直接初始化Session
|
||||
if stream.AppConfig.GB28181.IsMultiPort() {
|
||||
session.Init(filter.(*singleFilter).source)
|
||||
}
|
||||
|
||||
return session
|
||||
}
|
@@ -1,91 +0,0 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"github.com/lkmio/lkm/log"
|
||||
"github.com/lkmio/lkm/stream"
|
||||
"github.com/lkmio/transport"
|
||||
"github.com/pion/rtp"
|
||||
"net"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// UDPServer GB28181UDP收流
|
||||
type UDPServer struct {
|
||||
stream.StreamServer[*UDPSource]
|
||||
udp *transport.UDPServer
|
||||
filter Filter
|
||||
}
|
||||
|
||||
func (U *UDPServer) OnNewSession(_ net.Conn) *UDPSource {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (U *UDPServer) OnCloseSession(_ *UDPSource) {
|
||||
}
|
||||
|
||||
func (U *UDPServer) OnPacket(conn net.Conn, data []byte) []byte {
|
||||
U.StreamServer.OnPacket(conn, data)
|
||||
|
||||
packet := rtp.Packet{}
|
||||
err := packet.Unmarshal(data)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("解析rtp失败 err:%s conn:%s", err.Error(), conn.RemoteAddr().String())
|
||||
return nil
|
||||
}
|
||||
|
||||
source := U.filter.FindSource(packet.SSRC)
|
||||
if source == nil {
|
||||
log.Sugar.Errorf("ssrc匹配source失败 ssrc:%x conn:%s", packet.SSRC, conn.RemoteAddr().String())
|
||||
return nil
|
||||
}
|
||||
|
||||
if stream.SessionStateHandshakeSuccess == source.State() {
|
||||
conn.(*transport.Conn).Data = source
|
||||
source.PreparePublish(conn, packet.SSRC, source)
|
||||
}
|
||||
|
||||
packet.Raw = data
|
||||
_ = source.(*UDPSource).InputRtpPacket(&packet)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewUDPServer(filter Filter) (*UDPServer, error) {
|
||||
server := &UDPServer{
|
||||
filter: filter,
|
||||
}
|
||||
|
||||
var udp *transport.UDPServer
|
||||
var err error
|
||||
if stream.AppConfig.GB28181.IsMultiPort() {
|
||||
udp, err = TransportManger.NewUDPServer()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
udp = &transport.UDPServer{
|
||||
ReuseServer: transport.ReuseServer{
|
||||
EnableReuse: true,
|
||||
ConcurrentNumber: runtime.NumCPU(),
|
||||
},
|
||||
}
|
||||
|
||||
var gbAddr *net.UDPAddr
|
||||
gbAddr, err = net.ResolveUDPAddr("udp", stream.ListenAddr(stream.AppConfig.GB28181.Port[0]))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err = udp.Bind(gbAddr); err != nil {
|
||||
return server, err
|
||||
}
|
||||
}
|
||||
|
||||
udp.SetHandler(server)
|
||||
udp.Receive()
|
||||
server.udp = udp
|
||||
server.StreamServer = stream.StreamServer[*UDPSource]{
|
||||
SourceType: stream.SourceType28181,
|
||||
Handler: server,
|
||||
}
|
||||
return server, nil
|
||||
}
|
77
go.mod
77
go.mod
@@ -1,24 +1,25 @@
|
||||
module github.com/lkmio/lkm
|
||||
|
||||
require (
|
||||
github.com/lkmio/audio-transcoder v0.2.1
|
||||
github.com/lkmio/flv v0.0.0
|
||||
github.com/lkmio/mpeg v0.0.0
|
||||
github.com/lkmio/rtmp v0.0.0
|
||||
github.com/lkmio/rtp v0.0.0
|
||||
github.com/lkmio/transport v0.0.0
|
||||
github.com/lkmio/audio-transcoder v0.2.2
|
||||
github.com/lkmio/avformat v0.0.2
|
||||
github.com/lkmio/flv v0.0.3
|
||||
github.com/lkmio/mpeg v0.0.4
|
||||
github.com/lkmio/rtmp v0.0.3
|
||||
github.com/lkmio/rtp v0.0.2
|
||||
github.com/lkmio/transport v0.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/lkmio/avformat v0.0.0
|
||||
github.com/lkmio/g726 v0.1.3
|
||||
github.com/natefinch/lumberjack v2.0.0+incompatible
|
||||
github.com/pion/interceptor v0.1.25
|
||||
github.com/pion/rtcp v1.2.12
|
||||
github.com/pion/rtp v1.8.5
|
||||
github.com/pion/sdp/v3 v3.0.8
|
||||
github.com/pion/webrtc/v3 v3.2.29
|
||||
github.com/pion/interceptor v0.1.40
|
||||
github.com/pion/rtcp v1.2.15
|
||||
github.com/pion/rtp v1.8.21
|
||||
github.com/pion/sdp/v3 v3.0.14
|
||||
github.com/pion/webrtc/v4 v4.1.3
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2
|
||||
go.uber.org/zap v1.27.0
|
||||
@@ -26,44 +27,30 @@ require (
|
||||
|
||||
require (
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/google/uuid v1.3.1 // indirect
|
||||
github.com/lkmio/g726 v0.1.3 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/pion/datachannel v1.5.5 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.7 // indirect
|
||||
github.com/pion/ice/v2 v2.3.13 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/mdns v0.0.12 // indirect
|
||||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/pion/datachannel v1.5.10 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.6 // indirect
|
||||
github.com/pion/ice/v4 v4.0.10 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/sctp v1.8.12 // indirect
|
||||
github.com/pion/srtp/v2 v2.0.18 // indirect
|
||||
github.com/pion/stun v0.6.1 // indirect
|
||||
github.com/pion/transport/v2 v2.2.3 // indirect
|
||||
github.com/pion/turn/v2 v2.1.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/testify v1.9.0 // indirect
|
||||
github.com/pion/sctp v1.8.39 // indirect
|
||||
github.com/pion/srtp/v3 v3.0.6 // indirect
|
||||
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/term v0.16.0 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
replace github.com/lkmio/avformat => ../avformat
|
||||
|
||||
replace github.com/lkmio/mpeg => ../mpeg
|
||||
|
||||
replace github.com/lkmio/flv => ../flv
|
||||
|
||||
replace github.com/lkmio/rtmp => ../rtmp
|
||||
|
||||
replace github.com/lkmio/transport => ../transport
|
||||
|
||||
replace github.com/lkmio/rtp => ../rtp
|
||||
|
||||
go 1.19
|
||||
go 1.20
|
||||
|
@@ -35,8 +35,8 @@ type TransStream struct {
|
||||
duration int // 切片时长, 单位秒
|
||||
playlistLength int // 最大切片文件个数
|
||||
|
||||
PlaylistFormatPtr *string // 位于内存中的m3u8播放列表,每个sink都引用指针地址.
|
||||
PlaylistFormatPtrCounter []*collections.ReferenceCounter[[]byte] // string指针转byte[], 方便发送给sink
|
||||
PlaylistFormat *string // 位于内存中的m3u8播放列表,每个sink都引用指针地址.
|
||||
PlaylistFormatPtr []*collections.ReferenceCounter[[]byte] // string指针转byte[], 方便发送给sink
|
||||
}
|
||||
|
||||
func (t *TransStream) Input(packet *avformat.AVPacket, index int) ([]*collections.ReferenceCounter[[]byte], int64, bool, error) {
|
||||
@@ -60,8 +60,12 @@ func (t *TransStream) Input(packet *avformat.AVPacket, index int) ([]*collection
|
||||
newSegment = true
|
||||
}
|
||||
|
||||
pts := packet.ConvertPts(90000)
|
||||
dts := packet.ConvertDts(90000)
|
||||
duration := packet.GetDuration(90000)
|
||||
dts := t.Tracks[index].Dts
|
||||
pts := t.Tracks[index].Pts
|
||||
t.Tracks[index].Dts += duration
|
||||
t.Tracks[index].Pts = t.Tracks[index].Dts + packet.GetPtsDtsDelta(90000)
|
||||
|
||||
data := packet.Data
|
||||
if utils.AVMediaTypeVideo == packet.MediaType {
|
||||
data = avformat.AVCCPacket2AnnexB(t.FindTrackWithStreamIndex(packet.Index).Stream, packet)
|
||||
@@ -83,7 +87,7 @@ func (t *TransStream) Input(packet *avformat.AVPacket, index int) ([]*collection
|
||||
|
||||
// 缓存完第二个切片, 才响应发送m3u8文件. 如果一个切片就发, 播放器缓存少会卡顿.
|
||||
if newSegment && t.M3U8Writer.Size() > 1 {
|
||||
return t.PlaylistFormatPtrCounter, -1, true, nil
|
||||
return t.PlaylistFormatPtr, -1, true, nil
|
||||
}
|
||||
|
||||
return nil, -1, true, nil
|
||||
@@ -132,7 +136,7 @@ func (t *TransStream) flushSegment(end bool) error {
|
||||
// m3u8Txt += "#EXT-X-ENDLIST"
|
||||
//}
|
||||
|
||||
*t.PlaylistFormatPtr = m3u8Txt
|
||||
*t.PlaylistFormat = m3u8Txt
|
||||
|
||||
// 写入最新的m3u8到文件
|
||||
if t.m3u8File != nil {
|
||||
@@ -187,7 +191,7 @@ func (t *TransStream) createSegment() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *TransStream) Close() ([]*collections.ReferenceCounter[[]byte], int64, error) {
|
||||
func (t *TransStream) Close() ([]stream.TransStreamSegment, error) {
|
||||
var err error
|
||||
|
||||
if t.ctx.file != nil {
|
||||
@@ -206,7 +210,7 @@ func (t *TransStream) Close() ([]*collections.ReferenceCounter[[]byte], int64, e
|
||||
t.m3u8File = nil
|
||||
}
|
||||
|
||||
return nil, 0, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func stringPtrToBytes(ptr *string) []byte {
|
||||
@@ -273,13 +277,13 @@ func NewTransStream(dir, m3u8Name, tsFormat, tsUrl string, segmentDuration, play
|
||||
}
|
||||
|
||||
if playlistFormat != nil {
|
||||
transStream.PlaylistFormatPtr = playlistFormat
|
||||
transStream.PlaylistFormat = playlistFormat
|
||||
} else {
|
||||
transStream.PlaylistFormatPtr = new(string)
|
||||
transStream.PlaylistFormat = new(string)
|
||||
}
|
||||
|
||||
playlistFormatPtrCounter := collections.NewReferenceCounter[[]byte](stringPtrToBytes(transStream.PlaylistFormatPtr))
|
||||
transStream.PlaylistFormatPtrCounter = append(transStream.PlaylistFormatPtrCounter, playlistFormatPtrCounter)
|
||||
playlistFormatPtrCounter := collections.NewReferenceCounter[[]byte](stringPtrToBytes(transStream.PlaylistFormat))
|
||||
transStream.PlaylistFormatPtr = append(transStream.PlaylistFormatPtr, playlistFormatPtrCounter)
|
||||
// 创建TS封装器
|
||||
muxer := mpeg.NewTSMuxer()
|
||||
|
||||
|
@@ -17,7 +17,7 @@ type Demuxer struct {
|
||||
func (d *Demuxer) ProcessPrevPacket() error {
|
||||
var codec utils.AVCodecID
|
||||
index := d.FindBufferIndex(int(d.prevPacket.pt))
|
||||
bytes, err := d.BaseDemuxer.DataPipeline.Feat(index)
|
||||
bytes, err := d.BaseDemuxer.DataPipeline.Fetch(index)
|
||||
if err != nil {
|
||||
return err
|
||||
} else /*if d.prevPacket.packetType > AudioFrameMark {
|
||||
|
@@ -26,6 +26,7 @@ func (s *jtServer) OnNewSession(conn net.Conn) *Session {
|
||||
|
||||
func (s *jtServer) OnCloseSession(session *Session) {
|
||||
session.Close()
|
||||
stream.TCPReceiveBufferPool.Put(session.receiveBuffer[:cap(session.receiveBuffer)])
|
||||
}
|
||||
|
||||
func (s *jtServer) OnPacket(conn net.Conn, data []byte) []byte {
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package jt1078
|
||||
|
||||
import (
|
||||
"github.com/lkmio/avformat/utils"
|
||||
"github.com/lkmio/lkm/log"
|
||||
"github.com/lkmio/lkm/stream"
|
||||
"github.com/lkmio/transport"
|
||||
@@ -33,20 +32,10 @@ func (s *Session) Input(data []byte) (int, error) {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
// 首包处理, hook通知
|
||||
// 首包处理
|
||||
if firstOfPacket && demuxer.prevPacket != nil {
|
||||
s.SetID(demuxer.sim + "/" + strconv.Itoa(demuxer.channel))
|
||||
|
||||
go func() {
|
||||
_, state := stream.PreparePublishSource(s, true)
|
||||
if utils.HookStateOK != state {
|
||||
log.Sugar.Errorf("1078推流失败 source: %s", demuxer.sim)
|
||||
|
||||
if s.Conn != nil {
|
||||
s.Conn.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
stream.PreparePublishSourceWithAsync(s, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,13 +45,7 @@ func (s *Session) Input(data []byte) (int, error) {
|
||||
func (s *Session) Close() {
|
||||
log.Sugar.Infof("1078推流结束 %s", s.String())
|
||||
|
||||
if s.Conn != nil {
|
||||
s.Conn.Close()
|
||||
s.Conn = nil
|
||||
}
|
||||
|
||||
s.PublishSource.Close()
|
||||
stream.TCPReceiveBufferPool.Put(s.receiveBuffer[:cap(s.receiveBuffer)])
|
||||
}
|
||||
|
||||
func NewSession(conn net.Conn, version int) *Session {
|
||||
|
@@ -218,9 +218,9 @@ func TestPublish(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("publish", func(t *testing.T) {
|
||||
//path := "../../source_files/10352264314-2.bin"
|
||||
path := "../../source_files/10352264314-2.bin"
|
||||
//path := "../../source_files/013800138000-1.bin"
|
||||
path := "../../source_files/0714-1.bin"
|
||||
//path := "../../source_files/0714-1.bin"
|
||||
publish(path, "1078")
|
||||
})
|
||||
|
||||
|
40
main.go
40
main.go
@@ -75,12 +75,10 @@ func init() {
|
||||
// 初始化日志
|
||||
log.InitLogger(config.Log.FileLogging, zapcore.Level(stream.AppConfig.Log.Level), stream.AppConfig.Log.Name, stream.AppConfig.Log.MaxSize, stream.AppConfig.Log.MaxBackup, stream.AppConfig.Log.MaxAge, stream.AppConfig.Log.Compress)
|
||||
|
||||
if stream.AppConfig.GB28181.Enable && stream.AppConfig.GB28181.IsMultiPort() {
|
||||
gb28181.TransportManger = transport.NewTransportManager(config.ListenIP, uint16(stream.AppConfig.GB28181.Port[0]), uint16(stream.AppConfig.GB28181.Port[1]))
|
||||
}
|
||||
|
||||
if stream.AppConfig.Rtsp.Enable && stream.AppConfig.Rtsp.IsMultiPort() {
|
||||
rtsp.TransportManger = transport.NewTransportManager(config.ListenIP, uint16(stream.AppConfig.Rtsp.Port[1]), uint16(stream.AppConfig.Rtsp.Port[2]))
|
||||
if stream.AppConfig.GB28181.Enable || stream.AppConfig.Rtsp.Enable {
|
||||
transportManager := transport.NewTransportManager(config.ListenIP, uint16(stream.AppConfig.MediaPort[0]), uint16(stream.AppConfig.MediaPort[1]))
|
||||
gb28181.TransportManger = transportManager
|
||||
rtsp.TransportManger = transportManager
|
||||
}
|
||||
|
||||
// 创建dump目录
|
||||
@@ -112,7 +110,7 @@ func main() {
|
||||
}
|
||||
|
||||
if stream.AppConfig.Rtsp.Enable {
|
||||
rtspAddr, err := net.ResolveTCPAddr("tcp", stream.ListenAddr(stream.AppConfig.Rtsp.Port[0]))
|
||||
rtspAddr, err := net.ResolveTCPAddr("tcp", stream.ListenAddr(stream.AppConfig.Rtsp.Port))
|
||||
if err != nil {
|
||||
panic(rtspAddr)
|
||||
}
|
||||
@@ -134,33 +132,7 @@ func main() {
|
||||
log.Sugar.Info("启动http服务 addr:", stream.ListenAddr(stream.AppConfig.Http.Port))
|
||||
go startApiServer(net.JoinHostPort(stream.AppConfig.ListenIP, strconv.Itoa(stream.AppConfig.Http.Port)))
|
||||
|
||||
// 单端口模式下, 启动时就创建收流端口
|
||||
// 多端口模式下, 创建GBSource时才创建收流端口
|
||||
if stream.AppConfig.GB28181.Enable && !stream.AppConfig.GB28181.IsMultiPort() {
|
||||
if stream.AppConfig.GB28181.IsEnableUDP() {
|
||||
filter := gb28181.NewSSRCFilter(128)
|
||||
server, err := gb28181.NewUDPServer(filter)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
gb28181.SharedUDPServer = server
|
||||
log.Sugar.Info("启动GB28181 udp收流端口成功:" + stream.ListenAddr(stream.AppConfig.GB28181.Port[0]))
|
||||
gb28181.SSRCFilters = append(gb28181.SSRCFilters, filter)
|
||||
}
|
||||
|
||||
if stream.AppConfig.GB28181.IsEnableTCP() {
|
||||
filter := gb28181.NewSSRCFilter(128)
|
||||
server, err := gb28181.NewTCPServer(filter)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
gb28181.SharedTCPServer = server
|
||||
log.Sugar.Info("启动GB28181 tcp收流端口成功:" + stream.ListenAddr(stream.AppConfig.GB28181.Port[0]))
|
||||
gb28181.SSRCFilters = append(gb28181.SSRCFilters, filter)
|
||||
}
|
||||
}
|
||||
// GB28181收流时调用api创建收流端口
|
||||
|
||||
if stream.AppConfig.JT1078.Enable {
|
||||
// 无法通过包头区分2016和2019, 每个版本创建一个Server
|
||||
|
@@ -11,6 +11,7 @@ import (
|
||||
type FLVFileSink struct {
|
||||
stream.BaseSink
|
||||
file *os.File
|
||||
path string
|
||||
fail bool
|
||||
}
|
||||
|
||||
@@ -47,22 +48,27 @@ func (f *FLVFileSink) Close() {
|
||||
f.file.Close()
|
||||
f.file = nil
|
||||
}
|
||||
|
||||
if source := stream.SourceManager.Find(f.SourceID); source != nil {
|
||||
stream.HookRecordEvent(source, f.path)
|
||||
}
|
||||
}
|
||||
|
||||
// NewFLVFileSink 创建FLV文件录制流Sink
|
||||
// 保存path: dir/sourceId/yyyy-MM-dd/HH-mm-ss.flv
|
||||
func NewFLVFileSink(sourceId string) (stream.Sink, string, error) {
|
||||
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 {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// 创建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 {
|
||||
return nil, "", err
|
||||
}
|
||||
|
@@ -6,8 +6,8 @@ import (
|
||||
"github.com/lkmio/avformat/utils"
|
||||
"github.com/lkmio/lkm/log"
|
||||
"github.com/lkmio/lkm/stream"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/pion/webrtc/v3/pkg/media"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"github.com/pion/webrtc/v4/pkg/media"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
@@ -96,23 +96,25 @@ func (s *Sink) StartStreaming(transStream stream.TransStream) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// offer的sdp, 应答给http请求
|
||||
if s.cb != nil {
|
||||
log.Sugar.Infof("answer: %s", connection.LocalDescription().SDP)
|
||||
s.cb(connection.LocalDescription().SDP)
|
||||
}
|
||||
|
||||
<-complete
|
||||
connection.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {
|
||||
s.state = state
|
||||
log.Sugar.Infof("ice state: %v sink: %d source: %s", state.String(), s.GetID(), s.SourceID)
|
||||
log.Sugar.Infof("ice state: %v sink: %s source: %s", state.String(), stream.SinkID2String(s.GetID()), s.SourceID)
|
||||
|
||||
if state > webrtc.ICEConnectionStateDisconnected {
|
||||
log.Sugar.Errorf("webrtc peer断开连接 sink: %v source: %s", s.GetID(), s.SourceID)
|
||||
log.Sugar.Errorf("webrtc peer断开连接 sink: %s source: %s", stream.SinkID2String(s.GetID()), s.SourceID)
|
||||
s.Close()
|
||||
}
|
||||
})
|
||||
|
||||
s.peer = connection
|
||||
|
||||
// offer的sdp, 应答给http请求
|
||||
if s.cb != nil {
|
||||
s.cb(connection.LocalDescription().SDP)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -6,7 +6,7 @@ import (
|
||||
"github.com/lkmio/avformat/utils"
|
||||
"github.com/lkmio/lkm/stream"
|
||||
"github.com/pion/interceptor"
|
||||
"github.com/pion/webrtc/v3"
|
||||
"github.com/pion/webrtc/v4"
|
||||
"net"
|
||||
)
|
||||
|
||||
@@ -28,25 +28,42 @@ var (
|
||||
|
||||
type transStream struct {
|
||||
stream.BaseTransStream
|
||||
segments []stream.TransStreamSegment
|
||||
}
|
||||
|
||||
func (t *transStream) Input(packet *avformat.AVPacket, _ int) ([]*collections.ReferenceCounter[[]byte], int64, bool, error) {
|
||||
func (t *transStream) Input(packet *avformat.AVPacket, trackIndex int) ([]*collections.ReferenceCounter[[]byte], int64, bool, error) {
|
||||
t.ClearOutStreamBuffer()
|
||||
|
||||
var data *collections.ReferenceCounter[[]byte]
|
||||
if utils.AVMediaTypeAudio == packet.MediaType {
|
||||
t.AppendOutStreamBuffer(collections.NewReferenceCounter(packet.Data))
|
||||
data = collections.NewReferenceCounter(packet.Data)
|
||||
} else if utils.AVMediaTypeVideo == packet.MediaType {
|
||||
avStream := t.FindTrackWithStreamIndex(packet.Index).Stream
|
||||
if packet.Key {
|
||||
extra := avStream.CodecParameters.AnnexBExtraData()
|
||||
t.AppendOutStreamBuffer(collections.NewReferenceCounter(extra))
|
||||
}
|
||||
|
||||
data := avformat.AVCCPacket2AnnexB(avStream, packet)
|
||||
t.AppendOutStreamBuffer(collections.NewReferenceCounter(data))
|
||||
annexBData := avformat.AVCCPacket2AnnexB(t.FindTrackWithStreamIndex(packet.Index).Stream, packet)
|
||||
data = collections.NewReferenceCounter(annexBData)
|
||||
}
|
||||
|
||||
return t.OutBuffer[:t.OutBufferSize], int64(uint32(packet.GetDuration(1000))), utils.AVMediaTypeVideo == packet.MediaType && packet.Key, nil
|
||||
duration := int64(uint32(packet.GetDuration(1000)))
|
||||
key := utils.AVMediaTypeVideo == packet.MediaType && packet.Key
|
||||
if t.HasVideo() && stream.AppConfig.GOPCache {
|
||||
// 遇到视频关键帧, 丢弃前一帧缓存
|
||||
if key {
|
||||
t.segments = t.segments[:0]
|
||||
}
|
||||
|
||||
t.segments = append(t.segments, stream.TransStreamSegment{
|
||||
Data: []*collections.ReferenceCounter[[]byte]{data},
|
||||
TS: duration,
|
||||
Key: key,
|
||||
Index: trackIndex,
|
||||
})
|
||||
}
|
||||
|
||||
t.AppendOutStreamBuffer(data)
|
||||
return t.OutBuffer[:t.OutBufferSize], duration, key, nil
|
||||
}
|
||||
|
||||
func (t *transStream) ReadKeyFrameBuffer() ([]stream.TransStreamSegment, error) {
|
||||
return t.segments, nil
|
||||
}
|
||||
|
||||
func (t *transStream) WriteHeader() error {
|
||||
|
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/lkmio/lkm/stream"
|
||||
"github.com/lkmio/rtmp"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Session RTMP会话, 解析处理Message
|
||||
@@ -40,9 +41,17 @@ func (s *Session) OnPublish(app, stream_ string) utils.HookState {
|
||||
source.SetUrlValues(values)
|
||||
|
||||
// 统一处理source推流事件, source是否已经存在, hook回调....
|
||||
_, state := stream.PreparePublishSource(source, true)
|
||||
if utils.HookStateOK != state {
|
||||
log.Sugar.Errorf("rtmp推流失败 source: %s", sourceId)
|
||||
state := utils.HookStateOK
|
||||
_, err := stream.PreparePublishSource(source, true)
|
||||
if err != nil {
|
||||
str := err.Error()
|
||||
log.Sugar.Errorf("rtmp推流失败 source: %s err: %s", sourceId, str)
|
||||
|
||||
if strings.HasSuffix(str, "exist") {
|
||||
state = utils.HookStateOccupy
|
||||
} else {
|
||||
state = utils.HookStateFailure
|
||||
}
|
||||
} else {
|
||||
s.handle = source
|
||||
s.isPublisher = true
|
||||
@@ -77,7 +86,7 @@ func (s *Session) Input(data []byte) error {
|
||||
s.handle.(*Publisher).UpdateReceiveStats(len(data))
|
||||
|
||||
var err error
|
||||
s.handle.(*Publisher).ExecuteSyncEvent(func() {
|
||||
s.handle.(*Publisher).ExecuteWithStreamLock(func() {
|
||||
err = s.stack.Input(s.conn, data)
|
||||
})
|
||||
|
||||
|
@@ -21,7 +21,7 @@ type transStream struct {
|
||||
metaData *amf0.Object // 推流方携带的元数据
|
||||
}
|
||||
|
||||
func (t *transStream) Input(packet *avformat.AVPacket, _ int) ([]*collections.ReferenceCounter[[]byte], int64, bool, error) {
|
||||
func (t *transStream) Input(packet *avformat.AVPacket, index int) ([]*collections.ReferenceCounter[[]byte], int64, bool, error) {
|
||||
t.ClearOutStreamBuffer()
|
||||
|
||||
var data []byte
|
||||
@@ -37,9 +37,12 @@ func (t *transStream) Input(packet *avformat.AVPacket, _ int) ([]*collections.Re
|
||||
var keyBuffer bool
|
||||
var frameType int
|
||||
|
||||
dts = packet.ConvertDts(1000)
|
||||
pts = packet.ConvertPts(1000)
|
||||
duration := packet.GetDuration(1000)
|
||||
dts = t.Tracks[index].Dts
|
||||
pts = t.Tracks[index].Pts
|
||||
ct := pts - dts
|
||||
t.Tracks[index].Dts += duration
|
||||
t.Tracks[index].Pts = t.Tracks[index].Dts + packet.GetPtsDtsDelta(1000)
|
||||
|
||||
// chunk = header+payload(audio data / video data)
|
||||
if utils.AVMediaTypeAudio == packet.MediaType {
|
||||
@@ -122,7 +125,7 @@ func (t *transStream) ReadExtraData(_ int64) ([]*collections.ReferenceCounter[[]
|
||||
return []*collections.ReferenceCounter[[]byte]{collections.NewReferenceCounter(t.sequenceHeader)}, 0, nil
|
||||
}
|
||||
|
||||
func (t *transStream) ReadKeyFrameBuffer() ([]*collections.ReferenceCounter[[]byte], int64, error) {
|
||||
func (t *transStream) ReadKeyFrameBuffer() ([]stream.TransStreamSegment, error) {
|
||||
t.ClearOutStreamBuffer()
|
||||
|
||||
// 发送当前内存池已有的合并写切片
|
||||
@@ -130,7 +133,17 @@ func (t *transStream) ReadKeyFrameBuffer() ([]*collections.ReferenceCounter[[]by
|
||||
t.AppendOutStreamBuffer(bytes)
|
||||
})
|
||||
|
||||
return t.OutBuffer[:t.OutBufferSize], 0, nil
|
||||
if t.OutBufferSize < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return []stream.TransStreamSegment{
|
||||
{
|
||||
Data: t.OutBuffer[:t.OutBufferSize],
|
||||
TS: 0,
|
||||
Key: true,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *transStream) WriteHeader() error {
|
||||
@@ -223,15 +236,27 @@ func (t *transStream) WriteHeader() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *transStream) Close() ([]*collections.ReferenceCounter[[]byte], int64, error) {
|
||||
func (t *transStream) Close() ([]stream.TransStreamSegment, error) {
|
||||
t.ClearOutStreamBuffer()
|
||||
|
||||
// 发送剩余的流
|
||||
if segment, _ := t.MWBuffer.FlushSegment(); segment != nil {
|
||||
var key bool
|
||||
var segment *collections.ReferenceCounter[[]byte]
|
||||
if segment, key = t.MWBuffer.FlushSegment(); segment != nil {
|
||||
t.AppendOutStreamBuffer(segment)
|
||||
}
|
||||
|
||||
return t.OutBuffer[:t.OutBufferSize], 0, nil
|
||||
if t.OutBufferSize < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return []stream.TransStreamSegment{
|
||||
{
|
||||
Data: t.OutBuffer[:t.OutBufferSize],
|
||||
TS: 0,
|
||||
Key: key,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewTransStream(chunkSize int, metaData *amf0.Object) stream.TransStream {
|
||||
|
@@ -11,6 +11,7 @@ import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
@@ -72,7 +73,7 @@ func (h handler) Process(session *session, method string, url_ *url.URL, headers
|
||||
|
||||
source, _ := stream.Path2SourceID(url_.Path, "")
|
||||
|
||||
//反射调用各个处理函数
|
||||
// 反射调用各个处理函数
|
||||
results := m.Call([]reflect.Value{
|
||||
reflect.ValueOf(&h),
|
||||
reflect.ValueOf(Request{session, source, method, url_, headers}),
|
||||
@@ -220,11 +221,16 @@ func (h handler) OnSetup(request Request) (*http.Response, []byte, error) {
|
||||
|
||||
func (h handler) OnPlay(request Request) (*http.Response, []byte, error) {
|
||||
response := NewOKResponse(request.headers.Get("Cseq"))
|
||||
sessionHeader := request.headers.Get("Session")
|
||||
if sessionHeader != "" {
|
||||
|
||||
response.Header.Set("Date", time.Now().Format("Mon, 02 Jan 2006 15:04:05 GMT"))
|
||||
if sessionHeader := request.headers.Get("Session"); sessionHeader != "" {
|
||||
response.Header.Set("Session", sessionHeader)
|
||||
}
|
||||
|
||||
if rangeV := request.headers.Get("Range"); rangeV != "" {
|
||||
response.Header.Set("Range", rangeV)
|
||||
}
|
||||
|
||||
sink := request.session.sink
|
||||
sink.SetReady(true)
|
||||
source := stream.SourceManager.Find(sink.GetSourceID())
|
||||
@@ -233,6 +239,10 @@ func (h handler) OnPlay(request Request) (*http.Response, []byte, error) {
|
||||
}
|
||||
|
||||
source.GetTransStreamPublisher().AddSink(sink)
|
||||
|
||||
// RTP-Info: url=rtsp://192.168.2.110:8554/hls/mystream/trackID=0;seq=21592;rtptime=4586400,url=rtsp://192.168.2.110:8554/hls/mystream/trackID=1;seq=403;rtptime=412672\r\n
|
||||
//info := <-sink.onPlayResponse
|
||||
//response.Header.Set("RTP-Info", fmt.Sprintf("url=%s;seq=%d;rtptime=%d", "rtsp://192.168.2.119:554/hls/mystream/?track=0", info[0], info[1]))
|
||||
return response, nil, nil
|
||||
}
|
||||
|
||||
|
@@ -1,7 +1,7 @@
|
||||
package rtsp
|
||||
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"github.com/lkmio/avformat/utils"
|
||||
"github.com/lkmio/lkm/log"
|
||||
"github.com/lkmio/transport"
|
||||
@@ -64,20 +64,25 @@ func (s *server) OnConnected(conn net.Conn) []byte {
|
||||
func (s *server) OnPacket(conn net.Conn, data []byte) []byte {
|
||||
t := conn.(*transport.Conn)
|
||||
|
||||
// 丢弃rtp/rtcp包
|
||||
if data[0] == OverTcpMagic || data[0]>>7&0x1 == 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
method, url, header, err := parseMessage(data)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("failed to prase message:%s. err:%s conn:%s", string(data), err.Error(), conn.RemoteAddr().String())
|
||||
log.Sugar.Errorf("failed to prase message: %s. err: %s conn: %s", hex.EncodeToString(data), err.Error(), conn.RemoteAddr().String())
|
||||
_ = conn.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
err = s.handler.Process(t.Data.(*session), method, url, header)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("failed to process message of RTSP. err:%s conn:%s msg:%s", err.Error(), conn.RemoteAddr().String(), string(data))
|
||||
log.Sugar.Errorf("failed to process message of RTSP. err: %s conn: %s msg: %s", err.Error(), conn.RemoteAddr().String(), hex.EncodeToString(data))
|
||||
_ = conn.Close()
|
||||
}
|
||||
|
||||
//后续实现rtsp推流, 需要返回收流buffer
|
||||
// 后续实现rtsp推流, 需要返回收流buffer
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@@ -100,8 +100,10 @@ func (s *Sink) Write(index int, data []*collections.ReferenceCounter[[]byte], rt
|
||||
continue
|
||||
}
|
||||
|
||||
nano := uint64(time.Now().UnixNano())
|
||||
ntp := (nano/1000000000 + 2208988800<<32) | (nano % 1000000000)
|
||||
nano := time.Now().UnixNano()
|
||||
seconds := uint64(nano/1e9 + 2208988800)
|
||||
fraction := uint64((nano % 1e9) * (1 << 32) / 1e9)
|
||||
ntp := (seconds << 32) | fraction
|
||||
sr := rtcp.SenderReport{
|
||||
SSRC: sender.SSRC,
|
||||
NTPTime: ntp,
|
||||
|
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/lkmio/avformat/avc"
|
||||
"github.com/lkmio/avformat/collections"
|
||||
"github.com/lkmio/avformat/utils"
|
||||
"github.com/lkmio/lkm/log"
|
||||
"github.com/lkmio/lkm/stream"
|
||||
"github.com/lkmio/rtp"
|
||||
"github.com/pion/sdp/v3"
|
||||
@@ -20,19 +21,17 @@ const (
|
||||
)
|
||||
|
||||
// TransStream rtsp传输流封装
|
||||
// 低延迟是rtsp特性, 所以不考虑实现GOP缓存
|
||||
type TransStream struct {
|
||||
stream.BaseTransStream
|
||||
addr net.IPAddr
|
||||
addrType string
|
||||
urlFormat string
|
||||
|
||||
RtspTracks []*Track
|
||||
//oldTracks []*Track
|
||||
oldTracks map[int]uint16
|
||||
sdp string
|
||||
|
||||
rtpBuffer *stream.RtpBuffer
|
||||
sdp string
|
||||
RtspTracks []*Track
|
||||
lastEndSeq map[utils.AVCodecID]uint16 // 上次结束推流的rtp seq
|
||||
segments []stream.TransStreamSegment // 缓存的切片
|
||||
packetAllocator *stream.RtpBuffer // 分配rtp包
|
||||
}
|
||||
|
||||
func (t *TransStream) OverTCP(data []byte, channel int) {
|
||||
@@ -45,77 +44,106 @@ func (t *TransStream) Input(packet *avformat.AVPacket, trackIndex int) ([]*colle
|
||||
var ts uint32
|
||||
var result []*collections.ReferenceCounter[[]byte]
|
||||
track := t.RtspTracks[trackIndex]
|
||||
|
||||
duration := packet.GetDuration(track.payload.ClockRate)
|
||||
//dts := t.Tracks[trackIndex].Dts
|
||||
ts = uint32(t.Tracks[trackIndex].Pts)
|
||||
t.Tracks[trackIndex].Dts += duration
|
||||
t.Tracks[trackIndex].Pts = t.Tracks[trackIndex].Dts + packet.GetPtsDtsDelta(track.payload.ClockRate)
|
||||
|
||||
if utils.AVMediaTypeAudio == packet.MediaType {
|
||||
ts = uint32(packet.ConvertPts(track.payload.ClockRate))
|
||||
result = t.PackRtpPayload(track, trackIndex, packet.Data, ts)
|
||||
result = t.PackRtpPayload(track, trackIndex, packet.Data, ts, false)
|
||||
} else if utils.AVMediaTypeVideo == packet.MediaType {
|
||||
ts = uint32(packet.ConvertPts(track.payload.ClockRate))
|
||||
annexBData := avformat.AVCCPacket2AnnexB(t.BaseTransStream.Tracks[trackIndex].Stream, packet)
|
||||
data := avc.RemoveStartCode(annexBData)
|
||||
result = t.PackRtpPayload(track, trackIndex, data, ts)
|
||||
result = t.PackRtpPayload(track, trackIndex, annexBData, ts, packet.Key)
|
||||
}
|
||||
|
||||
return result, int64(ts), utils.AVMediaTypeVideo == packet.MediaType && packet.Key, nil
|
||||
}
|
||||
|
||||
func (t *TransStream) ReadExtraData(ts int64) ([]*collections.ReferenceCounter[[]byte], int64, error) {
|
||||
// 返回视频编码数据的rtp包
|
||||
for _, track := range t.RtspTracks {
|
||||
if utils.AVMediaTypeVideo != track.MediaType {
|
||||
continue
|
||||
}
|
||||
|
||||
// 回滚序号和时间戳
|
||||
index := int(track.StartSeq) - len(track.ExtraDataBuffer)
|
||||
for i, packet := range track.ExtraDataBuffer {
|
||||
rtp.RollbackSeq(packet.Get()[OverTcpHeaderSize:], index+i+1)
|
||||
binary.BigEndian.PutUint32(packet.Get()[OverTcpHeaderSize+4:], uint32(ts))
|
||||
}
|
||||
|
||||
// 目前只有视频需要发送扩展数据的rtp包, 所以直接返回
|
||||
return track.ExtraDataBuffer, ts, nil
|
||||
}
|
||||
|
||||
return nil, ts, nil
|
||||
func (t *TransStream) ReadKeyFrameBuffer() ([]stream.TransStreamSegment, error) {
|
||||
// 默认不开启rtsp的关键帧缓存, 一次发送rtp包过多, 播放器的jitter buffer可能会溢出丢弃, 造成播放花屏
|
||||
//return t.segments, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// PackRtpPayload 打包返回rtp over tcp的数据包
|
||||
func (t *TransStream) PackRtpPayload(track *Track, channel int, data []byte, timestamp uint32) []*collections.ReferenceCounter[[]byte] {
|
||||
func (t *TransStream) PackRtpPayload(track *Track, trackIndex int, data []byte, timestamp uint32, videoKey bool) []*collections.ReferenceCounter[[]byte] {
|
||||
// 分割nalu
|
||||
var payloads [][]byte
|
||||
if utils.AVCodecIdH264 == track.CodecID || utils.AVCodecIdH265 == track.CodecID {
|
||||
avc.SplitNalU(data, func(nalu []byte) {
|
||||
payloads = append(payloads, avc.RemoveStartCode(nalu))
|
||||
})
|
||||
} else {
|
||||
payloads = append(payloads, data)
|
||||
}
|
||||
|
||||
var result []*collections.ReferenceCounter[[]byte]
|
||||
var packet []byte
|
||||
var counter *collections.ReferenceCounter[[]byte]
|
||||
|
||||
// 保存开始序号
|
||||
track.StartSeq = track.Muxer.GetHeader().Seq
|
||||
track.Muxer.Input(data, timestamp, func() []byte {
|
||||
counter = t.rtpBuffer.Get()
|
||||
counter.Refer()
|
||||
for _, payload := range payloads {
|
||||
// 保存开始序号
|
||||
track.StartSeq = track.Muxer.GetHeader().Seq
|
||||
track.Muxer.Input(payload, timestamp, func() []byte {
|
||||
counter = t.packetAllocator.Get()
|
||||
counter.Refer()
|
||||
|
||||
packet = counter.Get()
|
||||
return packet[OverTcpHeaderSize:]
|
||||
}, func(bytes []byte) {
|
||||
track.EndSeq = track.Muxer.GetHeader().Seq
|
||||
overTCPPacket := packet[:OverTcpHeaderSize+len(bytes)]
|
||||
t.OverTCP(overTCPPacket, channel)
|
||||
packet = counter.Get()
|
||||
// 预留rtp over tcp 4字节头部
|
||||
return packet[OverTcpHeaderSize:]
|
||||
}, func(bytes []byte) {
|
||||
track.EndSeq = track.Muxer.GetHeader().Seq
|
||||
// 每个包都存在rtp over tcp 4字节头部
|
||||
overTCPPacket := packet[:OverTcpHeaderSize+len(bytes)]
|
||||
t.OverTCP(overTCPPacket, trackIndex)
|
||||
|
||||
counter.ResetData(overTCPPacket)
|
||||
result = append(result, counter)
|
||||
})
|
||||
counter.ResetData(overTCPPacket)
|
||||
result = append(result, counter)
|
||||
})
|
||||
}
|
||||
|
||||
// 引用计数保持为1
|
||||
for _, pkt := range result {
|
||||
pkt.Release()
|
||||
}
|
||||
|
||||
if t.HasVideo() && stream.AppConfig.GOPCache {
|
||||
// 遇到视频关键帧, 丢弃前一帧缓存
|
||||
if videoKey {
|
||||
for _, segment := range t.segments {
|
||||
for _, pkt := range segment.Data {
|
||||
pkt.Release()
|
||||
}
|
||||
}
|
||||
|
||||
t.segments = t.segments[:0]
|
||||
}
|
||||
|
||||
// 计数+1
|
||||
for _, pkt := range result {
|
||||
pkt.Refer()
|
||||
}
|
||||
|
||||
// 放在缓存末尾
|
||||
t.segments = append(t.segments, stream.TransStreamSegment{
|
||||
Data: result,
|
||||
TS: int64(timestamp),
|
||||
Key: videoKey,
|
||||
Index: trackIndex,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (t *TransStream) AddTrack(track *stream.Track) (int, error) {
|
||||
// 恢复上次拉流的序号
|
||||
var startSeq uint16
|
||||
if t.oldTracks != nil {
|
||||
if t.lastEndSeq != nil {
|
||||
var ok bool
|
||||
startSeq, ok = t.oldTracks[int(track.Stream.CodecID)]
|
||||
startSeq, ok = t.lastEndSeq[track.Stream.CodecID]
|
||||
utils.Assert(ok)
|
||||
}
|
||||
|
||||
@@ -132,15 +160,9 @@ func (t *TransStream) AddTrack(track *stream.Track) (int, error) {
|
||||
trackIndex := len(t.RtspTracks) - 1
|
||||
|
||||
// 将sps和pps按照单一模式打包
|
||||
var extraDataPackets []*collections.ReferenceCounter[[]byte]
|
||||
packAndAdd := func(data []byte) {
|
||||
packets := t.PackRtpPayload(rtspTrack, trackIndex, data, 0)
|
||||
for _, packet := range packets {
|
||||
extra := packet.Get()
|
||||
bytes := make([]byte, len(extra))
|
||||
copy(bytes, extra)
|
||||
extraDataPackets = append(extraDataPackets, collections.NewReferenceCounter(bytes))
|
||||
}
|
||||
packets := t.PackRtpPayload(rtspTrack, trackIndex, data, 0, true)
|
||||
utils.Assert(len(packets) == 1)
|
||||
}
|
||||
|
||||
if utils.AVMediaTypeVideo == track.Stream.MediaType {
|
||||
@@ -154,21 +176,19 @@ func (t *TransStream) AddTrack(track *stream.Track) (int, error) {
|
||||
ppsBytes := parameters.PPS()
|
||||
packAndAdd(avc.RemoveStartCode(spsBytes[0]))
|
||||
packAndAdd(avc.RemoveStartCode(ppsBytes[0]))
|
||||
|
||||
t.RtspTracks[trackIndex].ExtraDataBuffer = extraDataPackets
|
||||
}
|
||||
|
||||
return trackIndex, nil
|
||||
}
|
||||
|
||||
func (t *TransStream) Close() ([]*collections.ReferenceCounter[[]byte], int64, error) {
|
||||
func (t *TransStream) Close() ([]stream.TransStreamSegment, error) {
|
||||
for _, track := range t.RtspTracks {
|
||||
if track != nil {
|
||||
track.Close()
|
||||
}
|
||||
}
|
||||
|
||||
return nil, 0, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (t *TransStream) WriteHeader() error {
|
||||
@@ -238,7 +258,7 @@ func (t *TransStream) WriteHeader() error {
|
||||
mediaDescription.Attributes = append(mediaDescription.Attributes, fmtp)
|
||||
}
|
||||
|
||||
} else {
|
||||
} else if utils.AVMediaTypeVideo == track.MediaType {
|
||||
mediaDescription.MediaName.Media = "video"
|
||||
}
|
||||
|
||||
@@ -254,12 +274,12 @@ func (t *TransStream) WriteHeader() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewTransStream(addr net.IPAddr, urlFormat string, oldTracks map[int]uint16) stream.TransStream {
|
||||
func NewTransStream(addr net.IPAddr, urlFormat string, oldTracks map[utils.AVCodecID]uint16) stream.TransStream {
|
||||
t := &TransStream{
|
||||
addr: addr,
|
||||
urlFormat: urlFormat,
|
||||
oldTracks: oldTracks,
|
||||
rtpBuffer: stream.NewRtpBuffer(512),
|
||||
addr: addr,
|
||||
urlFormat: urlFormat,
|
||||
lastEndSeq: oldTracks,
|
||||
packetAllocator: stream.NewRtpBuffer(512),
|
||||
}
|
||||
|
||||
if addr.IP.To4() != nil {
|
||||
@@ -273,9 +293,14 @@ func NewTransStream(addr net.IPAddr, urlFormat string, oldTracks map[int]uint16)
|
||||
|
||||
func TransStreamFactory(source stream.Source, _ stream.TransStreamProtocol, _ []*stream.Track, _ stream.Sink) (stream.TransStream, error) {
|
||||
trackFormat := "?track=%d"
|
||||
var oldTracks map[int]uint16
|
||||
var oldTracks map[utils.AVCodecID]uint16
|
||||
if endInfo := source.GetTransStreamPublisher().GetStreamEndInfo(); endInfo != nil {
|
||||
oldTracks = endInfo.RtspTracks
|
||||
if oldTracks != nil {
|
||||
for codecID, seq := range oldTracks {
|
||||
log.Sugar.Infof("track codecID: %s, seq: %d", codecID, seq)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NewTransStream(net.IPAddr{
|
||||
|
@@ -1,7 +1,6 @@
|
||||
package rtsp
|
||||
|
||||
import (
|
||||
"github.com/lkmio/avformat/collections"
|
||||
"github.com/lkmio/avformat/utils"
|
||||
"github.com/lkmio/rtp"
|
||||
)
|
||||
@@ -13,20 +12,19 @@ type Track struct {
|
||||
StartSeq uint16
|
||||
EndSeq uint16
|
||||
CodecID utils.AVCodecID
|
||||
|
||||
Muxer rtp.Muxer
|
||||
ExtraDataBuffer []*collections.ReferenceCounter[[]byte] // 缓存带有编码信息的rtp包, 对所有sink通用
|
||||
Muxer rtp.Muxer
|
||||
}
|
||||
|
||||
func (r *Track) Close() {
|
||||
|
||||
}
|
||||
|
||||
func NewRTSPTrack(muxer rtp.Muxer, payload rtp.PayloadType, mediaType utils.AVMediaType, id utils.AVCodecID) *Track {
|
||||
stream := &Track{
|
||||
payload: payload,
|
||||
Muxer: muxer,
|
||||
MediaType: mediaType,
|
||||
CodecID: id,
|
||||
Muxer: muxer,
|
||||
}
|
||||
|
||||
return stream
|
||||
|
@@ -77,10 +77,8 @@ type JT1078Config struct {
|
||||
}
|
||||
|
||||
type RtspConfig struct {
|
||||
TransportConfig
|
||||
|
||||
enableConfig
|
||||
Port []int `json:"port"`
|
||||
Port int `json:"port"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
@@ -106,8 +104,6 @@ type HttpConfig struct {
|
||||
|
||||
type GB28181Config struct {
|
||||
enableConfig
|
||||
TransportConfig
|
||||
Port []int `json:"port"`
|
||||
}
|
||||
|
||||
type WebRtcConfig struct {
|
||||
@@ -124,14 +120,6 @@ func (g TransportConfig) IsEnableUDP() bool {
|
||||
return strings.Contains(g.Transport, "UDP")
|
||||
}
|
||||
|
||||
func (g GB28181Config) IsMultiPort() bool {
|
||||
return len(g.Port) > 1
|
||||
}
|
||||
|
||||
func (g RtspConfig) IsMultiPort() bool {
|
||||
return len(g.Port) == 3
|
||||
}
|
||||
|
||||
// M3U8Path 根据sourceId返回m3u8的磁盘路径
|
||||
// 切片及目录生成规则, 以SourceId为34020000001320000001/34020000001320000001为例:
|
||||
// 创建文件夹34020000001320000001, 34020000001320000001.m3u8文件, 文件列表中切片url为34020000001320000001_seq.ts
|
||||
@@ -215,7 +203,7 @@ func GetStreamPlayUrls(source string) []string {
|
||||
|
||||
if AppConfig.Rtsp.Enable {
|
||||
// 不拼接userinfo
|
||||
urls = append(urls, fmt.Sprintf("rtsp://%s:%d/%s", AppConfig.PublicIP, AppConfig.Rtsp.Port[0], source))
|
||||
urls = append(urls, fmt.Sprintf("rtsp://%s:%d/%s", AppConfig.PublicIP, AppConfig.Rtsp.Port, source))
|
||||
}
|
||||
|
||||
//if AppConfig.Http.Enable {
|
||||
@@ -232,6 +220,28 @@ func GetStreamPlayUrls(source string) []string {
|
||||
return urls
|
||||
}
|
||||
|
||||
func GetStreamPlayUrlsMap(source string) map[string]string {
|
||||
urls := GetStreamPlayUrls(source)
|
||||
playUrlMap := make(map[string]string)
|
||||
for _, url := range urls {
|
||||
|
||||
if strings.HasPrefix(url, "ws") {
|
||||
playUrlMap["ws_flv"] = url
|
||||
} else if strings.HasSuffix(url, ".flv") {
|
||||
playUrlMap["flv"] = url
|
||||
} else if strings.HasSuffix(url, ".m3u8") {
|
||||
playUrlMap["hls"] = url
|
||||
} else if strings.HasSuffix(url, ".rtc") {
|
||||
playUrlMap["rtc"] = url
|
||||
} else if strings.HasPrefix(url, "rtmp") {
|
||||
playUrlMap["rtmp"] = url
|
||||
} else if strings.HasPrefix(url, "rtsp") {
|
||||
playUrlMap["rtsp"] = url
|
||||
}
|
||||
}
|
||||
return playUrlMap
|
||||
}
|
||||
|
||||
// DumpStream2File 保存推流到文件, 用4字节帧长分割
|
||||
func DumpStream2File(sourceType SourceType, conn net.Conn, data []byte) {
|
||||
path := fmt.Sprintf("dump/%s-%s", sourceType.String(), conn.RemoteAddr().String())
|
||||
@@ -273,6 +283,7 @@ type AppConfig_ struct {
|
||||
IdleTimeout int64 `json:"idle_timeout"` // 多长时间(单位秒)没有拉流. 如果开启hook通知, 根据hook响应, 决定是否关闭Source(200-不关闭/非200关闭). 否则会直接关闭Source.
|
||||
ReceiveTimeout int64 `json:"receive_timeout"` // 多长时间(单位秒)没有收到流. 如果开启hook通知, 根据hook响应, 决定是否关闭Source(200-不关闭/非200关闭). 否则会直接关闭Source.
|
||||
Debug bool `json:"debug"` // debug模式, 开启将保存推流
|
||||
MediaPort []int `json:"media_port"` // 媒体端口范围
|
||||
|
||||
//缓存指定时长的包,满了之后才发送给Sink. 可以降低用户态和内核态的交互频率,大幅提升性能.
|
||||
//合并写的大小范围,应当大于一帧的时长,不超过一组GOP的时长,在实际发送流的时候也会遵循此条例.
|
||||
@@ -308,7 +319,7 @@ func SetDefaultConfig(config *AppConfig_) {
|
||||
if !config.GOPCache {
|
||||
config.GOPCache = true
|
||||
config.MergeWriteLatency = 350
|
||||
log.Sugar.Warnf("强制开启GOP缓存")
|
||||
println("强制开启GOP缓存")
|
||||
}
|
||||
|
||||
config.MergeWriteLatency = limitInt(350, 2000, config.MergeWriteLatency) // 最低缓存350毫秒数据才发送 最高缓存2秒数据才发送
|
||||
@@ -340,3 +351,8 @@ func limitInt(min, max, value int) int {
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
// GenerateRecordStreamPlayUrl 生成录制文件的播放url
|
||||
func GenerateRecordStreamPlayUrl(recordFile string) string {
|
||||
return fmt.Sprintf("http://%s:%d/record/%s", AppConfig.PublicIP, AppConfig.Http.Port, recordFile)
|
||||
}
|
||||
|
@@ -2,6 +2,7 @@ package stream
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"github.com/lkmio/avformat/collections"
|
||||
"github.com/lkmio/lkm/log"
|
||||
"github.com/lkmio/transport"
|
||||
@@ -34,6 +35,7 @@ func (t TransportType) String() string {
|
||||
type ForwardSink struct {
|
||||
BaseSink
|
||||
socket transport.Transport
|
||||
spareSocket transport.Transport // 对讲备选udp发送
|
||||
transportType TransportType
|
||||
receiveTimer *time.Timer
|
||||
ssrc uint32
|
||||
@@ -68,7 +70,7 @@ func (f *ForwardSink) OnDisConnected(conn net.Conn, err error) {
|
||||
|
||||
func (f *ForwardSink) Write(index int, data []*collections.ReferenceCounter[[]byte], ts int64, keyVideo bool) error {
|
||||
// TCP等待连接后再转发数据
|
||||
if TransportTypeUDP != f.transportType && f.Conn == nil {
|
||||
if TransportTypeUDP != f.transportType && f.Conn == nil && f.spareSocket == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -81,12 +83,11 @@ func (f *ForwardSink) Write(index int, data []*collections.ReferenceCounter[[]by
|
||||
f.rtpBuffer = NewRtpBuffer(1024)
|
||||
}
|
||||
|
||||
processedData = make([]*collections.ReferenceCounter[[]byte], 0, len(data))
|
||||
} else if f.rtpBuffer == nil {
|
||||
f.rtpBuffer = NewRtpBuffer(1)
|
||||
}
|
||||
|
||||
for i, datum := range data {
|
||||
for _, datum := range data {
|
||||
src := datum.Get()
|
||||
counter := f.rtpBuffer.Get()
|
||||
bytes := counter.Get()
|
||||
@@ -103,7 +104,7 @@ func (f *ForwardSink) Write(index int, data []*collections.ReferenceCounter[[]by
|
||||
} else {
|
||||
counter.ResetData(bytes[:length])
|
||||
counter.Refer()
|
||||
processedData[i] = counter
|
||||
processedData = append(processedData, counter)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +123,15 @@ func (f *ForwardSink) Write(index int, data []*collections.ReferenceCounter[[]by
|
||||
processedData = data
|
||||
}
|
||||
|
||||
if TransportTypeUDP == f.transportType {
|
||||
spare := f.Conn == nil && f.spareSocket != nil
|
||||
if TransportTypeUDP == f.transportType || spare {
|
||||
var sender = f.socket
|
||||
if spare {
|
||||
sender = f.spareSocket
|
||||
}
|
||||
|
||||
for _, datum := range processedData {
|
||||
f.socket.(*transport.UDPClient).Write(datum.Get()[2:])
|
||||
sender.(*transport.UDPClient).Write(datum.Get()[2:])
|
||||
}
|
||||
} else {
|
||||
return f.BaseSink.Write(index, processedData, ts, keyVideo)
|
||||
@@ -141,6 +148,10 @@ func (f *ForwardSink) Close() {
|
||||
f.socket.Close()
|
||||
}
|
||||
|
||||
if f.spareSocket != nil {
|
||||
f.spareSocket.Close()
|
||||
}
|
||||
|
||||
if f.receiveTimer != nil {
|
||||
f.receiveTimer.Stop()
|
||||
}
|
||||
@@ -169,21 +180,19 @@ func NewForwardSink(transportType TransportType, protocol TransStreamProtocol, s
|
||||
BaseSink: BaseSink{ID: sinkId, SourceID: sourceId, State: SessionStateCreated, Protocol: protocol},
|
||||
transportType: transportType,
|
||||
ssrc: ssrc,
|
||||
requireSSRCMatch: true, // 默认要求ssrc一致
|
||||
requireSSRCMatch: false, // 默认允许ssrc可以不一致
|
||||
}
|
||||
|
||||
if transportType == TransportTypeUDP {
|
||||
remoteAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
client, err := manager.NewUDPClient(remoteAddr)
|
||||
if err != nil {
|
||||
} else if client, err := manager.NewUDPClient(remoteAddr); err != nil {
|
||||
return nil, 0, err
|
||||
} else {
|
||||
sink.socket = client
|
||||
}
|
||||
|
||||
sink.socket = client
|
||||
} else if transportType == TransportTypeTCPClient {
|
||||
client := transport.TCPClient{}
|
||||
err := manager.AllocPort(true, func(port uint16) error {
|
||||
@@ -221,7 +230,20 @@ func NewForwardSink(transportType TransportType, protocol TransStreamProtocol, s
|
||||
tcpServer.SetHandler(sink)
|
||||
tcpServer.Accept()
|
||||
sink.socket = tcpServer
|
||||
sink.StartReceiveTimer()
|
||||
|
||||
// 同时创建udp发送器, 兼容不支持tcp对讲的设备
|
||||
if TransStreamGBTalk == protocol {
|
||||
localAddr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", AppConfig.ListenIP, tcpServer.ListenPort()))
|
||||
remoteAddr, err := net.ResolveUDPAddr("udp", addr)
|
||||
|
||||
udp := &transport.UDPClient{}
|
||||
err = udp.Connect(localAddr, remoteAddr)
|
||||
if err == nil {
|
||||
sink.spareSocket = udp
|
||||
}
|
||||
} else {
|
||||
sink.StartReceiveTimer()
|
||||
}
|
||||
}
|
||||
|
||||
return sink, sink.socket.ListenPort(), nil
|
||||
|
@@ -59,15 +59,16 @@ func Hook(event HookEvent, params string, body interface{}) (*http.Response, err
|
||||
response, err := SendHookEvent(url, bytes)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("failed to %s the hook event. err: %s", event.ToString(), err.Error())
|
||||
return response, err
|
||||
} else {
|
||||
log.Sugar.Infof("received response for hook %s event: status='%s', response body='%s'", event.ToString(), response.Status, responseBodyToString(response))
|
||||
}
|
||||
|
||||
if err == nil && http.StatusOK != response.StatusCode {
|
||||
return response, fmt.Errorf("unexpected response status: %s for request %s", response.Status, url)
|
||||
if http.StatusOK != response.StatusCode {
|
||||
return response, fmt.Errorf("unexpected response status: %s", response.Status)
|
||||
}
|
||||
|
||||
return response, err
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func NewHookPlayEventInfo(sink Sink) eventInfo {
|
||||
|
@@ -16,7 +16,15 @@ func PreparePlaySink(sink Sink, waitTimeout bool) (*http.Response, utils.HookSta
|
||||
var response *http.Response
|
||||
|
||||
if AppConfig.Hooks.IsEnableOnPlay() {
|
||||
hook, err := Hook(HookEventPlay, sink.UrlValues().Encode(), NewHookPlayEventInfo(sink))
|
||||
body := struct {
|
||||
eventInfo
|
||||
Sink string `json:"sink"`
|
||||
}{
|
||||
eventInfo: NewHookPlayEventInfo(sink),
|
||||
Sink: SinkID2String(sink.GetID()),
|
||||
}
|
||||
|
||||
hook, err := Hook(HookEventPlay, sink.UrlValues().Encode(), body)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("播放事件-通知失败 err: %s sink: %s-%v source: %s", err.Error(), sink.GetProtocol().String(), sink.GetID(), sink.GetSourceID())
|
||||
|
||||
|
@@ -2,52 +2,83 @@ package stream
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/lkmio/avformat/utils"
|
||||
"github.com/lkmio/lkm/log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
func PreparePublishSource(source Source, hook bool) (*http.Response, utils.HookState) {
|
||||
var response *http.Response
|
||||
|
||||
if err := SourceManager.Add(source); err != nil {
|
||||
return nil, utils.HookStateOccupy
|
||||
func AddSource(source Source) error {
|
||||
err := SourceManager.add(source)
|
||||
if err == nil {
|
||||
source.SetState(SessionStateHandshakeSuccess)
|
||||
}
|
||||
|
||||
if hook && AppConfig.Hooks.IsEnablePublishEvent() {
|
||||
rep, state := HookPublishEvent(source)
|
||||
if utils.HookStateOK != state {
|
||||
return err
|
||||
}
|
||||
|
||||
func PreparePublishSource(source Source, add bool) (*http.Response, error) {
|
||||
var response *http.Response
|
||||
|
||||
if add {
|
||||
if err := AddSource(source); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if SourceManager.Find(source.GetID()) == nil {
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
|
||||
if AppConfig.Hooks.IsEnablePublishEvent() {
|
||||
rep, err := HookPublishEvent(source)
|
||||
if err != nil {
|
||||
_, _ = SourceManager.Remove(source.GetID())
|
||||
return rep, state
|
||||
return rep, err
|
||||
}
|
||||
|
||||
response = rep
|
||||
}
|
||||
|
||||
// 此时才认为source推流成功
|
||||
source.SetState(SessionStateTransferring)
|
||||
source.SetCreateTime(time.Now())
|
||||
|
||||
urls := GetStreamPlayUrls(source.GetID())
|
||||
indent, _ := json.MarshalIndent(urls, "", "\t")
|
||||
|
||||
log.Sugar.Infof("%s准备推流 source:%s 拉流地址:\r\n%s", source.GetType().String(), source.GetID(), indent)
|
||||
log.Sugar.Infof("%s推流 source: %s 拉流地址:\r\n%s", source.GetType().String(), source.GetID(), indent)
|
||||
|
||||
return response, utils.HookStateOK
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func HookPublishEvent(source Source) (*http.Response, utils.HookState) {
|
||||
var response *http.Response
|
||||
func PreparePublishSourceWithAsync(source Source, add bool) {
|
||||
go func() {
|
||||
var err error
|
||||
// 加锁执行, 保证并发安全
|
||||
source.ExecuteWithDeleteLock(func() {
|
||||
if source.IsClosed() {
|
||||
err = fmt.Errorf("source is closed")
|
||||
} else if _, err = PreparePublishSource(source, add); err == nil {
|
||||
}
|
||||
})
|
||||
|
||||
if AppConfig.Hooks.IsEnablePublishEvent() {
|
||||
hook, err := Hook(HookEventPublish, source.UrlValues().Encode(), NewHookPublishEventInfo(source))
|
||||
if err != nil {
|
||||
return hook, utils.HookStateFailure
|
||||
}
|
||||
log.Sugar.Errorf("GB28181推流失败 err: %s source: %s", err.Error(), source.GetID())
|
||||
|
||||
response = hook
|
||||
if !source.IsClosed() {
|
||||
source.Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
|
||||
func HookPublishEvent(source Source) (*http.Response, error) {
|
||||
if AppConfig.Hooks.IsEnablePublishEvent() {
|
||||
return Hook(HookEventPublish, source.UrlValues().Encode(), NewHookPublishEventInfo(source))
|
||||
}
|
||||
|
||||
return response, utils.HookStateOK
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func HookPublishDoneEvent(source Source) {
|
||||
|
@@ -33,8 +33,8 @@ type MergeWritingBuffer interface {
|
||||
}
|
||||
|
||||
type mbBuffer struct {
|
||||
buffer collections.BlockBuffer
|
||||
segments *collections.Queue[*collections.ReferenceCounter[[]byte]]
|
||||
buffer collections.BlockBuffer // 合并写内存缓冲区
|
||||
segments *collections.Queue[*collections.ReferenceCounter[[]byte]] // 包含多个合并写切片
|
||||
}
|
||||
|
||||
type mergeWritingBuffer struct {
|
||||
@@ -56,13 +56,15 @@ func (m *mergeWritingBuffer) TryAlloc(size int, ts int64, videoPkt, videoKey boo
|
||||
|
||||
buffer := m.buffers.Peek(m.buffers.Size() - 1).buffer
|
||||
bytes := buffer.AvailableBytes()
|
||||
// 内存不足, 分配新的内存缓冲区
|
||||
if bytes < size {
|
||||
// 非完整切片,先保存切片再分配新的内存
|
||||
// 让外部先flush, 再分配新的内存
|
||||
if buffer.PendingBlockSize() > 0 {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// -1, 当前内存池不释放
|
||||
// 释放未使用的内存缓冲区
|
||||
// -1, 最新的内存缓冲区不释放
|
||||
release(m.buffers, m.buffers.Size()-1)
|
||||
m.buffers.Push(MWBufferPool.Get().(*mbBuffer))
|
||||
}
|
||||
@@ -100,7 +102,7 @@ func (m *mergeWritingBuffer) alloc(size int, ts int64, videoPkt, videoKey bool)
|
||||
|
||||
func (m *mergeWritingBuffer) FlushSegment() (*collections.ReferenceCounter[[]byte], bool) {
|
||||
buffer := m.buffers.Peek(m.buffers.Size() - 1)
|
||||
data := buffer.buffer.Feat()
|
||||
data := buffer.buffer.Fetch()
|
||||
if len(data) == 0 {
|
||||
return nil, false
|
||||
}
|
||||
@@ -116,6 +118,7 @@ func (m *mergeWritingBuffer) FlushSegment() (*collections.ReferenceCounter[[]byt
|
||||
}
|
||||
|
||||
if AppConfig.GOPCache {
|
||||
// +1=2
|
||||
counter.Refer()
|
||||
m.lastKeyVideoDataSegments.Push(counter)
|
||||
}
|
||||
@@ -172,11 +175,13 @@ func (m *mergeWritingBuffer) HasVideoDataInCurrentSegment() bool {
|
||||
}
|
||||
|
||||
func (m *mergeWritingBuffer) Close() *collections.Queue[*mbBuffer] {
|
||||
// 减少关键帧切片的引用计数
|
||||
for m.lastKeyVideoDataSegments.Size() > 0 {
|
||||
m.lastKeyVideoDataSegments.Pop().Release()
|
||||
}
|
||||
|
||||
if m.buffers.Size() > 0 && !release(m.buffers, m.buffers.Size()) {
|
||||
// 还有sink在使用, 返回未释放的内存缓冲区
|
||||
return m.buffers
|
||||
}
|
||||
|
||||
|
@@ -8,6 +8,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// BlockBufferSize 合并写缓冲区的内存块大小
|
||||
// 一块缓冲区可以包含多个合并写切片
|
||||
BlockBufferSize = 1024 * 1024 * 2
|
||||
)
|
||||
|
||||
@@ -23,37 +25,40 @@ var (
|
||||
},
|
||||
}
|
||||
|
||||
pendingReleaseBuffers = make(map[string]*collections.Queue[*mbBuffer])
|
||||
pendingReleaseBuffers = make(map[string]*collections.Queue[*mbBuffer]) // 等待释放的合并写缓冲区
|
||||
lock sync.Mutex
|
||||
)
|
||||
|
||||
// AddMWBuffersToPending 添加合并写缓冲区到等待释放队列
|
||||
func AddMWBuffersToPending(sourceId string, transStreamId TransStreamID, buffers *collections.Queue[*mbBuffer]) {
|
||||
key := fmt.Sprintf("%s-%d", sourceId, transStreamId)
|
||||
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
for buffers.Size() > 0 {
|
||||
v, ok := pendingReleaseBuffers[key]
|
||||
if ok {
|
||||
// 第二次都推流结束了,第一次的内存还被占用
|
||||
// 强制释放上次推流的内存池
|
||||
log.Sugar.Warnf("force release last pending buffers of %s", key)
|
||||
v, ok := pendingReleaseBuffers[key]
|
||||
if ok {
|
||||
// 第二次都推流结束了,第一次的内存还被占用
|
||||
// 强制释放上次推流的内存池
|
||||
log.Sugar.Warnf("force release last pending buffers of %s", key)
|
||||
|
||||
for v.Size() > 0 {
|
||||
pop := v.Pop()
|
||||
pop.buffer.Clear()
|
||||
pop.segments.Clear()
|
||||
MWBufferPool.Put(pop)
|
||||
}
|
||||
|
||||
delete(pendingReleaseBuffers, key)
|
||||
for v.Size() > 0 {
|
||||
pop := v.Pop()
|
||||
pop.buffer.Clear()
|
||||
pop.segments.Clear()
|
||||
MWBufferPool.Put(pop)
|
||||
}
|
||||
|
||||
delete(pendingReleaseBuffers, key)
|
||||
}
|
||||
|
||||
if buffers.Size() > 0 {
|
||||
pendingReleaseBuffers[key] = buffers
|
||||
}
|
||||
}
|
||||
|
||||
// ReleasePendingBuffers 释放等待释放的合并写缓冲区
|
||||
// 拉流结束后主动调用一次, 创建传输流的时候也调用一次
|
||||
func ReleasePendingBuffers(sourceId string, transStreamId TransStreamID) {
|
||||
key := fmt.Sprintf("%s-%d", sourceId, transStreamId)
|
||||
|
||||
@@ -68,6 +73,7 @@ func ReleasePendingBuffers(sourceId string, transStreamId TransStreamID) {
|
||||
delete(pendingReleaseBuffers, key)
|
||||
}
|
||||
|
||||
// release 释放合并写缓冲区
|
||||
func release(buffers *collections.Queue[*mbBuffer], length int) bool {
|
||||
for i := 0; i < length; i++ {
|
||||
buffer := buffers.Peek(0)
|
||||
|
@@ -10,11 +10,11 @@ type RtpBuffer struct {
|
||||
|
||||
func (r *RtpBuffer) Get() *collections.ReferenceCounter[[]byte] {
|
||||
if r.queue.Size() > 0 {
|
||||
rtp := r.queue.Peek(0)
|
||||
if rtp.UseCount() < 2 {
|
||||
bytes := rtp.Get()
|
||||
rtp.ResetData(bytes[:cap(bytes)])
|
||||
return rtp
|
||||
pkt := r.queue.Peek(0)
|
||||
if pkt.UseCount() < 2 {
|
||||
r.queue.Pop()
|
||||
r.Put(pkt)
|
||||
return pkt
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,13 @@ func (r *RtpBuffer) Clear() {
|
||||
}
|
||||
}
|
||||
|
||||
// Put 归还rtp包
|
||||
func (r *RtpBuffer) Put(pkt *collections.ReferenceCounter[[]byte]) {
|
||||
bytes := pkt.Get()
|
||||
pkt.ResetData(bytes[:cap(bytes)])
|
||||
r.queue.Push(pkt)
|
||||
}
|
||||
|
||||
func NewRtpBuffer(capacity int) *RtpBuffer {
|
||||
return &RtpBuffer{queue: collections.NewQueue[*collections.ReferenceCounter[[]byte]](capacity)}
|
||||
}
|
||||
|
@@ -23,7 +23,7 @@ func (f *RtpStream) Input(packet *avformat.AVPacket, _ int) ([]*collections.Refe
|
||||
bytes := counter.Get()
|
||||
binary.BigEndian.PutUint16(bytes, size-2)
|
||||
copy(bytes[2:], packet.Data)
|
||||
counter.ResetData(bytes[:2+len(bytes)])
|
||||
counter.ResetData(bytes[:size])
|
||||
|
||||
// 每帧都当关键帧, 直接发给上级
|
||||
return []*collections.ReferenceCounter[[]byte]{counter}, -1, true, nil
|
||||
|
@@ -175,7 +175,7 @@ func (s *BaseSink) fastForward(firstSegment *collections.ReferenceCounter[[]byte
|
||||
|
||||
func (s *BaseSink) doAsyncWrite() {
|
||||
defer func() {
|
||||
// 释放未发送的数据
|
||||
// 释放未发送的合并写切片
|
||||
for buffer := s.pendingSendQueue.Pop(); buffer != nil; buffer = s.pendingSendQueue.Pop() {
|
||||
buffer.Release()
|
||||
}
|
||||
@@ -241,9 +241,12 @@ func (s *BaseSink) doAsyncWrite() {
|
||||
|
||||
func (s *BaseSink) EnableAsyncWriteMode(queueSize int) {
|
||||
utils.Assert(s.Conn != nil)
|
||||
s.pendingSendQueue = NewNonBlockingChannel[*collections.ReferenceCounter[[]byte]](queueSize)
|
||||
s.cancelCtx, s.cancelFunc = context.WithCancel(context.Background())
|
||||
go s.doAsyncWrite()
|
||||
// 只初始化一次
|
||||
if s.pendingSendQueue == nil {
|
||||
s.pendingSendQueue = NewNonBlockingChannel[*collections.ReferenceCounter[[]byte]](queueSize)
|
||||
s.cancelCtx, s.cancelFunc = context.WithCancel(context.Background())
|
||||
go s.doAsyncWrite()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BaseSink) Write(index int, data []*collections.ReferenceCounter[[]byte], ts int64, keyVideo bool) error {
|
||||
|
@@ -16,7 +16,7 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
StreamEndInfoBride func(source string, tracks []*Track, streams map[TransStreamID]TransStream) *StreamEndInfo
|
||||
StreamEndInfoBride func(source string, originTracks []*Track, streams map[TransStreamID]TransStream) *StreamEndInfo
|
||||
)
|
||||
|
||||
// Source 对推流源的封装
|
||||
@@ -80,7 +80,9 @@ type Source interface {
|
||||
|
||||
StartTimers(source Source)
|
||||
|
||||
ExecuteSyncEvent(cb func())
|
||||
ExecuteWithStreamLock(cb func())
|
||||
|
||||
ExecuteWithDeleteLock(cb func())
|
||||
|
||||
UpdateReceiveStats(dataLen int)
|
||||
}
|
||||
@@ -105,8 +107,8 @@ type PublishSource struct {
|
||||
createTime time.Time // source创建时间
|
||||
statistics *BitrateStatistics // 码流统计
|
||||
streamLogger avformat.OnUnpackStream2FileHandler
|
||||
// streamLock sync.RWMutex
|
||||
streamLock sync.Mutex
|
||||
streamLock sync.Mutex // 收流、探测超时等操作互斥锁
|
||||
deleteLock sync.Mutex // 双重锁, 防止在关闭source时, 其他操作同时进行
|
||||
|
||||
timers struct {
|
||||
receiveTimer *time.Timer // 收流超时计时器
|
||||
@@ -139,7 +141,7 @@ func (s *PublishSource) SetID(id string) {
|
||||
}
|
||||
|
||||
func (s *PublishSource) Init() {
|
||||
s.SetState(SessionStateHandshakeSuccess)
|
||||
s.SetState(SessionStateCreated)
|
||||
|
||||
s.statistics = NewBitrateStatistics()
|
||||
s.streamPublisher = NewTransStreamPublisher(s.ID)
|
||||
@@ -158,10 +160,8 @@ func (s *PublishSource) Input(data []byte) (int, error) {
|
||||
s.UpdateReceiveStats(len(data))
|
||||
var n int
|
||||
var err error
|
||||
s.ExecuteSyncEvent(func() {
|
||||
if s.closed.Load() {
|
||||
err = fmt.Errorf("source closed")
|
||||
} else {
|
||||
s.ExecuteWithStreamLock(func() {
|
||||
if !s.closed.Load() {
|
||||
n, err = s.TransDemuxer.Input(data)
|
||||
}
|
||||
})
|
||||
@@ -177,18 +177,20 @@ func (s *PublishSource) SetState(state SessionState) {
|
||||
s.state = state
|
||||
}
|
||||
|
||||
func (s *PublishSource) DoClose() {
|
||||
func (s *PublishSource) doClose() {
|
||||
log.Sugar.Debugf("closing the %s source. id: %s. closed flag: %t", s.Type, s.ID, s.closed.Load())
|
||||
|
||||
// 已关闭, 直接返回
|
||||
if s.closed.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
var closed bool
|
||||
s.ExecuteSyncEvent(func() {
|
||||
s.ExecuteWithStreamLock(func() {
|
||||
closed = s.closed.Swap(true)
|
||||
})
|
||||
|
||||
// 已关闭, 直接返回
|
||||
if closed {
|
||||
return
|
||||
}
|
||||
@@ -216,17 +218,20 @@ func (s *PublishSource) DoClose() {
|
||||
s.clearUnusedPackets(track.Packets)
|
||||
}
|
||||
|
||||
// 等传输流发布器关闭结束
|
||||
// 停止发布输出流
|
||||
// 同步执行
|
||||
s.streamPublisher.close()
|
||||
|
||||
// 释放解复用器
|
||||
// 释放转码器
|
||||
// 释放每路转协议流, 将所有sink添加到等待队列
|
||||
_, err := SourceManager.Remove(s.ID)
|
||||
if err != nil {
|
||||
// source不存在, 在创建source时, 未添加到manager中, 目前只有1078流会出现这种情况(tcp连接到端口, 没有推流或推流数据无效, 无法定位到手机号, 以至于无法执行PreparePublishSource函数), 将不再处理后续事情.
|
||||
log.Sugar.Errorf("删除源失败 source: %s err: %s", s.ID, err.Error())
|
||||
return
|
||||
s.state = SessionStateClosed
|
||||
|
||||
// 只删除被添加的source, 否则会造成id相同的source被误删
|
||||
if s.state >= SessionStateHandshakeSuccess {
|
||||
_, err := SourceManager.Remove(s.ID)
|
||||
if err != nil {
|
||||
// source不存在, 在创建source时, 未添加到manager中, 目前只有1078流会出现这种情况(tcp连接到端口, 没有推流或推流数据无效, 无法定位到手机号, 以至于无法执行PreparePublishSource函数), 将不再处理后续事情.
|
||||
log.Sugar.Errorf("删除源失败 source: %s err: %s", s.ID, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 异步hook
|
||||
@@ -241,7 +246,9 @@ func (s *PublishSource) DoClose() {
|
||||
}
|
||||
|
||||
func (s *PublishSource) Close() {
|
||||
s.DoClose()
|
||||
s.ExecuteWithDeleteLock(func() {
|
||||
s.doClose()
|
||||
})
|
||||
}
|
||||
|
||||
// 解析完所有track后, 创建各种输出流
|
||||
@@ -257,8 +264,8 @@ func (s *PublishSource) writeHeader() {
|
||||
|
||||
if len(s.originTracks.All()) == 0 {
|
||||
log.Sugar.Errorf("没有一路track, 删除source: %s", s.ID)
|
||||
// 异步执行ProbeTimeout函数中还没释放锁
|
||||
go s.DoClose()
|
||||
// 此时还持有stream lock, 异步关闭source
|
||||
go CloseSource(s.ID)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -318,6 +325,8 @@ func (s *PublishSource) OnTrackNotFind() {
|
||||
}
|
||||
|
||||
log.Sugar.Errorf("no tracks found. source id: %s", s.ID)
|
||||
// 异步关闭source
|
||||
go CloseSource(s.ID)
|
||||
}
|
||||
|
||||
func (s *PublishSource) OnPacket(packet *avformat.AVPacket) {
|
||||
@@ -391,7 +400,7 @@ func (s *PublishSource) SetUrlValues(values url.Values) {
|
||||
s.urlValues = values
|
||||
}
|
||||
|
||||
func (s *PublishSource) ExecuteSyncEvent(cb func()) {
|
||||
func (s *PublishSource) ExecuteWithStreamLock(cb func()) {
|
||||
// 无竞争情况下, 接近原子操作
|
||||
s.streamLock.Lock()
|
||||
defer s.streamLock.Unlock()
|
||||
@@ -412,7 +421,7 @@ func (s *PublishSource) GetBitrateStatistics() *BitrateStatistics {
|
||||
|
||||
func (s *PublishSource) ProbeTimeout() {
|
||||
if s.TransDemuxer != nil {
|
||||
s.ExecuteSyncEvent(func() {
|
||||
s.ExecuteWithStreamLock(func() {
|
||||
if !s.closed.Load() {
|
||||
s.TransDemuxer.ProbeComplete()
|
||||
}
|
||||
@@ -446,3 +455,9 @@ func (s *PublishSource) StartTimers(source Source) {
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func (s *PublishSource) ExecuteWithDeleteLock(cb func()) {
|
||||
s.deleteLock.Lock()
|
||||
defer s.deleteLock.Unlock()
|
||||
cb()
|
||||
}
|
||||
|
@@ -16,7 +16,7 @@ type sourceManger struct {
|
||||
m sync.Map
|
||||
}
|
||||
|
||||
func (s *sourceManger) Add(source Source) error {
|
||||
func (s *sourceManger) add(source Source) error {
|
||||
_, ok := s.m.LoadOrStore(source.GetID(), source)
|
||||
if ok {
|
||||
return fmt.Errorf("the source %s has been exist", source.GetID())
|
||||
|
@@ -195,8 +195,15 @@ func StartIdleTimer(source Source) *time.Timer {
|
||||
return idleTimer
|
||||
}
|
||||
|
||||
func CloseSource(id string) {
|
||||
source := SourceManager.Find(id)
|
||||
if source != nil {
|
||||
source.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// LoopEvent 循环读取事件
|
||||
func LoopEvent(source Source) {
|
||||
source.StartTimers(source)
|
||||
go source.GetTransStreamPublisher().run()
|
||||
source.GetTransStreamPublisher().start()
|
||||
}
|
||||
|
@@ -18,20 +18,21 @@ func init() {
|
||||
// 如果重新推流之前,陆续有拉流端断开,直至sink计数为0,删除保存的推流信息。
|
||||
type StreamEndInfo struct {
|
||||
ID string
|
||||
Timestamps map[utils.AVCodecID][2]int64 // 每路track结束时间戳
|
||||
M3U8Writer M3U8Writer // 保存M3U8生成器
|
||||
PlaylistFormat *string // M3U8播放列表
|
||||
RtspTracks map[int]uint16 // rtsp每路track的结束序号
|
||||
FLVPrevTagSize uint32 // flv的最后一个tag大小, 下次生成flv时作为prev tag size
|
||||
Timestamps map[TransStreamID]map[utils.AVCodecID][2]int64 // 每个传输流的结束时间戳
|
||||
OriginTracks map[utils.AVCodecID]interface{} // 原始推流track
|
||||
M3U8Writer M3U8Writer // 保存M3U8生成器
|
||||
PlaylistFormat *string // M3U8播放列表
|
||||
RtspTracks map[utils.AVCodecID]uint16 // rtsp每路track的结束序号
|
||||
FLVPrevTagSize uint32 // flv的最后一个tag大小, 下次生成flv时作为prev tag size
|
||||
}
|
||||
|
||||
func EqualsTracks(info *StreamEndInfo, tracks []*Track) bool {
|
||||
//if len(info.Timestamps) != len(tracks) {
|
||||
// return false
|
||||
//}
|
||||
if len(info.OriginTracks) != len(tracks) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, track := range tracks {
|
||||
if _, ok := info.Timestamps[track.Stream.CodecID]; !ok {
|
||||
if _, ok := info.OriginTracks[track.Stream.CodecID]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/lkmio/lkm/log"
|
||||
"github.com/lkmio/lkm/transcode"
|
||||
"github.com/lkmio/transport"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -31,7 +32,7 @@ type StreamEvent struct {
|
||||
type TransStreamPublisher interface {
|
||||
Post(event *StreamEvent)
|
||||
|
||||
run()
|
||||
start()
|
||||
|
||||
close()
|
||||
|
||||
@@ -66,21 +67,36 @@ type TransStreamPublisher interface {
|
||||
ExecuteSyncEvent(cb func())
|
||||
|
||||
SetSourceID(id string)
|
||||
|
||||
// StartRecord 开启录制
|
||||
// 如果AppConfig已经开启了全局录制, 则无需手动开启, 返回false
|
||||
StartRecord() bool
|
||||
|
||||
// StopRecord 停止录制
|
||||
// 如果AppConfig已经开启了全局录制, 返回error
|
||||
StopRecord() error
|
||||
|
||||
RecordStartTime() time.Time
|
||||
|
||||
GetRecordStreamPlayUrl() string
|
||||
}
|
||||
|
||||
type transStreamPublisher struct {
|
||||
source string
|
||||
streamEvents *NonBlockingChannel[*StreamEvent]
|
||||
mainContextEvents chan func()
|
||||
earlyEvents collections.LinkedList[func()] // 早于启动前的事件, 等待启动后执行
|
||||
|
||||
sinkCount int // 拉流计数
|
||||
gopBuffer GOPBuffer // GOP缓存, 音频和视频混合使用, 以视频关键帧为界, 缓存第二个视频关键帧时, 释放前一组gop
|
||||
|
||||
recordSink Sink // 每个Source的录制流
|
||||
recordFilePath string // 录制流文件路径
|
||||
hlsStream TransStream // HLS传输流
|
||||
originTracks TrackManager // 推流的原始track
|
||||
transcodeTracks map[utils.AVCodecID]*TranscodeTrack // 转码Track
|
||||
recordSink Sink // 每个Source的录制流
|
||||
recordFilePath string // 录制流文件路径
|
||||
recordStartTime time.Time // 开始录制时间
|
||||
hasManualRecording bool // 是否开启手动录像
|
||||
hlsStream TransStream // HLS传输流
|
||||
originTracks TrackManager // 推流的原始track
|
||||
transcodeTracks map[utils.AVCodecID]*TranscodeTrack // 转码Track
|
||||
|
||||
transStreams map[TransStreamID]TransStream // 所有输出流
|
||||
forwardTransStream TransStream // 转发流
|
||||
@@ -90,11 +106,11 @@ type transStreamPublisher struct {
|
||||
hasVideo bool // 是否存在视频
|
||||
completed atomic.Bool // 推流track是否解析完毕
|
||||
closed atomic.Bool
|
||||
streamEndInfo *StreamEndInfo // 之前推流源信息
|
||||
accumulateTimestamps bool // 是否累加时间戳
|
||||
timestampModeDecided bool // 是否已经决定使用推流的时间戳,或者累加时间戳
|
||||
streamEndInfo *StreamEndInfo // 上次结束推流的信息
|
||||
lastStreamEndTime time.Time // 最近结束拉流的时间
|
||||
bitstreamFilterBuffer *collections.RBBlockBuffer // annexb和avcc转换的缓冲区
|
||||
mute sync.Mutex
|
||||
started bool
|
||||
}
|
||||
|
||||
func (t *transStreamPublisher) Post(event *StreamEvent) {
|
||||
@@ -102,14 +118,6 @@ func (t *transStreamPublisher) Post(event *StreamEvent) {
|
||||
}
|
||||
|
||||
func (t *transStreamPublisher) run() {
|
||||
t.streamEvents = NewNonBlockingChannel[*StreamEvent](256)
|
||||
t.mainContextEvents = make(chan func(), 256)
|
||||
|
||||
t.transStreams = make(map[TransStreamID]TransStream, 10)
|
||||
t.sinks = make(map[SinkID]Sink, 128)
|
||||
t.transStreamSinks = make(map[TransStreamID]map[SinkID]Sink, len(transStreamFactories)+1)
|
||||
t.transcodeTracks = make(map[utils.AVCodecID]*TranscodeTrack, 4)
|
||||
|
||||
defer func() {
|
||||
// 清空管道
|
||||
for event := t.streamEvents.Pop(); event != nil; event = t.streamEvents.Pop() {
|
||||
@@ -151,8 +159,39 @@ func (t *transStreamPublisher) run() {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *transStreamPublisher) start() {
|
||||
t.mute.Lock()
|
||||
defer t.mute.Unlock()
|
||||
|
||||
t.streamEvents = NewNonBlockingChannel[*StreamEvent](256)
|
||||
t.mainContextEvents = make(chan func(), 256)
|
||||
|
||||
t.transStreams = make(map[TransStreamID]TransStream, 10)
|
||||
t.sinks = make(map[SinkID]Sink, 128)
|
||||
t.transStreamSinks = make(map[TransStreamID]map[SinkID]Sink, len(transStreamFactories)+1)
|
||||
t.transcodeTracks = make(map[utils.AVCodecID]*TranscodeTrack, 4)
|
||||
|
||||
go t.run()
|
||||
t.started = true
|
||||
|
||||
// 放置先于启动的事件到主管道
|
||||
for t.earlyEvents.Size() > 0 {
|
||||
t.mainContextEvents <- t.earlyEvents.Remove(0)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *transStreamPublisher) PostEvent(cb func()) {
|
||||
t.mainContextEvents <- cb
|
||||
if t.started {
|
||||
t.mainContextEvents <- cb
|
||||
return
|
||||
}
|
||||
|
||||
// 早于启动前的事件, 添加到等待队列
|
||||
t.mute.Lock()
|
||||
defer t.mute.Unlock()
|
||||
if !t.started {
|
||||
t.earlyEvents.Add(cb)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *transStreamPublisher) ExecuteSyncEvent(cb func()) {
|
||||
@@ -167,20 +206,27 @@ func (t *transStreamPublisher) ExecuteSyncEvent(cb func()) {
|
||||
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() {
|
||||
if t.transStreams == nil {
|
||||
t.transStreams = make(map[TransStreamID]TransStream, 10)
|
||||
}
|
||||
|
||||
// 创建录制流
|
||||
if AppConfig.Record.Enable {
|
||||
sink, path, err := CreateRecordStream(t.source)
|
||||
if err != nil {
|
||||
log.Sugar.Errorf("创建录制sink失败 source: %s err: %s", t.source, err.Error())
|
||||
} else {
|
||||
t.recordSink = sink
|
||||
t.recordFilePath = path
|
||||
}
|
||||
if AppConfig.Record.Enable || t.hasManualRecording {
|
||||
t.createRecordSink()
|
||||
}
|
||||
|
||||
// 创建HLS输出流
|
||||
@@ -215,7 +261,7 @@ func (t *transStreamPublisher) CreateTransStream(protocol TransStreamProtocol, t
|
||||
// 匹配和创建适合TransStream流协议的track
|
||||
var finalTracks []*Track
|
||||
for _, track := range tracks {
|
||||
// 对应传输流支持的编码器列表
|
||||
// 传输流支持的编码器列表
|
||||
supportedCodecs, ok := SupportedCodes[protocol]
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("unknown protocol %s", protocol.String()))
|
||||
@@ -240,7 +286,7 @@ func (t *transStreamPublisher) CreateTransStream(protocol TransStreamProtocol, t
|
||||
|
||||
if !ok {
|
||||
log.Sugar.Warnf("不支持的编码器 source: %s stream: %s codec: %s", t.source, protocol.String(), track.Stream.CodecID)
|
||||
// 尝试音频转码
|
||||
// 如果没有开启音频转码或非音频流,跳过
|
||||
if utils.AVMediaTypeAudio != track.Stream.MediaType || transcode.CreateAudioTranscoder == nil {
|
||||
continue
|
||||
}
|
||||
@@ -272,19 +318,10 @@ func (t *transStreamPublisher) CreateTransStream(protocol TransStreamProtocol, t
|
||||
stream.Index = len(t.originTracks.tracks) + len(t.transcodeTracks)
|
||||
newTrack := &Track{Stream: stream}
|
||||
|
||||
// 如果之前有转码过, 则使用之前的时间戳
|
||||
if t.streamEndInfo != nil {
|
||||
oldTimestamps, ok := t.streamEndInfo.Timestamps[transcoder.GetEncoderID()]
|
||||
if ok {
|
||||
newTrack.Dts = oldTimestamps[0]
|
||||
newTrack.Pts = oldTimestamps[1]
|
||||
}
|
||||
}
|
||||
|
||||
transcodeTrack = NewTranscodeTrack(newTrack, transcoder)
|
||||
t.transcodeTracks[transcoder.GetEncoderID()] = transcodeTrack
|
||||
|
||||
// 转码GOPBuffer中的音频
|
||||
// 转码GOP中的推流音频
|
||||
t.transcodeGOPBuffer(transcodeTrack)
|
||||
} else {
|
||||
log.Sugar.Infof("使用已经存在的音频转码track source: %s stream: %s src: %s dst: %s", t.source, protocol.String(), track.Stream.CodecID, transcodeTrack.transcoder.GetEncoderID())
|
||||
@@ -293,9 +330,11 @@ func (t *transStreamPublisher) CreateTransStream(protocol TransStreamProtocol, t
|
||||
track = transcodeTrack.track
|
||||
}
|
||||
|
||||
// 重新拷贝一个track,传输流内部使用track的时间戳,
|
||||
newTrack := *track
|
||||
finalTracks = append(finalTracks, &newTrack)
|
||||
// 创建新的track
|
||||
newTrack := &Track{
|
||||
Stream: track.Stream,
|
||||
}
|
||||
finalTracks = append(finalTracks, newTrack)
|
||||
}
|
||||
|
||||
if len(finalTracks) < 1 {
|
||||
@@ -337,6 +376,22 @@ func (t *transStreamPublisher) CreateTransStream(protocol TransStreamProtocol, t
|
||||
transStream.SetID(id)
|
||||
transStream.SetProtocol(protocol)
|
||||
|
||||
// 使用上次推流结束的时间戳
|
||||
if t.streamEndInfo != nil {
|
||||
oldTimestamps, ok := t.streamEndInfo.Timestamps[id]
|
||||
if ok {
|
||||
for _, track := range transStream.GetTracks() {
|
||||
track.Dts = oldTimestamps[track.Stream.CodecID][0]
|
||||
track.Pts = oldTimestamps[track.Stream.CodecID][1]
|
||||
|
||||
log.Sugar.Debugf("使用上次结束推流的时间戳 source: %s stream: %s track: %s dts: %d pts: %d", t.source, protocol, track.Stream.CodecID, track.Dts, track.Pts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试清空等待释放的合并写缓冲区
|
||||
ReleasePendingBuffers(t.source, id)
|
||||
|
||||
t.transStreams[id] = transStream
|
||||
// 创建输出流对应的拉流队列
|
||||
t.transStreamSinks[id] = make(map[SinkID]Sink, 128)
|
||||
@@ -345,6 +400,9 @@ func (t *transStreamPublisher) CreateTransStream(protocol TransStreamProtocol, t
|
||||
// 设置转发流
|
||||
if TransStreamGBCascaded == transStream.GetProtocol() {
|
||||
t.forwardTransStream = transStream
|
||||
} else if AppConfig.GOPCache && t.hasVideo {
|
||||
// 新建传输流,发送GOP缓存
|
||||
t.DispatchGOPBuffer(transStream)
|
||||
}
|
||||
|
||||
return transStream, nil
|
||||
@@ -396,13 +454,13 @@ func (t *transStreamPublisher) DispatchPacketToStream(transStream TransStream, p
|
||||
// DispatchBuffer 分发传输流
|
||||
func (t *transStreamPublisher) DispatchBuffer(transStream TransStream, index int, data []*collections.ReferenceCounter[[]byte], timestamp int64, keyVideo bool) {
|
||||
sinks := t.transStreamSinks[transStream.GetID()]
|
||||
exist := transStream.HasVideo()
|
||||
hasVideo := transStream.HasVideo()
|
||||
|
||||
for _, sink := range sinks {
|
||||
|
||||
if sink.GetSentPacketCount() < 1 {
|
||||
// 如果存在视频, 确保向sink发送的第一帧是关键帧
|
||||
if exist && !keyVideo {
|
||||
if hasVideo && !keyVideo {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -419,6 +477,12 @@ func (t *transStreamPublisher) DispatchBuffer(transStream TransStream, index int
|
||||
}
|
||||
}
|
||||
|
||||
func (t *transStreamPublisher) DispatchSegments(transStream TransStream, segments []TransStreamSegment) {
|
||||
for _, segment := range segments {
|
||||
t.DispatchBuffer(transStream, segment.Index, segment.Data, segment.TS, segment.Key)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *transStreamPublisher) pendingSink(sink Sink) {
|
||||
log.Sugar.Errorf("向sink推流超时,关闭连接. %s-sink: %s source: %s", sink.GetProtocol().String(), sink.GetID(), t.source)
|
||||
go sink.Close()
|
||||
@@ -441,6 +505,12 @@ func (t *transStreamPublisher) write(sink Sink, index int, data []*collections.R
|
||||
return false
|
||||
}
|
||||
|
||||
func (t *transStreamPublisher) writeSegments(sink Sink, segments []TransStreamSegment) {
|
||||
for _, segment := range segments {
|
||||
t.write(sink, segment.Index, segment.Data, segment.TS, segment.Key)
|
||||
}
|
||||
}
|
||||
|
||||
// 创建sink需要的输出流
|
||||
func (t *transStreamPublisher) doAddSink(sink Sink, resume bool) bool {
|
||||
// 暂时不考虑多路视频流,意味着只能1路视频流和多路音频流,同理originStreams和allStreams里面的Stream互斥. 同时多路音频流的Codec必须一致
|
||||
@@ -550,18 +620,13 @@ func (t *transStreamPublisher) doAddSink(sink Sink, resume bool) bool {
|
||||
}
|
||||
|
||||
// 发送已缓存的合并写切片
|
||||
keyBuffer, timestamp, _ := transStream.ReadKeyFrameBuffer()
|
||||
if len(keyBuffer) > 0 {
|
||||
if extraData, _, _ := transStream.ReadExtraData(timestamp); len(extraData) > 0 {
|
||||
t.write(sink, 0, extraData, timestamp, false)
|
||||
segments, _ := transStream.ReadKeyFrameBuffer()
|
||||
if len(segments) > 0 {
|
||||
if extraData, _, _ := transStream.ReadExtraData(0); len(extraData) > 0 {
|
||||
t.write(sink, 0, extraData, 0, false)
|
||||
}
|
||||
|
||||
t.write(sink, 0, keyBuffer, timestamp, true)
|
||||
}
|
||||
|
||||
// 新建传输流,发送已经缓存的音视频帧
|
||||
if !exist && AppConfig.GOPCache && t.hasVideo && TransStreamGBCascaded != transStream.GetProtocol() {
|
||||
t.DispatchGOPBuffer(transStream)
|
||||
t.writeSegments(sink, segments)
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -611,12 +676,12 @@ func (t *transStreamPublisher) clearSinkStreaming(sink Sink) {
|
||||
delete(transStreamSinks, sink.GetID())
|
||||
t.lastStreamEndTime = time.Now()
|
||||
sink.StopStreaming(t.transStreams[sink.GetTransStreamID()])
|
||||
delete(t.sinks, sink.GetID())
|
||||
}
|
||||
|
||||
func (t *transStreamPublisher) doRemoveSink(sink Sink) bool {
|
||||
if _, ok := t.sinks[sink.GetID()]; ok {
|
||||
t.clearSinkStreaming(sink)
|
||||
delete(t.sinks, sink.GetID())
|
||||
|
||||
t.sinkCount--
|
||||
log.Sugar.Infof("sink count: %d source: %s", t.sinkCount, t.source)
|
||||
@@ -663,16 +728,16 @@ func (t *transStreamPublisher) doClose() {
|
||||
tracks = append(tracks, track.track)
|
||||
}
|
||||
|
||||
sourceHistory := StreamEndInfoBride(t.source, tracks, t.transStreams)
|
||||
sourceHistory := StreamEndInfoBride(t.source, t.originTracks.All(), t.transStreams)
|
||||
streamEndInfoManager.Add(sourceHistory)
|
||||
}
|
||||
|
||||
// 关闭所有输出流
|
||||
for _, transStream := range t.transStreams {
|
||||
// 发送剩余包
|
||||
data, ts, _ := transStream.Close()
|
||||
if len(data) > 0 {
|
||||
t.DispatchBuffer(transStream, -1, data, ts, true)
|
||||
segments, _ := transStream.Close()
|
||||
if len(segments) > 0 {
|
||||
t.DispatchSegments(transStream, segments)
|
||||
}
|
||||
|
||||
// 如果是tcp传输流, 归还合并写缓冲区
|
||||
@@ -686,7 +751,6 @@ func (t *transStreamPublisher) doClose() {
|
||||
// 将所有sink添加到等待队列
|
||||
for _, sink := range t.sinks {
|
||||
transStreamID := sink.GetTransStreamID()
|
||||
sink.SetTransStreamID(0)
|
||||
if t.recordSink == sink {
|
||||
continue
|
||||
}
|
||||
@@ -720,20 +784,6 @@ func (t *transStreamPublisher) WriteHeader() {
|
||||
// 尝试使用上次结束推流的时间戳
|
||||
if streamInfo := streamEndInfoManager.Remove(t.source); streamInfo != nil && EqualsTracks(streamInfo, t.originTracks.All()) {
|
||||
t.streamEndInfo = streamInfo
|
||||
|
||||
// 恢复每路track的时间戳
|
||||
for _, track := range t.originTracks.All() {
|
||||
timestamps := streamInfo.Timestamps[track.Stream.CodecID]
|
||||
track.Dts = timestamps[0]
|
||||
track.Pts = timestamps[1]
|
||||
}
|
||||
}
|
||||
|
||||
// 纠正GOP中的时间戳
|
||||
if t.gopBuffer != nil && t.gopBuffer.Size() != 0 {
|
||||
t.gopBuffer.PeekAll(func(packet *collections.ReferenceCounter[*avformat.AVPacket]) {
|
||||
t.CorrectTimestamp(packet.Get())
|
||||
})
|
||||
}
|
||||
|
||||
// 创建录制流和HLS
|
||||
@@ -776,13 +826,13 @@ func (t *transStreamPublisher) ClearGopBuffer(free bool) {
|
||||
if packet.Release() && free {
|
||||
avformat.FreePacket(packet.Get())
|
||||
}
|
||||
|
||||
// 释放annexb和avcc格式转换的缓存
|
||||
if t.bitstreamFilterBuffer != nil {
|
||||
t.bitstreamFilterBuffer.Pop()
|
||||
}
|
||||
})
|
||||
|
||||
// 释放annexb和avcc格式转换的缓存
|
||||
if t.bitstreamFilterBuffer != nil {
|
||||
t.bitstreamFilterBuffer.Clear()
|
||||
}
|
||||
|
||||
// 丢弃转码track中的缓存
|
||||
for _, track := range t.transcodeTracks {
|
||||
track.Clear()
|
||||
@@ -810,8 +860,6 @@ func (t *transStreamPublisher) OnPacket(packet *collections.ReferenceCounter[*av
|
||||
|
||||
// track解析完毕后,才能生成传输流
|
||||
if t.completed.Load() {
|
||||
t.CorrectTimestamp(packet.Get())
|
||||
|
||||
// 分发给各个传输流
|
||||
t.DispatchPacket(packet.Get())
|
||||
|
||||
@@ -819,6 +867,7 @@ func (t *transStreamPublisher) OnPacket(packet *collections.ReferenceCounter[*av
|
||||
for _, track := range t.transcodeTracks {
|
||||
transcodePackets := track.Input(packet.Get())
|
||||
for _, transcodePkt := range transcodePackets {
|
||||
//log.Sugar.Infof("packet dts: %d, pts: %d, t dts: %d, pts: %d", packet.Get().Dts, packet.Get().Pts, transcodePkt.Dts, transcodePkt.Pts)
|
||||
t.DispatchPacket(transcodePkt)
|
||||
}
|
||||
}
|
||||
@@ -844,35 +893,6 @@ func (t *transStreamPublisher) OnNewTrack(track *Track) {
|
||||
}
|
||||
}
|
||||
|
||||
// CorrectTimestamp 纠正时间戳
|
||||
func (t *transStreamPublisher) CorrectTimestamp(packet *avformat.AVPacket) {
|
||||
// 对比第一包的时间戳和上次推流的最后时间戳。如果小于上次的推流时间戳,则在原来的基础上累加。
|
||||
if t.streamEndInfo != nil && !t.timestampModeDecided {
|
||||
t.timestampModeDecided = true
|
||||
|
||||
timestamps := t.streamEndInfo.Timestamps[packet.CodecID]
|
||||
t.accumulateTimestamps = true
|
||||
log.Sugar.Infof("使用上次推流的时间戳 dts: %d, pts: %d", timestamps[0], timestamps[1])
|
||||
}
|
||||
|
||||
track := t.originTracks.Find(packet.CodecID)
|
||||
if track == nil {
|
||||
return
|
||||
}
|
||||
duration := packet.GetDuration(packet.Timebase)
|
||||
|
||||
// 根据duration来累加时间戳
|
||||
if t.accumulateTimestamps {
|
||||
offset := packet.Pts - packet.Dts
|
||||
packet.Dts = track.Dts + duration
|
||||
packet.Pts = packet.Dts + offset
|
||||
}
|
||||
|
||||
track.Dts = packet.Dts
|
||||
track.Pts = packet.Pts
|
||||
track.FrameDuration = int(duration)
|
||||
}
|
||||
|
||||
func (t *transStreamPublisher) GetTransStreams() map[TransStreamID]TransStream {
|
||||
return t.transStreams
|
||||
}
|
||||
@@ -901,6 +921,54 @@ func (t *transStreamPublisher) SetSourceID(id string) {
|
||||
t.source = id
|
||||
}
|
||||
|
||||
func (t *transStreamPublisher) StartRecord() bool {
|
||||
if AppConfig.Record.Enable || t.recordSink != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var ok bool
|
||||
t.ExecuteSyncEvent(func() {
|
||||
t.hasManualRecording = true
|
||||
// 如果探测还未结束
|
||||
if !t.completed.Load() {
|
||||
return
|
||||
}
|
||||
|
||||
if t.recordSink == nil && t.createRecordSink() {
|
||||
ok = t.doAddSink(t.recordSink, false)
|
||||
}
|
||||
})
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
func (t *transStreamPublisher) StopRecord() error {
|
||||
if AppConfig.Record.Enable {
|
||||
return fmt.Errorf("录制常开")
|
||||
}
|
||||
|
||||
t.ExecuteSyncEvent(func() {
|
||||
t.hasManualRecording = false
|
||||
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 (t *transStreamPublisher) GetRecordStreamPlayUrl() string {
|
||||
return GenerateRecordStreamPlayUrl(filepath.ToSlash(t.recordFilePath))
|
||||
}
|
||||
|
||||
func NewTransStreamPublisher(source string) TransStreamPublisher {
|
||||
return &transStreamPublisher{
|
||||
transStreams: make(map[TransStreamID]TransStream),
|
||||
|
@@ -18,8 +18,10 @@ type StreamServer[T any] struct {
|
||||
}
|
||||
|
||||
func (s *StreamServer[T]) OnConnected(conn net.Conn) []byte {
|
||||
log.Sugar.Debugf("%s连接 conn:%s", s.SourceType.String(), conn.RemoteAddr().String())
|
||||
conn.(*transport.Conn).Data = s.Handler.OnNewSession(conn)
|
||||
log.Sugar.Debugf("%s连接 conn: %s", s.SourceType.String(), conn.RemoteAddr().String())
|
||||
if s.Handler != nil {
|
||||
conn.(*transport.Conn).Data = s.Handler.OnNewSession(conn)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -35,7 +37,7 @@ func (s *StreamServer[T]) OnDisConnected(conn net.Conn, err error) {
|
||||
log.Sugar.Debugf("%s断开连接 conn:%s", s.SourceType.String(), conn.RemoteAddr().String())
|
||||
|
||||
t := conn.(*transport.Conn)
|
||||
if t.Data != nil {
|
||||
if s.Handler != nil && t.Data != nil {
|
||||
s.Handler.OnCloseSession(t.Data.(T))
|
||||
t.Data = nil
|
||||
}
|
||||
|
@@ -43,10 +43,10 @@ type TransStream interface {
|
||||
ReadExtraData(timestamp int64) ([]*collections.ReferenceCounter[[]byte], int64, error)
|
||||
|
||||
// ReadKeyFrameBuffer 读取最近的包含视频关键帧的合并写队列
|
||||
ReadKeyFrameBuffer() ([]*collections.ReferenceCounter[[]byte], int64, error)
|
||||
ReadKeyFrameBuffer() ([]TransStreamSegment, error)
|
||||
|
||||
// Close 关闭传输流, 返回还未flush的合并写块
|
||||
Close() ([]*collections.ReferenceCounter[[]byte], int64, error)
|
||||
Close() ([]TransStreamSegment, error)
|
||||
|
||||
HasVideo() bool
|
||||
|
||||
@@ -55,6 +55,13 @@ type TransStream interface {
|
||||
GetMWBuffer() MergeWritingBuffer
|
||||
}
|
||||
|
||||
type TransStreamSegment struct {
|
||||
Data []*collections.ReferenceCounter[[]byte]
|
||||
TS int64
|
||||
Key bool
|
||||
Index int
|
||||
}
|
||||
|
||||
type BaseTransStream struct {
|
||||
ID TransStreamID
|
||||
Tracks []*Track
|
||||
@@ -119,8 +126,8 @@ func (t *BaseTransStream) FindTrackWithStreamIndex(streamIndex int) *Track {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *BaseTransStream) Close() ([]*collections.ReferenceCounter[[]byte], int64, error) {
|
||||
return nil, 0, nil
|
||||
func (t *BaseTransStream) Close() ([]TransStreamSegment, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (t *BaseTransStream) GetProtocol() TransStreamProtocol {
|
||||
@@ -167,8 +174,8 @@ func (t *BaseTransStream) ReadExtraData(timestamp int64) ([]*collections.Referen
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func (t *BaseTransStream) ReadKeyFrameBuffer() ([]*collections.ReferenceCounter[[]byte], int64, error) {
|
||||
return nil, 0, nil
|
||||
func (t *BaseTransStream) ReadKeyFrameBuffer() ([]TransStreamSegment, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (t *BaseTransStream) IsTCPStreaming() bool {
|
||||
|
@@ -2,14 +2,13 @@ package stream
|
||||
|
||||
import "github.com/lkmio/avformat/utils"
|
||||
|
||||
// TransStreamID 每个传输流的唯一Id,根据输出流协议ID+track index生成
|
||||
// 输出流协议占低8位
|
||||
// 每个音视频编译器ID占用8位. 意味着每个输出流至多7路流.
|
||||
// TransStreamID 每个传输流的唯一Id, 根据输出流协议ID+track index生成
|
||||
// 输出流协议占低8位, track index占用8位, 最多支持7路流.
|
||||
type TransStreamID uint64
|
||||
|
||||
func (id TransStreamID) HasTrack(index int) bool {
|
||||
for i := 1; i < 8; i++ {
|
||||
if int(id>>(i*8))&0xFF == index {
|
||||
if (int(id>>(i*8))&0xFF)-1 == index {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -21,25 +20,6 @@ func (id TransStreamID) Protocol() TransStreamProtocol {
|
||||
return TransStreamProtocol(id & 0xFF)
|
||||
}
|
||||
|
||||
// GenerateTransStreamID 根据传入的推拉流协议和编码器ID生成StreamId
|
||||
// 请确保ids根据值升序排序传参
|
||||
/*func GenerateTransStreamID(protocol GetProtocol, ids ...utils.AVCodecID) GetTransStreamID {
|
||||
len_ := len(ids)
|
||||
utils.Assert(len_ > 0 && len_ < 8)
|
||||
|
||||
var streamId uint64
|
||||
streamId = uint64(protocol) << 56
|
||||
|
||||
for i, GetID := range ids {
|
||||
bId, ok := narrowCodecIds[int(GetID)]
|
||||
utils.Assert(ok)
|
||||
|
||||
streamId |= uint64(bId) << (48 - i*8)
|
||||
}
|
||||
|
||||
return GetTransStreamID(streamId)
|
||||
}*/
|
||||
|
||||
// GenerateTransStreamID 根据输出流协议和输出流包含的音视频编码器ID生成流ID
|
||||
func GenerateTransStreamID(protocol TransStreamProtocol, tracks ...*Track) TransStreamID {
|
||||
len_ := len(tracks)
|
||||
@@ -47,7 +27,8 @@ func GenerateTransStreamID(protocol TransStreamProtocol, tracks ...*Track) Trans
|
||||
|
||||
var streamId = uint64(protocol) & 0xFF
|
||||
for i, track := range tracks {
|
||||
streamId |= uint64(track.Stream.Index) << ((i + 1) * 8)
|
||||
// +1是为了避免0值
|
||||
streamId |= uint64(track.Stream.Index+1) << ((i + 1) * 8)
|
||||
}
|
||||
|
||||
return TransStreamID(streamId)
|
||||
|
Reference in New Issue
Block a user