Compare commits

...

32 Commits

Author SHA1 Message Date
ydajiang
6ff273c9e5 feat: 兼容国标UDP广播 2025-09-24 20:26:58 +08:00
ydajiang
fc5b8f5a1b feat: 适配livegbs一对一对讲接口 2025-09-24 18:20:28 +08:00
ydajiang
ef313b1ea4 feat: 支持国标倍速播放 2025-09-14 21:39:50 +08:00
ydajiang
a5b7fc6f24 feat: 支持国标录像下载 2025-09-14 19:35:39 +08:00
ydajiang
69308c466b feat: 支持开启和结束录制流 2025-09-13 15:45:16 +08:00
ydajiang
7150525c20 chore: 拉流hook携带sinkid 2025-09-06 09:43:47 +08:00
ydajiang
f526be83e9 fix: 级联发生崩溃问题; 2025-09-05 10:18:48 +08:00
ydajiang
c5851a0e01 feat: stream/info接口返回拉流端计数 2025-08-30 17:07:09 +08:00
ydajiang
6ce491445d feat: 适配livegbs stream/info接口 2025-08-29 15:37:15 +08:00
ydajiang
eeaeee14d5 feat: 适配livegbs前端拉流 2025-08-25 10:31:19 +08:00
ydajiang
0ce4f3d607 chore: 未探测到track, 关闭source 2025-08-25 10:30:05 +08:00
ydajiang
8339234df4 chore: update audio-transcoder module to v0.2.2 2025-08-09 11:25:44 +08:00
ydajiang
3f23747da6 Update README.md 2025-08-08 20:08:16 +08:00
ydajiang
42c38ad815 fix: rtc拉流等待时, 取消http请求未关闭sink问题 2025-08-08 19:45:36 +08:00
ydajiang
f63ae846c8 fix: http/ws-flv, rtc拉流sinkid冲突问题 2025-08-08 19:44:37 +08:00
ydajiang
1e730b8ef6 chore: 按照字符串打印sinkid 2025-08-08 19:43:29 +08:00
ydajiang
cf7041150a refactor: rtsp和gb28181不单独声明媒体端口, 强制udp和tcp全开 2025-08-08 17:59:55 +08:00
ydajiang
d7ad1dc725 chore: update module avformat to v0.0.2 2025-08-08 17:48:54 +08:00
ydajiang
ca52588bae refactor: gb28181仅支持多端口推流, 提升代码健壮性 2025-08-08 17:14:33 +08:00
ydajiang
cac5e91471 chore: update Go version to 1.20 in Dockerfile 2025-08-06 15:58:08 +08:00
ydajiang
b57a9de773 update module mepg to v0.0.3 2025-08-06 14:10:11 +08:00
ydajiang
7806098ad6 fix: 关闭推流失败的source时, 造成相同id的source被错误关闭问题; 2025-08-06 14:05:50 +08:00
ydajiang
791f75c54c Replace local module paths with remote versions 2025-07-28 09:53:58 +08:00
ydajiang
4870830a6c feat: rtc支持关键帧缓存 2025-07-27 15:24:00 +08:00
ydajiang
c6aba06199 feat: rtsp流支持关键帧缓存 2025-07-27 15:05:37 +08:00
ydajiang
77d18481c0 Update go version to 1.20 2025-07-26 23:29:09 +08:00
ydajiang
7fc147bc8a feat: rtc拉流支持h265 2025-07-26 14:58:38 +08:00
ydajiang
1e51835b6b 遗漏提交 2025-07-25 09:21:45 +08:00
ydajiang
525911fd9a fix: 使用上次结束时间戳造成崩溃问题 2025-07-24 20:14:51 +08:00
ydajiang
95925e2778 fix: 错误解析vlc rtsp tcp拉流发送的rtcp包, 造成拉流中断问题 2025-07-24 19:56:54 +08:00
ydajiang
22177deb15 fix: 错误计算rtcp时间戳, 造成vlc拉流卡顿问题 2025-07-24 19:24:01 +08:00
ydajiang
a2c372a367 feat: 传输流时间戳根据duration累加 2025-07-24 14:32:51 +08:00
57 changed files with 1572 additions and 1133 deletions

View File

@@ -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作为基础镜像

View File

@@ -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
View File

@@ -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
View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -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": {

View File

@@ -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切片

View File

@@ -1,10 +0,0 @@
package gb28181
// Filter 关联Source
type Filter interface {
AddSource(ssrc uint32, source GBSource) bool
RemoveSource(ssrc uint32)
FindSource(ssrc uint32) GBSource
}

View File

@@ -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}
}

View File

@@ -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)}
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
View File

@@ -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

View File

@@ -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()

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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
View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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)
})

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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,

View File

@@ -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{

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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())

View File

@@ -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) {

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)}
}

View File

@@ -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

View File

@@ -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 {

View File

@@ -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()
}

View File

@@ -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())

View File

@@ -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()
}

View File

@@ -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
}
}

View File

@@ -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),

View File

@@ -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
}

View File

@@ -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 {

View File

@@ -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)