mirror of
https://github.com/lkmio/gb-cms.git
synced 2026-04-22 23:17:22 +08:00
335 lines
10 KiB
Go
335 lines
10 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"gb-cms/common"
|
|
"gb-cms/dao"
|
|
"gb-cms/log"
|
|
"gb-cms/stack"
|
|
"github.com/ghettovoice/gosip/sip"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
func (api *ApiServer) OnStreamStart(v *InviteParams, w http.ResponseWriter, r *http.Request) (interface{}, error) {
|
|
return api.DoStartStream(v, w, r, "stream")
|
|
}
|
|
|
|
func (api *ApiServer) OnPlaybackStart(v *InviteParams, w http.ResponseWriter, r *http.Request) (interface{}, error) {
|
|
if v.Download {
|
|
return api.DoStartStream(v, w, r, "download")
|
|
} else {
|
|
return api.DoStartStream(v, w, r, "playback")
|
|
}
|
|
}
|
|
|
|
func (api *ApiServer) DoStartStream(v *InviteParams, w http.ResponseWriter, r *http.Request, action string) (interface{}, error) {
|
|
var code int
|
|
var stream *dao.StreamModel
|
|
var err error
|
|
if "playback" == action {
|
|
code, stream, err = apiServer.DoInvite(common.InviteTypePlayback, v, true)
|
|
} else if "download" == action {
|
|
code, stream, err = apiServer.DoInvite(common.InviteTypeDownload, v, true)
|
|
} else if "stream" == action {
|
|
code, stream, err = apiServer.DoInvite(common.InviteTypePlay, v, true)
|
|
} else {
|
|
return nil, fmt.Errorf("action not found")
|
|
}
|
|
|
|
if http.StatusOK != code {
|
|
log.Sugar.Errorf("请求流失败 err: %s", err.Error())
|
|
return nil, err
|
|
}
|
|
|
|
// 录像下载, 转发到streaminfo接口
|
|
if "download" == action {
|
|
if r.URL.RawQuery == "" {
|
|
r.URL.RawQuery = "streamid=" + string(v.streamId)
|
|
} else if r.URL.RawQuery != "" {
|
|
r.URL.RawQuery += "&streamid=" + string(v.streamId)
|
|
}
|
|
common.HttpForwardTo("/api/v1/stream/info", w, r)
|
|
return nil, nil
|
|
}
|
|
|
|
var urls map[string]string
|
|
urls = make(map[string]string, 10)
|
|
for _, streamUrl := range stream.Urls {
|
|
var streamName string
|
|
|
|
if strings.HasPrefix(streamUrl, "ws") {
|
|
streamName = "WS_FLV"
|
|
} else if strings.HasSuffix(streamUrl, ".flv") {
|
|
streamName = "FLV"
|
|
} else if strings.HasSuffix(streamUrl, ".m3u8") {
|
|
streamName = "HLS"
|
|
} else if strings.HasSuffix(streamUrl, ".rtc") {
|
|
streamName = "WEBRTC"
|
|
} else if strings.HasPrefix(streamUrl, "rtmp") {
|
|
streamName = "RTMP"
|
|
} else if strings.HasPrefix(streamUrl, "rtsp") {
|
|
streamName = "RTSP"
|
|
}
|
|
|
|
// 加上登录的token, 播放授权
|
|
streamUrl += "?stream_token=" + v.Token
|
|
|
|
// 兼容livegbs前端播放webrtc
|
|
if streamName == "WEBRTC" {
|
|
if strings.HasPrefix(streamUrl, "http") {
|
|
streamUrl = strings.Replace(streamUrl, "http", "webrtc", 1)
|
|
} else if strings.HasPrefix(streamUrl, "https") {
|
|
streamUrl = strings.Replace(streamUrl, "https", "webrtcs", 1)
|
|
}
|
|
|
|
streamUrl += "&wf=livegbs"
|
|
}
|
|
|
|
urls[streamName] = streamUrl
|
|
}
|
|
|
|
response := LiveGBSStream{
|
|
AudioEnable: true,
|
|
CDN: "",
|
|
CascadeSize: 0,
|
|
ChannelID: v.ChannelID,
|
|
ChannelName: "未读取通道名",
|
|
ChannelPTZType: 0,
|
|
CloudRecord: false,
|
|
DecodeSize: 0,
|
|
DeviceID: v.DeviceID,
|
|
Duration: 1,
|
|
FLV: urls["FLV"],
|
|
HLS: urls["HLS"],
|
|
InBitRate: 0,
|
|
InBytes: 0,
|
|
NumOutputs: 0,
|
|
Ondemand: true,
|
|
OutBytes: 0,
|
|
RTMP: urls["RTMP"],
|
|
RecordStartAt: "",
|
|
RelaySize: 0,
|
|
SMSID: "",
|
|
SnapURL: GetSnapshotPath(stream.StreamID) + "?preview_snapshot=1&t=" + stream.SessionID,
|
|
SourceAudioCodecName: "",
|
|
SourceAudioSampleRate: 0,
|
|
SourceVideoCodecName: "",
|
|
SourceVideoFrameRate: 0,
|
|
SourceVideoHeight: 0,
|
|
SourceVideoWidth: 0,
|
|
StartAt: "",
|
|
StreamID: string(stream.StreamID),
|
|
Transport: "TCP",
|
|
VideoFrameCount: 0,
|
|
WEBRTC: urls["WEBRTC"],
|
|
WS_FLV: urls["WS_FLV"],
|
|
}
|
|
|
|
return response, err
|
|
}
|
|
|
|
// DoInvite 发起Invite请求
|
|
// @params sync 是否异步等待流媒体的publish事件(确认收到流), 目前请求流分两种方式,流媒体hook和http接口, hook方式同步等待确认收到流再应答, http接口直接应答成功。
|
|
func (api *ApiServer) DoInvite(inviteType common.InviteType, params *InviteParams, sync bool) (int, *dao.StreamModel, error) {
|
|
device, _ := dao.Device.QueryDevice(params.DeviceID)
|
|
if device == nil || !device.Online() {
|
|
return http.StatusNotFound, nil, fmt.Errorf("设备离线 id: %s", params.DeviceID)
|
|
}
|
|
|
|
// 解析回放或下载的时间范围参数
|
|
var startTimeSeconds string
|
|
var endTimeSeconds string
|
|
if common.InviteTypePlay != inviteType {
|
|
startTime, err := time.ParseInLocation("2006-01-02T15:04:05", params.StartTime, time.Local)
|
|
if err != nil {
|
|
return http.StatusBadRequest, nil, err
|
|
}
|
|
|
|
endTime, err := time.ParseInLocation("2006-01-02T15:04:05", params.EndTime, time.Local)
|
|
if err != nil {
|
|
return http.StatusBadRequest, nil, err
|
|
}
|
|
|
|
startTimeSeconds = strconv.FormatInt(startTime.Unix(), 10)
|
|
endTimeSeconds = strconv.FormatInt(endTime.Unix(), 10)
|
|
}
|
|
|
|
if params.streamId == "" {
|
|
params.streamId = common.GenerateStreamID(inviteType, device.GetID(), params.ChannelID, params.StartTime, params.EndTime)
|
|
}
|
|
|
|
if params.Setup == "" {
|
|
params.Setup = device.GetSetup().String()
|
|
}
|
|
|
|
// 解析回放或下载速度参数
|
|
speed, _ := strconv.Atoi(params.Speed)
|
|
if speed < 1 {
|
|
speed = 4
|
|
}
|
|
d := &stack.Device{DeviceModel: device}
|
|
stream, err := d.StartStream(inviteType, params.streamId, params.ChannelID, startTimeSeconds, endTimeSeconds, params.Setup, speed, sync)
|
|
if err != nil {
|
|
return http.StatusInternalServerError, nil, err
|
|
}
|
|
|
|
return http.StatusOK, stream, nil
|
|
}
|
|
|
|
func (api *ApiServer) OnCloseStream(v *StreamIDParams, _ http.ResponseWriter, _ *http.Request) (interface{}, error) {
|
|
stack.CloseStream(v.StreamID, true)
|
|
return "OK", nil
|
|
}
|
|
|
|
func (api *ApiServer) OnCloseLiveStream(v *InviteParams, _ http.ResponseWriter, _ *http.Request) (interface{}, error) {
|
|
id := common.GenerateStreamID(common.InviteTypePlay, v.DeviceID, v.ChannelID, "", "")
|
|
stack.CloseStream(id, true)
|
|
return "OK", nil
|
|
}
|
|
|
|
func (api *ApiServer) OnStreamInfo(w http.ResponseWriter, r *http.Request) {
|
|
common.HttpForwardTo("/api/v1/stream/info", w, r)
|
|
}
|
|
|
|
func (api *ApiServer) OnSessionList(q *QueryDeviceChannel, _ http.ResponseWriter, r *http.Request) (interface{}, error) {
|
|
// 分页参数
|
|
if q.Limit < 1 {
|
|
q.Limit = 10
|
|
}
|
|
|
|
//filter := q.Filter // playing-正在播放/stream-不包含回放和下载/record-正在回放的流/hevc-h265流/cascade-级联
|
|
var streams []*dao.StreamModel
|
|
var err error
|
|
if "cascade" == q.Filter {
|
|
protocols := []int{stack.TransStreamGBCascaded}
|
|
var ids []string
|
|
ids, _, err = dao.Sink.QueryStreamIds(protocols, (q.Start/q.Limit)+1, q.Limit)
|
|
if len(ids) > 0 {
|
|
streams, err = dao.Stream.QueryStreamsByIds(ids)
|
|
}
|
|
} else if "stream" == q.Filter {
|
|
streams, _, err = dao.Stream.QueryStreams(q.Keyword, (q.Start/q.Limit)+1, q.Limit, "play")
|
|
} else if "record" == q.Filter {
|
|
streams, _, err = dao.Stream.QueryStreams(q.Keyword, (q.Start/q.Limit)+1, q.Limit, "playback")
|
|
} else if "playing" == q.Filter {
|
|
protocols := []int{stack.TransStreamRtmp, stack.TransStreamFlv, stack.TransStreamRtsp, stack.TransStreamHls, stack.TransStreamRtc}
|
|
var ids []string
|
|
ids, _, err = dao.Sink.QueryStreamIds(protocols, (q.Start/q.Limit)+1, q.Limit)
|
|
if len(ids) > 0 {
|
|
streams, err = dao.Stream.QueryStreamsByIds(ids)
|
|
}
|
|
} else {
|
|
streams, _, err = dao.Stream.QueryStreams(q.Keyword, (q.Start/q.Limit)+1, q.Limit, "")
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
response := struct {
|
|
SessionCount int
|
|
SessionList []*StreamInfo
|
|
}{}
|
|
|
|
bytes := make([]byte, 4096)
|
|
for _, stream := range streams {
|
|
values := url.Values{}
|
|
values.Set("streamid", string(stream.StreamID))
|
|
resp, err := stack.MSQueryStreamInfo(r.Header, values.Encode())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var n int
|
|
n, err = resp.Body.Read(bytes)
|
|
_ = resp.Body.Close()
|
|
if n < 1 {
|
|
break
|
|
}
|
|
|
|
info := &StreamInfo{}
|
|
err = json.Unmarshal(bytes[:n], info)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
info.ChannelName = stream.Name
|
|
response.SessionList = append(response.SessionList, info)
|
|
}
|
|
|
|
response.SessionCount = len(response.SessionList)
|
|
return &response, nil
|
|
}
|
|
|
|
func (api *ApiServer) OnSessionStop(params *StreamIDParams, _ http.ResponseWriter, _ *http.Request) (interface{}, error) {
|
|
err := stack.MSCloseSource(string(params.StreamID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return "OK", nil
|
|
}
|
|
|
|
func (api *ApiServer) OnRecordStart(writer http.ResponseWriter, request *http.Request) {
|
|
common.HttpForwardTo("/api/v1/record/start", writer, request)
|
|
}
|
|
|
|
func (api *ApiServer) OnRecordStop(writer http.ResponseWriter, request *http.Request) {
|
|
common.HttpForwardTo("/api/v1/record/stop", writer, request)
|
|
}
|
|
|
|
func (api *ApiServer) OnPlaybackControl(params *StreamIDParams, _ http.ResponseWriter, _ *http.Request) (interface{}, error) {
|
|
if "scale" != params.Command || params.Scale <= 0 || params.Scale > 4 {
|
|
return nil, errors.New("scale error")
|
|
}
|
|
|
|
stream, err := dao.Stream.QueryStream(params.StreamID)
|
|
if err != nil {
|
|
return nil, err
|
|
} else if stream.Dialog == nil {
|
|
return nil, errors.New("stream not found")
|
|
}
|
|
|
|
// 查找設備
|
|
device, err := dao.Device.QueryDevice(stream.DeviceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
s := &stack.Device{DeviceModel: device}
|
|
s.ScalePlayback(stream.Dialog, params.Scale)
|
|
err = stack.MSSpeedSet(string(params.StreamID), params.Scale)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return "OK", nil
|
|
}
|
|
|
|
func (api *ApiServer) OnSeekPlayback(v *SeekParams, _ http.ResponseWriter, _ *http.Request) (interface{}, error) {
|
|
log.Sugar.Debugf("快进回放 %v", *v)
|
|
|
|
model, _ := dao.Stream.QueryStream(v.StreamId)
|
|
if model == nil || model.Dialog == nil {
|
|
log.Sugar.Errorf("快进回放失败 stream不存在 %s", v.StreamId)
|
|
return nil, fmt.Errorf("stream不存在")
|
|
}
|
|
|
|
stream := &stack.Stream{StreamModel: model}
|
|
seekRequest := stream.CreateRequestFromDialog(sip.INFO)
|
|
seq, _ := seekRequest.CSeq()
|
|
body := fmt.Sprintf(stack.SeekBodyFormat, seq.SeqNo, v.Seconds)
|
|
seekRequest.SetBody(body, true)
|
|
seekRequest.RemoveHeader(stack.RtspMessageType.Name())
|
|
seekRequest.AppendHeader(&stack.RtspMessageType)
|
|
|
|
common.SipStack.SendRequest(seekRequest)
|
|
return nil, nil
|
|
}
|