流媒体使用的是开源项目lal的子项目lalmax
项目地址:https://github.com/q191201771/lalmax
Lal官网:https://pengrl.com/
1.LALMAX部署
这里假设的我流媒体服务器地址的IP是:10.13.1.36
将 lalmax 的程序 上传到 /home/lalmax/ 目录
启动
./lalmax
启动完之后就可以使用他的api进行推流拉流,这里以Onvif协议的摄像头为例:
推流
推流地址:http://10.13.1.36:1290/api/ctrl/onvif/pull
POST请求参数:
{
"addr": "10.12.7.102:8000",
"username": "admin",
"password": "hn_123456",
"rtspmode": 0,
"pullallprofiles": false,
"streamname": "test112"
}
查询流信息
GET请求:http://10.13.1.36:8083/api/stat/all_group
删除推流
GET请求:http://10.13.1.36:8083/api/ctrl/stop_relay_pull?stream_name=test110
stream_name 是流的名称。
监控画面
监控画面web控制台:http://10.13.1.36:8083/lal.html
2、lalmax的二开说明
lalmax的官方代码在推流的时候,是没有 streamname 这个参数的,默认的streamname 官方使用了 摄像头的型号 来命令,就是导致当同时查看 多个相同型号摄像头时,会占用同一个 streamname
需要对onvif/server.go的文件进行了一些修改:
package onvif
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/q191201771/lal/pkg/base"
"github.com/q191201771/naza/pkg/nazalog"
goonvif "github.com/use-go/onvif"
"github.com/use-go/onvif/device"
media "github.com/use-go/onvif/media"
sdk "github.com/use-go/onvif/sdk/device"
sdkmedia "github.com/use-go/onvif/sdk/media"
onvifcmd "github.com/use-go/onvif/xsd/onvif"
)
type OnvifPullRequest struct {
StreamName string `json:"streamname"` // streamName
Addr string `json:"addr"` // 摄像机IP:PORT
Username string `json:"username"` // 用户名
Password string `json:"password"` // 密码
RtspMode int `json:"rtspmode"` // rtsp拉流模式,0-tcp, 1-udp
PullAllProfiles bool `json:"pullallprofiles"` // 是否请求所有profiles
}
type OnvifServer struct {
}
func NewOnvifServer() *OnvifServer {
return &OnvifServer{}
}
func (s *OnvifServer) HandlePull(c *gin.Context) {
pullreq := OnvifPullRequest{}
err := c.ShouldBind(&pullreq)
if err != nil {
c.Status(http.StatusBadRequest)
return
}
// pullreq.StreamName不能为空
if pullreq.StreamName == "" {
var v base.ApiCtrlStartRelayPullResp
v.ErrorCode = base.ErrorCodeParamMissing
v.Desp = "streamName is empty"
c.JSON(http.StatusOK, v)
return
}
dev, err := goonvif.NewDevice(goonvif.DeviceParams{
Xaddr: pullreq.Addr,
Username: pullreq.Username,
Password: pullreq.Password,
})
if err != nil {
nazalog.Error(err)
// mww@2024-11-12
var v base.ApiCtrlStartRelayPullResp
v.ErrorCode = base.ErrorCodeParamMissing
v.Desp = err.Error()
c.JSON(http.StatusOK, v)
return
}
deviceInfoReq := device.GetDeviceInformation{}
deviceInfoRes, err := sdk.Call_GetDeviceInformation(context.Background(), dev, deviceInfoReq)
if err != nil {
nazalog.Error(err)
return
}
getCapabilities := device.GetCapabilities{Category: "All"}
_, err = sdk.Call_GetCapabilities(context.Background(), dev, getCapabilities)
if err != nil {
nazalog.Error("Call_GetCapabilities failed, err:", err)
c.Status(http.StatusInternalServerError)
return
}
profilesReq := media.GetProfiles{}
profilesRes, err := sdkmedia.Call_GetProfiles(context.Background(), dev, profilesReq)
if err != nil {
nazalog.Error("Call_GetProfiles failed, err:", err)
c.Status(http.StatusInternalServerError)
return
}
if len(profilesRes.Profiles) == 0 {
nazalog.Error("profilesRes.Profiles invalid")
c.Status(http.StatusInternalServerError)
return
}
var protocol onvifcmd.TransportProtocol
if pullreq.RtspMode == 1 {
protocol = "UDP"
} else {
protocol = "TCP"
}
if pullreq.PullAllProfiles {
for _, profile := range profilesRes.Profiles {
streamUrlReq := media.GetStreamUri{
ProfileToken: profile.Token,
StreamSetup: onvifcmd.StreamSetup{
Stream: "RTP-Unicast",
Transport: onvifcmd.Transport{
Protocol: protocol,
},
},
}
streamUrlRes, err := sdkmedia.Call_GetStreamUri(context.Background(), dev, streamUrlReq)
if err != nil {
nazalog.Error(err)
return
}
playUrl := buildPlayUrl(string(streamUrlRes.MediaUri.Uri), pullreq.Username, pullreq.Password)
// 使用stream name作为streamid
// DoPull(playUrl, fmt.Sprintf("%s-%s", deviceInfoRes.Model, profile.Name), pullreq.RtspMode)
DoPull(playUrl, fmt.Sprintf("%s-%s", deviceInfoRes.Model, profile.Name), pullreq.RtspMode)
}
} else {
streamUrlReq := media.GetStreamUri{
ProfileToken: profilesRes.Profiles[0].Token,
StreamSetup: onvifcmd.StreamSetup{
Stream: "RTP-Unicast",
Transport: onvifcmd.Transport{
Protocol: protocol,
},
},
}
streamUrlRes, err := sdkmedia.Call_GetStreamUri(context.Background(), dev, streamUrlReq)
if err != nil {
nazalog.Error(err)
return
}
playUrl := buildPlayUrl(string(streamUrlRes.MediaUri.Uri), pullreq.Username, pullreq.Password)
// mww@2024-11-12
// 使用OnvifPullRequest中的streamName
DoPull(playUrl, pullreq.StreamName, pullreq.RtspMode)
//DoPull(playUrl, fmt.Sprintf("%s-%s", deviceInfoRes.Model, profilesRes.Profiles[0].Name), pullreq.RtspMode)
var v base.ApiCtrlStartRelayPullResp
v.ErrorCode = base.ErrorCodeSucc
v.Desp = "success"
v.Data.StreamName = pullreq.StreamName
c.JSON(http.StatusOK, v)
}
}
func buildPlayUrl(rawurl, username, password string) string {
if username != "" && password != "" {
playUrl := fmt.Sprintf("rtsp://%s:%s@%s", username, password, strings.TrimLeft(rawurl, "rtsp://"))
return playUrl
}
return rawurl
}
func DoPull(url, streamname string, rtspmod int) {
request := base.ApiCtrlStartRelayPullReq{
Url: url,
StreamName: streamname,
RtspMode: rtspmod,
AutoStopPullAfterNoOutMs: -1,
}
data, _ := json.Marshal(request)
req, err := http.NewRequest("POST", "http://127.0.0.1:8083/api/ctrl/start_relay_pull", bytes.NewReader(data))
if err != nil {
return
}
req.Header.Set("Content-Type", "application/json")
cli := &http.Client{
Transport: http.DefaultTransport,
Timeout: time.Duration(5) * time.Second,
}
resp, err := cli.Do(req)
if err != nil {
return
}
if resp.StatusCode != 200 {
return
}
resp.Body.Close()
return
}