mirror of
https://github.com/langhuihui/monibuca.git
synced 2025-09-27 01:15:52 +08:00
455 lines
13 KiB
Go
455 lines
13 KiB
Go
package plugin_gb28181pro
|
||
|
||
import (
|
||
"errors"
|
||
"fmt"
|
||
"math/rand"
|
||
"net/url"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
sipgo "github.com/emiago/sipgo"
|
||
"github.com/emiago/sipgo/sip"
|
||
m7s "m7s.live/v5"
|
||
pkg "m7s.live/v5/pkg"
|
||
"m7s.live/v5/pkg/task"
|
||
"m7s.live/v5/pkg/util"
|
||
gb28181 "m7s.live/v5/plugin/gb28181/pkg"
|
||
mrtp "m7s.live/v5/plugin/rtp/pkg"
|
||
)
|
||
|
||
// Plugin-specific progress steps for GB28181
|
||
const (
|
||
StepDeviceLookup pkg.StepName = "device_lookup"
|
||
StepSIPPrepare pkg.StepName = "sip_prepare"
|
||
StepSDPBuild pkg.StepName = "sdp_build"
|
||
StepInviteSend pkg.StepName = "invite_send"
|
||
StepResponseWait pkg.StepName = "response_wait"
|
||
)
|
||
|
||
var gbPullSteps = []pkg.StepDef{
|
||
{Name: pkg.StepPublish, Description: "Publishing stream"},
|
||
{Name: StepDeviceLookup, Description: "Looking up device and channel"},
|
||
{Name: StepSIPPrepare, Description: "Preparing SIP invitation"},
|
||
{Name: StepSDPBuild, Description: "Building SDP content"},
|
||
{Name: StepInviteSend, Description: "Sending SIP INVITE"},
|
||
{Name: StepResponseWait, Description: "Waiting for response"},
|
||
{Name: pkg.StepStreaming, Description: "Receiving media stream"},
|
||
}
|
||
|
||
type Dialog struct {
|
||
task.Task
|
||
Channel *Channel
|
||
gb28181.InviteOptions
|
||
gb *GB28181Plugin
|
||
session *sipgo.DialogClientSession
|
||
pullCtx m7s.PullJob
|
||
start string
|
||
end string
|
||
StreamMode mrtp.StreamMode // 数据流传输模式(UDP:udp传输/TCP-ACTIVE:tcp主动模式/TCP-PASSIVE:tcp被动模式)
|
||
targetIP string // 目标设备的IP地址
|
||
targetPort int // 目标设备的端口
|
||
/**
|
||
子码流的配置,默认格式为:
|
||
stream=stream:0;stream=stream:1
|
||
GB28181-2022:
|
||
stream=streanumber:0;stream=streamnumber:1
|
||
大华为:
|
||
stream=streamprofile:0;stream=streamprofile:1
|
||
水星,tp-link:
|
||
stream=streamMode:main;stream=streamMode:sub
|
||
*/
|
||
stream string
|
||
}
|
||
|
||
func (d *Dialog) GetCallID() string {
|
||
if d.session != nil && d.session.InviteRequest != nil && d.session.InviteRequest.CallID() != nil {
|
||
return d.session.InviteRequest.CallID().Value()
|
||
} else {
|
||
return ""
|
||
}
|
||
}
|
||
|
||
func (d *Dialog) GetPullJob() *m7s.PullJob {
|
||
return &d.pullCtx
|
||
}
|
||
|
||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||
|
||
var seededRand *rand.Rand = rand.New(rand.NewSource(time.Now().UnixNano()))
|
||
|
||
func GenerateCallID(length int) string {
|
||
b := make([]byte, length)
|
||
for i := range b {
|
||
b[i] = charset[seededRand.Intn(len(charset))]
|
||
}
|
||
return string(b)
|
||
}
|
||
|
||
func (d *Dialog) Start() (err error) {
|
||
// Initialize progress tracking for pull operations
|
||
d.pullCtx.SetProgressStepsDefs(gbPullSteps)
|
||
|
||
// 处理时间范围
|
||
d.InviteOptions.Start = d.start
|
||
d.InviteOptions.End = d.End
|
||
if d.IsLive() {
|
||
d.pullCtx.PublishConfig.PubType = m7s.PublishTypeVod
|
||
}
|
||
err = d.pullCtx.Publish()
|
||
if err != nil {
|
||
d.pullCtx.Fail(err.Error())
|
||
return
|
||
}
|
||
|
||
d.pullCtx.GoToStepConst(StepDeviceLookup)
|
||
|
||
sss := strings.Split(d.pullCtx.RemoteURL, "/")
|
||
if len(sss) < 2 {
|
||
d.Info("remote url is invalid", d.pullCtx.RemoteURL)
|
||
d.pullCtx.Fail("remote url is invalid")
|
||
return
|
||
}
|
||
deviceId, channelId := sss[len(sss)-2], sss[len(sss)-1]
|
||
var device *Device
|
||
if deviceTmp, ok := d.gb.devices.Get(deviceId); ok {
|
||
device = deviceTmp
|
||
d.StreamMode = device.StreamMode
|
||
if channel, ok := deviceTmp.channels.Get(deviceId + "_" + channelId); ok {
|
||
d.Channel = channel
|
||
} else if channel, ok := deviceTmp.channels.Find(func(c *Channel) bool {
|
||
return c.CustomChannelId == channelId
|
||
}); ok {
|
||
channelId = channel.ChannelId
|
||
d.Channel = channel
|
||
} else {
|
||
d.pullCtx.Fail(fmt.Sprintf("channel %s not found", channelId))
|
||
return fmt.Errorf("channel %s not found", channelId)
|
||
}
|
||
} else {
|
||
d.pullCtx.Fail(fmt.Sprintf("device %s not found", deviceId))
|
||
return fmt.Errorf("device %s not found", deviceId)
|
||
}
|
||
|
||
d.pullCtx.GoToStepConst(StepSIPPrepare)
|
||
|
||
//defer d.gb.dialogs.Remove(d)
|
||
switch d.StreamMode {
|
||
case mrtp.StreamModeTCPPassive:
|
||
if d.gb.tcpPort > 0 {
|
||
d.MediaPort = d.gb.tcpPort
|
||
} else {
|
||
if d.gb.MediaPort.Valid() {
|
||
select {
|
||
case d.MediaPort = <-d.gb.tcpPorts:
|
||
default:
|
||
d.pullCtx.Fail("no available tcp port")
|
||
return fmt.Errorf("no available tcp port")
|
||
}
|
||
} else {
|
||
d.MediaPort = d.gb.MediaPort[0]
|
||
}
|
||
}
|
||
case mrtp.StreamModeUDP:
|
||
if d.gb.udpPort > 0 {
|
||
d.MediaPort = d.gb.udpPort
|
||
} else {
|
||
if d.gb.MediaPort.Valid() {
|
||
select {
|
||
case d.MediaPort = <-d.gb.udpPorts:
|
||
default:
|
||
return fmt.Errorf("no available udp port")
|
||
}
|
||
} else {
|
||
d.MediaPort = d.gb.MediaPort[0]
|
||
}
|
||
}
|
||
}
|
||
|
||
d.pullCtx.GoToStepConst(StepSDPBuild)
|
||
|
||
ssrc := d.CreateSSRC(d.gb.Serial)
|
||
d.Info("MediaIp is ", device.MediaIp)
|
||
|
||
// 构建 SDP 内容
|
||
sdpInfo := []string{
|
||
"v=0",
|
||
fmt.Sprintf("o=%s 0 0 IN IP4 %s", channelId, device.SipIp),
|
||
fmt.Sprintf("s=%s", util.Conditional(d.IsLive(), "Play", "Playback")), // 根据是否有时间参数决定
|
||
}
|
||
|
||
// 非直播模式下添加u行,保持在s=和c=之间
|
||
if !d.IsLive() {
|
||
sdpInfo = append(sdpInfo, fmt.Sprintf("u=%s:0", channelId))
|
||
}
|
||
|
||
// 添加c行
|
||
sdpInfo = append(sdpInfo, "c=IN IP4 "+device.MediaIp)
|
||
|
||
// 将字符串时间转换为 Unix 时间戳
|
||
if !d.IsLive() {
|
||
startTime, endTime, err := util.TimeRangeQueryParse(url.Values{"start": []string{d.start}, "end": []string{d.end}})
|
||
if err != nil {
|
||
return errors.New("parse end time error")
|
||
}
|
||
sdpInfo = append(sdpInfo, fmt.Sprintf("t=%d %d", startTime.Unix(), endTime.Unix()))
|
||
} else {
|
||
sdpInfo = append(sdpInfo, "t=0 0")
|
||
}
|
||
|
||
// 添加媒体行和相关属性
|
||
var mediaLine string
|
||
switch device.StreamMode {
|
||
case mrtp.StreamModeTCPPassive, mrtp.StreamModeTCPActive:
|
||
mediaLine = fmt.Sprintf("m=video %d TCP/RTP/AVP 96", d.MediaPort)
|
||
case mrtp.StreamModeUDP:
|
||
mediaLine = fmt.Sprintf("m=video %d RTP/AVP 96", d.MediaPort)
|
||
default:
|
||
mediaLine = fmt.Sprintf("m=video %d TCP/RTP/AVP 96", d.MediaPort)
|
||
}
|
||
|
||
sdpInfo = append(sdpInfo, mediaLine)
|
||
|
||
sdpInfo = append(sdpInfo, "a=recvonly")
|
||
if d.stream != "" {
|
||
sdpInfo = append(sdpInfo, "a="+d.stream)
|
||
}
|
||
sdpInfo = append(sdpInfo, "a=rtpmap:96 PS/90000")
|
||
|
||
//根据传输模式添加 setup 和 connection 属性
|
||
switch device.StreamMode {
|
||
case mrtp.StreamModeTCPPassive:
|
||
sdpInfo = append(sdpInfo,
|
||
"a=setup:passive",
|
||
"a=connection:new",
|
||
)
|
||
case mrtp.StreamModeTCPActive:
|
||
sdpInfo = append(sdpInfo,
|
||
"a=setup:active",
|
||
"a=connection:new",
|
||
)
|
||
case mrtp.StreamModeUDP:
|
||
sdpInfo = append(sdpInfo,
|
||
"a=setup:active",
|
||
"a=connection:new",
|
||
)
|
||
default:
|
||
sdpInfo = append(sdpInfo,
|
||
"a=setup:passive",
|
||
"a=connection:new",
|
||
)
|
||
}
|
||
|
||
// 添加 SSRC
|
||
sdpInfo = append(sdpInfo, fmt.Sprintf("y=%s", ssrc))
|
||
|
||
// 创建 INVITE 请求
|
||
recipient := sip.Uri{
|
||
Host: device.IP,
|
||
Port: device.Port,
|
||
User: channelId,
|
||
}
|
||
// 设置必需的头部
|
||
contentTypeHeader := sip.ContentTypeHeader("APPLICATION/SDP")
|
||
subjectHeader := sip.NewHeader("Subject", fmt.Sprintf("%s:%s,%s:0", channelId, ssrc, d.gb.Serial))
|
||
//allowHeader := sip.NewHeader("Allow", "INVITE, ACK, CANCEL, REGISTER, MESSAGE, NOTIFY, BYE")
|
||
//Toheader里需要放入目录通道的id
|
||
toHeader := sip.ToHeader{
|
||
Address: sip.Uri{User: channelId, Host: channelId[0:10]},
|
||
}
|
||
userAgentHeader := sip.NewHeader("User-Agent", "M7S/"+m7s.Version)
|
||
|
||
//customCallID := fmt.Sprintf("%s-%s-%d@%s", device.DeviceId, channelId, time.Now().Unix(), device.SipIp)
|
||
customCallID := fmt.Sprintf("%s@%s", GenerateCallID(32), device.MediaIp)
|
||
callID := sip.CallIDHeader(customCallID)
|
||
viaHeader := sip.ViaHeader{
|
||
ProtocolName: "SIP",
|
||
ProtocolVersion: "2.0",
|
||
Transport: device.Transport,
|
||
Host: device.MediaIp,
|
||
Port: device.LocalPort,
|
||
Params: sip.NewParams(),
|
||
}
|
||
viaHeader.Params.Add("branch", sip.GenerateBranchN(10)).Add("rport", "")
|
||
maxforward := sip.MaxForwardsHeader(70)
|
||
//contentLengthHeader := sip.ContentLengthHeader(len(strings.Join(sdpInfo, "\r\n") + "\r\n"))
|
||
csqHeader := sip.CSeqHeader{
|
||
SeqNo: uint32(device.SN),
|
||
MethodName: "INVITE",
|
||
}
|
||
//request.AppendHeader(&contentLengthHeader)
|
||
contactHDR := sip.ContactHeader{
|
||
Address: sip.Uri{
|
||
User: d.gb.Serial,
|
||
Host: device.MediaIp,
|
||
Port: device.LocalPort,
|
||
},
|
||
}
|
||
|
||
fromHDR := sip.FromHeader{
|
||
Address: sip.Uri{
|
||
User: d.gb.Serial,
|
||
Host: device.MediaIp,
|
||
Port: device.LocalPort,
|
||
},
|
||
Params: sip.NewParams(),
|
||
}
|
||
fromHDR.Params.Add("tag", sip.GenerateTagN(32))
|
||
dialogClientCache := sipgo.NewDialogClientCache(device.client, contactHDR)
|
||
// 创建会话
|
||
d.Info("start to invite", "recipient:", recipient, " viaHeader:", viaHeader, " fromHDR:", fromHDR, " toHeader:", toHeader, " device.contactHDR:", device.contactHDR, "contactHDR:", contactHDR)
|
||
|
||
d.pullCtx.GoToStepConst(StepInviteSend)
|
||
|
||
// 判断当前系统类型
|
||
//if runtime.GOOS == "windows" {
|
||
// d.session, err = dialogClientCache.Invite(d.gb, recipient, []byte(strings.Join(sdpInfo, "\r\n")+"\r\n"), &callID, &csqHeader, &fromHDR, &toHeader, &maxforward, userAgentHeader, subjectHeader, &contentTypeHeader)
|
||
//} else {
|
||
if strings.ToLower(device.Transport) == "tcp" {
|
||
d.session, err = dialogClientCache.Invite(d.gb, recipient, []byte(strings.Join(sdpInfo, "\r\n")+"\r\n"), &viaHeader, &callID, &csqHeader, &fromHDR, &toHeader, &maxforward, userAgentHeader, subjectHeader, &contentTypeHeader)
|
||
} else {
|
||
d.session, err = dialogClientCache.Invite(d.gb, recipient, []byte(strings.Join(sdpInfo, "\r\n")+"\r\n"), &callID, &csqHeader, &fromHDR, &toHeader, &maxforward, userAgentHeader, subjectHeader, &contentTypeHeader)
|
||
}
|
||
//}
|
||
// 最后添加Content-Length头部
|
||
if err != nil {
|
||
d.pullCtx.Fail("dialog invite error: " + err.Error())
|
||
return errors.New("dialog invite error" + err.Error())
|
||
}
|
||
|
||
d.pullCtx.GoToStepConst(StepResponseWait)
|
||
return
|
||
}
|
||
|
||
func (d *Dialog) Run() (err error) {
|
||
d.Info("before WaitAnswer")
|
||
err = d.session.WaitAnswer(d.gb, sipgo.AnswerOptions{})
|
||
d.Info("after WaitAnswer")
|
||
if err != nil {
|
||
d.pullCtx.Fail("等待响应错误: " + err.Error())
|
||
return errors.New("wait answer error" + err.Error())
|
||
}
|
||
inviteResponseBody := string(d.session.InviteResponse.Body())
|
||
d.Info("inviteResponse", "body", inviteResponseBody)
|
||
ds := strings.Split(inviteResponseBody, "\r\n")
|
||
for _, l := range ds {
|
||
if ls := strings.Split(l, "="); len(ls) > 1 {
|
||
switch ls[0] {
|
||
case "y":
|
||
if len(ls[1]) > 0 {
|
||
if _ssrc, err := strconv.ParseInt(ls[1], 10, 0); err == nil {
|
||
d.SSRC = uint32(_ssrc)
|
||
} else {
|
||
d.pullCtx.Fail("解析邀请响应y字段错误: " + err.Error())
|
||
return errors.New("read invite respose y error" + err.Error())
|
||
}
|
||
}
|
||
case "c":
|
||
// 解析 c=IN IP4 xxx.xxx.xxx.xxx 格式
|
||
parts := strings.Split(ls[1], " ")
|
||
if len(parts) >= 3 {
|
||
d.targetIP = parts[len(parts)-1]
|
||
}
|
||
case "m":
|
||
// 解析 m=video port xxx 格式
|
||
parts := strings.Split(ls[1], " ")
|
||
if len(parts) >= 2 {
|
||
if port, err := strconv.Atoi(parts[1]); err == nil {
|
||
d.targetPort = port
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if d.session.InviteResponse.Contact() != nil {
|
||
if &d.session.InviteRequest.Recipient != &d.session.InviteResponse.Contact().Address {
|
||
d.session.InviteResponse.Contact().Address = d.session.InviteRequest.Recipient
|
||
}
|
||
}
|
||
|
||
// 移动到流数据接收步骤
|
||
d.pullCtx.GoToStepConst(pkg.StepStreaming)
|
||
|
||
var pub mrtp.PSReceiver
|
||
pub.Publisher = d.pullCtx.Publisher
|
||
switch d.StreamMode {
|
||
case mrtp.StreamModeTCPActive:
|
||
pub.ListenAddr = fmt.Sprintf("%s:%d", d.targetIP, d.targetPort)
|
||
case mrtp.StreamModeTCPPassive:
|
||
if d.gb.tcpPort > 0 {
|
||
d.Info("into single port mode,use gb.tcpPort", d.gb.tcpPort)
|
||
// 创建一个可取消的上下文
|
||
reader := &gb28181.SinglePortReader{
|
||
SSRC: d.SSRC,
|
||
Mouth: make(chan []byte, 1),
|
||
Context: d,
|
||
}
|
||
var loaded bool
|
||
reader, loaded = d.gb.singlePorts.LoadOrStore(reader)
|
||
if loaded {
|
||
reader.Context = d
|
||
}
|
||
pub.SinglePort = reader
|
||
d.OnStop(func() {
|
||
reader.Close()
|
||
d.gb.singlePorts.Remove(reader)
|
||
})
|
||
}
|
||
pub.ListenAddr = fmt.Sprintf(":%d", d.MediaPort)
|
||
case mrtp.StreamModeUDP:
|
||
if d.gb.udpPort > 0 {
|
||
d.Info("into single port mode, use gb.udpPort", d.gb.udpPort)
|
||
reader := &gb28181.SinglePortReader{
|
||
SSRC: d.SSRC,
|
||
Mouth: make(chan []byte, 100),
|
||
Context: d,
|
||
}
|
||
var loaded bool
|
||
reader, loaded = d.gb.singlePorts.LoadOrStore(reader)
|
||
if loaded {
|
||
reader.Context = d
|
||
}
|
||
pub.SinglePort = reader
|
||
d.OnStop(func() {
|
||
reader.Close()
|
||
d.gb.singlePorts.Remove(reader)
|
||
})
|
||
}
|
||
pub.ListenAddr = fmt.Sprintf(":%d", d.MediaPort)
|
||
}
|
||
pub.StreamMode = d.StreamMode
|
||
|
||
err = d.session.Ack(d.gb)
|
||
if err != nil {
|
||
d.Error("ack session err", err)
|
||
}
|
||
|
||
return d.RunTask(&pub)
|
||
}
|
||
|
||
func (d *Dialog) GetKey() string {
|
||
return d.GetCallID()
|
||
}
|
||
|
||
func (d *Dialog) Dispose() {
|
||
if d.StreamMode == mrtp.StreamModeUDP {
|
||
if d.gb.udpPort == 0 { //多端口
|
||
// 如果没有设置udp端口,则将MediaPort设置为0,表示不再使用
|
||
d.gb.udpPorts <- d.MediaPort
|
||
}
|
||
} else {
|
||
if d.gb.tcpPort == 0 {
|
||
// 如果没有设置tcp端口,则将MediaPort设置为0,表示不再使用
|
||
d.gb.tcpPorts <- d.MediaPort
|
||
}
|
||
}
|
||
d.Info("dialog dispose", "ssrc", d.SSRC, "mediaPort", d.MediaPort, "streamMode", d.StreamMode, "deviceId", d.Channel.DeviceId, "channelId", d.Channel.ChannelId)
|
||
if d.session != nil && d.session.InviteResponse != nil {
|
||
err := d.session.Bye(d)
|
||
if err != nil {
|
||
d.Error("dialog bye bye err", err)
|
||
}
|
||
}
|
||
}
|