mirror of
https://github.com/lkmio/lkm.git
synced 2025-09-27 03:26:01 +08:00
274 lines
6.9 KiB
Go
274 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"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"
|
|
)
|
|
|
|
const (
|
|
InviteTypePlay = "play"
|
|
InviteTypePlayback = "playback"
|
|
InviteTypeDownload = "download"
|
|
InviteTypeBroadcast = "broadcast"
|
|
InviteTypeTalk = "talk"
|
|
)
|
|
|
|
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
|
|
}
|
|
|
|
type SourceSDP struct {
|
|
Source string `json:"source"` // GetSourceID
|
|
SDP
|
|
}
|
|
|
|
type GBOffer struct {
|
|
SourceSDP
|
|
AnswerSetup string `json:"answer_setup,omitempty"` // 希望应答的连接方式
|
|
}
|
|
|
|
func (api *ApiServer) OnGBSourceCreate(v *SourceSDP, w http.ResponseWriter, r *http.Request) {
|
|
log.Sugar.Infof("创建国标源: %v", v)
|
|
|
|
// 返回收流地址
|
|
response := &struct {
|
|
SDP
|
|
Urls []string `json:"urls"`
|
|
}{}
|
|
|
|
var err error
|
|
// 响应错误消息
|
|
defer func() {
|
|
if err != nil {
|
|
log.Sugar.Errorf("创建国标源失败 err: %s", err.Error())
|
|
httpResponseError(w, err.Error())
|
|
}
|
|
}()
|
|
|
|
source := stream.SourceManager.Find(v.Source)
|
|
if source != nil {
|
|
err = fmt.Errorf("%s 源已经存在", v.Source)
|
|
return
|
|
}
|
|
|
|
tcp := true
|
|
var active bool
|
|
if v.Setup == "passive" {
|
|
} else if v.Setup == "active" {
|
|
active = true
|
|
} else {
|
|
tcp = false
|
|
//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 {
|
|
ssrc = gb28181.GetVodSSRC()
|
|
} else {
|
|
ssrc = gb28181.GetLiveSSRC()
|
|
}
|
|
|
|
ssrcValue, _ := strconv.Atoi(ssrc)
|
|
_, port, err := gb28181.NewGBSource(v.Source, uint32(ssrcValue), tcp, active)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
response.Addr = net.JoinHostPort(stream.AppConfig.PublicIP, strconv.Itoa(port))
|
|
response.Urls = stream.GetStreamPlayUrls(v.Source)
|
|
response.SSRC = ssrc
|
|
|
|
log.Sugar.Infof("创建国标源成功, addr: %s, ssrc: %d", response.Addr, ssrcValue)
|
|
httpResponseOK(w, response)
|
|
}
|
|
|
|
func (api *ApiServer) OnGBSourceConnect(v *SourceSDP, w http.ResponseWriter, r *http.Request) {
|
|
log.Sugar.Infof("设置国标主动拉流连接地址: %v", v)
|
|
|
|
var err error
|
|
// 响应错误消息
|
|
defer func() {
|
|
if err != nil {
|
|
log.Sugar.Errorf("设置国标主动拉流失败 err: %s", err.Error())
|
|
httpResponseError(w, err.Error())
|
|
}
|
|
}()
|
|
|
|
source := stream.SourceManager.Find(v.Source)
|
|
if source == nil {
|
|
err = fmt.Errorf("%s 源不存在", v.Source)
|
|
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)
|
|
}
|
|
}
|
|
|
|
func (api *ApiServer) OnGBOfferCreate(v *SourceSDP, w http.ResponseWriter, r *http.Request) {
|
|
// 预览下级设备
|
|
if v.SessionName == "" || v.SessionName == InviteTypePlay ||
|
|
v.SessionName == InviteTypePlayback ||
|
|
v.SessionName == InviteTypeDownload {
|
|
api.OnGBSourceCreate(v, w, r)
|
|
} else {
|
|
// 向上级转发广播和对讲, 或者是向设备发送invite talk
|
|
}
|
|
}
|
|
|
|
func (api *ApiServer) OnGBAnswerCreate(v *GBOffer, w http.ResponseWriter, r *http.Request) {
|
|
log.Sugar.Infof("创建应答 offer: %v", v)
|
|
|
|
var sink stream.Sink
|
|
var err error
|
|
// 响应错误消息
|
|
defer func() {
|
|
if err != nil {
|
|
log.Sugar.Errorf("创建应答失败 err: %s", err.Error())
|
|
httpResponseError(w, err.Error())
|
|
|
|
if sink != nil {
|
|
sink.Close()
|
|
}
|
|
}
|
|
}()
|
|
|
|
source := stream.SourceManager.Find(v.Source)
|
|
if source == nil {
|
|
err = fmt.Errorf("%s 源不存在", v.Source)
|
|
return
|
|
}
|
|
|
|
addr, _ := net.ResolveTCPAddr("tcp", r.RemoteAddr)
|
|
sinkId := stream.NetAddr2SinkId(addr)
|
|
|
|
// sinkId添加随机数
|
|
if ipv4, ok := sinkId.(uint64); ok {
|
|
random := uint64(utils.RandomIntInRange(0x1000, 0xFFFF0000))
|
|
sinkId = (ipv4 & 0xFFFFFFFF00000000) | (random << 16) | (ipv4 & 0xFFFF)
|
|
}
|
|
|
|
setup := gb28181.SetupTypeFromString(v.Setup)
|
|
if v.AnswerSetup != "" {
|
|
setup = gb28181.SetupTypeFromString(v.AnswerSetup)
|
|
}
|
|
|
|
var protocol stream.TransStreamProtocol
|
|
// 级联转发
|
|
if v.SessionName == "" || v.SessionName == InviteTypePlay ||
|
|
v.SessionName == InviteTypePlayback ||
|
|
v.SessionName == InviteTypeDownload {
|
|
protocol = stream.TransStreamGBCascadedForward
|
|
} else {
|
|
// 对讲广播转发
|
|
protocol = stream.TransStreamGBTalkForward
|
|
}
|
|
|
|
var port int
|
|
sink, port, err = stream.NewForwardSink(setup.TransportType(), protocol, sinkId, v.Source, v.Addr, gb28181.TransportManger)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
log.Sugar.Infof("创建转发sink成功, sink: %s port: %d transport: %s", sink.GetID(), port, setup.TransportType())
|
|
_, state := stream.PreparePlaySink(sink)
|
|
if utils.HookStateOK != state {
|
|
err = fmt.Errorf("failed to prepare play sink")
|
|
return
|
|
}
|
|
|
|
response := struct {
|
|
Sink string `json:"sink"` //sink id
|
|
SDP
|
|
}{Sink: stream.SinkId2String(sinkId), SDP: SDP{Addr: net.JoinHostPort(stream.AppConfig.PublicIP, strconv.Itoa(port))}}
|
|
|
|
httpResponseOK(w, &response)
|
|
}
|
|
|
|
// OnGBTalk 国标广播/对讲流程:
|
|
// 1. 浏览器使用WS携带source_id访问/api/v1/gb28181/talk, 如果source_id冲突, 直接断开ws连接
|
|
// 2. WS链接建立后, 调用gb-cms接口/api/v1/broadcast/invite, 向设备发送广播请求
|
|
func (api *ApiServer) OnGBTalk(w http.ResponseWriter, r *http.Request) {
|
|
conn, err := api.upgrader.Upgrade(w, r, nil)
|
|
if err != nil {
|
|
log.Sugar.Errorf("升级为websocket失败 err: %s", err.Error())
|
|
conn.Close()
|
|
return
|
|
}
|
|
|
|
// 获取id
|
|
id := r.FormValue("source")
|
|
|
|
talkSource := gb28181.NewTalkSource(id, conn)
|
|
talkSource.Init(stream.TCPReceiveBufferQueueSize)
|
|
talkSource.SetUrlValues(r.Form)
|
|
|
|
_, state := stream.PreparePublishSource(talkSource, true)
|
|
if utils.HookStateOK != state {
|
|
log.Sugar.Errorf("对讲失败, source: %s", talkSource)
|
|
conn.Close()
|
|
return
|
|
}
|
|
|
|
log.Sugar.Infof("ws对讲连接成功, source: %s", talkSource)
|
|
|
|
go stream.LoopEvent(talkSource)
|
|
|
|
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
|
|
}
|
|
|
|
for i := 0; i < length; {
|
|
data := stream.UDPReceiveBufferPool.Get().([]byte)
|
|
n := bufio.MinInt(stream.UDPReceiveBufferSize, length-i)
|
|
copy(data, bytes[:n])
|
|
_ = talkSource.PublishSource.Input(data[:n])
|
|
i += n
|
|
}
|
|
}
|
|
|
|
talkSource.Close()
|
|
}
|