流媒体服务LALMAX的部署安装与使用


流媒体使用的是开源项目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

invalid image(图片无法加载)

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
}

superadmin 2025年2月11日 11:20 收藏文档