├── .github └── workflows │ ├── go.yml │ └── release.yml ├── Dockerfile ├── LICENSE ├── README.md ├── build.sh ├── conf ├── cert.pem ├── config.go ├── key.pem ├── lalmax.conf.json └── lalserver.conf.json ├── document ├── api.md ├── config.md ├── gb28181.md ├── rtc.md └── srt.md ├── fmp4 ├── hls │ ├── server.go │ └── session.go └── http-fmp4 │ ├── server.go │ └── session.go ├── gb28181 ├── auth.go ├── avail_conn_pool.go ├── channel.go ├── device.go ├── http_logic.go ├── inviteoption.go ├── mediaserver │ ├── conn.go │ ├── mediaserver_t.go │ └── server.go ├── mpegps │ ├── bitstream.go │ ├── pes_proto.go │ ├── ps_demuxer.go │ ├── ps_demuxer_test.go │ ├── ps_muxer.go │ ├── ps_proto.go │ └── util.go ├── ptz.go ├── server.go ├── t_http_api.go ├── util.go └── xml.go ├── go.mod ├── go.sum ├── hook ├── gop_cache.go ├── hookmanager.go └── hooksession.go ├── image ├── gb-hk.png ├── init.png ├── rtc_01.jpeg ├── rtc_02.png ├── srt_0.png ├── srt_1.png ├── srt_2.png └── srt_3.png ├── main.go ├── onvif └── server.go ├── room └── server.go ├── rtc ├── jessibucasession.go ├── packer.go ├── peerConnection.go ├── server.go ├── unpacker.go ├── whepsession.go └── whipsession.go ├── run.sh ├── server ├── http_notify.go ├── middle.go ├── router.go ├── router_test.go └── server.go ├── srt ├── pub.go ├── server.go ├── stream_id.go └── sub.go ├── thirdparty └── srt-1.5.1.tar.gz └── version ├── README.md ├── v0.1.0.md └── v0.2.0.md /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v4 21 | with: 22 | go-version: '1.21' 23 | 24 | - name: Build for Linux, macOS, and Windows 25 | run: | 26 | GOOS=linux go build -o lalmax-linux main.go 27 | GOOS=darwin go build -o lalmax-macos main.go 28 | GOOS=windows go build -o lalmax-windows.exe main.go 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # https://github.com/wangyoucao577/go-release-action 2 | 3 | name: build-go-binary 4 | 5 | on: 6 | release: 7 | types: [created] # 表示在创建新的 Release 时触发 8 | 9 | permissions: 10 | contents: write 11 | packages: write 12 | 13 | jobs: 14 | build-go-binary: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | goos: [linux, windows, darwin] # 需要打包的系统 19 | goarch: [amd64, arm64] # 需要打包的架构 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: wangyoucao577/go-release-action@v1.49 23 | with: 24 | github_token: ${{ secrets.GITHUB_TOKEN }} 25 | goos: ${{ matrix.goos }} 26 | goarch: ${{ matrix.goarch }} 27 | goversion: 1.22 28 | md5sum: false 29 | extra_files: ./README.md ./conf 30 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21.1 2 | ENV GOPROXY=https://goproxy.cn,https://goproxy.io,direct 3 | LABEL maintainer="Kevin Zang" 4 | 5 | WORKDIR /code 6 | COPY . . 7 | RUN /bin/bash ./build.sh 8 | 9 | EXPOSE 1935 8080 4433 5544 8083 8084 1290 30000-30100/udp 6001/udp 4888/udp 10 | 11 | CMD export LD_LIBRARY_PATH=/usr/local/lib/ && ./run.sh -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Chef 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lalmax 2 | lalmax是在lal的基础上集成第三方库,可以提供SRT、RTC、mp4、gb28181、onvif等解决方案 3 | 4 | # 编译 5 | ./build.sh 6 | 7 | # 运行 8 | ./run.sh或者./lalmax -c conf/lalmax.conf.json 9 | 10 | # 配置说明 11 | lalmax.conf.json配置主要由2部分组成 12 | 13 | (1) lal_config_path表示lal配置文件的路径,用于加载lal本身的配置,具体配置说明见[lal配置](https://pengrl.com/lal/#/ConfigBrief) 14 | 15 | (2) 剩余的配置则为lalmax的配置,具体配置说明见[config.md](./document/config.md) 16 | 17 | 18 | # docker运行 19 | ``` 20 | docker build -t lalmax:init ./ 21 | 22 | docker run -it -p 1935:1935 -p 8080:8080 -p 4433:4433 -p 5544:5544 -p 8083:8083 -p 8084:8084 -p 30000-30100:30000-30100/udp -p 1290:1290 -p 6001:6001/udp lalmax:init 23 | 24 | ``` 25 | 26 | # 架构 27 | 28 | ![图片](image/init.png) 29 | 30 | # 支持的协议 31 | ## 推流 32 | (1) RTSP 33 | 34 | (2) SRT 35 | 36 | (3) RTMP 37 | 38 | (4) RTC(WHIP) 39 | 40 | (5) GB28181 41 | 42 | 具体的推流url地址(除了srt/whip) 43 | 44 | https://pengrl.com/lal/#/streamurllist 45 | 46 | ## 拉流 47 | (1) RTSP 48 | 49 | (2) SRT 50 | 51 | (3) RTMP 52 | 53 | (4) HLS(S)-TS 54 | 55 | (5) HTTP(S)-FLV 56 | 57 | (6) HTTP(S)-TS 58 | 59 | (7) RTC(WHEP) 60 | 61 | (8) HTTP(S)-FMP4 62 | 63 | (9) HLS(S)-FMP4/LLHLS 64 | 65 | 66 | 具体的拉流url地址见https://pengrl.com/lal/#/streamurllist(除了srt/whep) 67 | 68 | ## [SRT](./document/srt.md) 69 | (1)使用gosrt库 70 | 71 | (2)暂时不支持SRT加密 72 | 73 | (3)支持H264/H265/AAC 74 | 75 | (4)可以对接OBS/VLC 76 | 77 | ``` 78 | 推流url 79 | srt://127.0.0.1:6001?streamid=#!::h=test110,m=publish 80 | 81 | 拉流url 82 | srt://127.0.0.1:6001?streamid=#!::h=test110,m=request 83 | ``` 84 | 85 | ## [WebRTC](./document/rtc.md) 86 | (1)支持WHIP推流和WHEP拉流,暂时只支持POST信令 87 | 88 | (2)支持H264/G711A/G711U/OPUS 89 | 90 | (3)可以对接OBS、vue-wish 91 | 92 | (4)WHEP支持对接Safari HEVC 93 | 94 | (5)支持datachannel,只支持对接jessibuca播放器 95 | 96 | (6)WHIP支持对接OBS 30.2 beta HEVC 97 | 98 | datachannel播放地址:webrtc://127.0.0.1:1290/webrtc/play/live/test110 99 | 100 | ``` 101 | WHIP推流url 102 | http(s)://127.0.0.1:1290/webrtc/whip?streamid=test110 103 | 104 | WHEP拉流url 105 | http(s)://127.0.0.1:1290/webrtc/whep?streamid=test110 106 | ``` 107 | 108 | ## Http-fmp4 109 | (1) 支持H264/H265/AAC/G711A/G711U 110 | 111 | ``` 112 | 拉流url 113 | http(s)://127.0.0.1:1290/live/m4s/test110.mp4 114 | ``` 115 | 116 | ## HLS(fmp4/Low Latency) 117 | (1) 支持H264/H265/AAC/OPUS 118 | 119 | ``` 120 | 拉流url 121 | http(s)://127.0.0.1:1290/live/hls/test110/index.m3u8 122 | ``` 123 | 124 | ## [GB28181](./document/gb28181.md) 125 | (1) 作为SIP服务器与设备进行SIP交互,使用单端口/多端口收流 126 | 127 | (2) 提供相关API获取设备信息、播放某通道等功能 128 | 129 | (3) 支持H264/H265/AAC/G711A/G711U 130 | 131 | (4) 支持TCP/UDP 132 | 133 | ## Onvif 134 | (1) onvif接入设备进行pull拉流 135 | 136 | (2) 支持tcp/udp拉流 137 | 138 | (3) pull api:http://127.0.0.1:1290/api/ctrl/onvif/pull 139 | ``` 140 | { 141 | "addr": , // 设备IP:PORT 142 | "username": , // 用户名 143 | "password": , // 密码 144 | "rtspmode": , // rtsp拉流模式,0-tcp, 1-udp 145 | "pullallprofiles": // 是否请求所有的profiles 146 | } 147 | ``` 148 | 149 | ## Room 150 | (1)集成livekit实现房间功能 151 | 152 | 153 | # QQ交流群 154 | 11818248 155 | 156 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "build lalmax" 4 | go build -o lalmax main.go -------------------------------------------------------------------------------- /conf/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICpDCCAYwCCQDWutSYrD7joDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls 3 | b2NhbGhvc3QwHhcNMjAwOTA3MTAxNzI2WhcNMzAwOTA1MTAxNzI2WjAUMRIwEAYD 4 | VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDh 5 | dywjAnWKbaQCBtNA9mbvqVsM2S7MStaw9JkcZ45L/MtKxUoP/1egqdADtBPeZxZl 6 | XPxx+vcox9uO2nPZ+OyjedzMtgddK8ix0u2QPdOoc8+HW0fYGKO+YOXKUXUpawIg 7 | ZUhkUZAgrvlZUIewlZ9T0zMAsN3PUuZtg8ux+V3fY28l2QuulC5Q68i8m5vPVwj8 8 | QRitxtKj66fE7Ut5xIc9XAuFJcvYVFSoZuB3/xNbyVev1e2bAe4kYq/+Jt2CTZf4 9 | y7ESQsJn1Ybj0ippOFp9ZJq53roqEF9L0jUKNxNnJHhNc6niUfYehfvAvqXq/QiU 10 | B2qXpZonaq0AICMYFb+nAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAG98/mR8poSA 11 | MepIK6CfZsBzbDkl01R4+dw8RY9SwnOgFE5ddTh955UQvK1ORlVrTgBGkpH3djQd 12 | 1I4ADvrYIYHekgoRQX+fFNGyviEftzUUV4aq04JTttrrWrilgoF356pkkID0xSsq 13 | 9dr7at36wzV/Sbt9DrW8S9iBX6aUk3PPDxHJhi6xl+bYE6lepVlQ27FZjONo6cpg 14 | MnoRnsOhi/T+VHDUOaR+Xl4I77hESq6ipnV0GJfAAJ7R0hjfBmRbdIxo88iHN8vf 15 | iBHq2THAF+mzApI85ASltPNF+i8ZX0Fn9CvJFwci5eiaoEjjXMbon5X6/n5ovnW/ 16 | SaAkF3AobD0= 17 | -----END CERTIFICATE----- 18 | -------------------------------------------------------------------------------- /conf/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | ) 7 | 8 | var defaultConfig Config 9 | 10 | type Config struct { 11 | SrtConfig SrtConfig `json:"srt_config"` // srt配置 12 | RtcConfig RtcConfig `json:"rtc_config"` // rtc配置 13 | HttpConfig HttpConfig `json:"http_config"` // http/https配置 14 | HttpFmp4Config HttpFmp4Config `json:"httpfmp4_config"` // http-fmp4配置 15 | HlsConfig HlsConfig `json:"hls_config"` // hls-fmp4/llhls配置 16 | GB28181Config GB28181Config `json:"gb28181_config"` // gb28181配置 17 | OnvifConfig OnvifConfig `json:"onvif_config"` // onvif配置 18 | ServerId string `json:"server_id"` // http 通知唯一标识 19 | HttpNotifyConfig HttpNotifyConfig `json:"http_notify"` // http 通知配置 20 | LalSvrConfigPath string `json:"lal_config_path"` // lal配置目录 21 | HookConfig HookConfig `json:"hook_config"` // gop cache配置 22 | RoomConfig RoomConfig `json:"room_config"` // room配置 23 | } 24 | 25 | type SrtConfig struct { 26 | Enable bool `json:"enable"` // srt服务使能配置 27 | Addr string `json:"addr"` // srt服务监听地址 28 | } 29 | 30 | type RtcConfig struct { 31 | Enable bool `json:"enable"` // rtc服务使能配置 32 | ICEHostNATToIPs []string `json:"ice_host_nat_to_ips"` // rtc服务公网IP,未设置使用内网 33 | ICEUDPMuxPort int `json:"ice_udp_mux_port"` // rtc udp mux port 34 | ICETCPMuxPort int `json:"ice_tcp_mux_port"` // rtc tcp mux port 35 | WriteChanSize int `json:"write_chan_size"` 36 | } 37 | 38 | type HttpConfig struct { 39 | ListenAddr string `json:"http_listen_addr"` // http服务监听地址 40 | EnableHttps bool `json:"enable_https"` // https使能标志 41 | HttpsListenAddr string `json:"https_listen_addr"` // https监听地址 42 | HttpsCertFile string `json:"https_cert_file"` // https cert 文件 43 | HttpsKeyFile string `json:"https_key_file"` // https key 文件 44 | CtrlAuthWhitelist CtrlAuthWhitelist `json:"ctrl_auth_whitelist"` 45 | } 46 | 47 | // CtrlAuthWhitelist 控制类接口鉴权 48 | type CtrlAuthWhitelist struct { 49 | IPs []string // 允许访问的远程 IP,零值时不生效 50 | Secrets []string // 认证信息,零值时不生效 51 | } 52 | 53 | type HttpFmp4Config struct { 54 | Enable bool `json:"enable"` // http-fmp4使能标志 55 | } 56 | 57 | type HlsConfig struct { 58 | Enable bool `json:"enable"` // hls使能标志 59 | SegmentCount int `json:"segment_count"` // 分片个数,llhls默认7个 60 | SegmentDuration int `json:"segment_duration"` // hls分片时长,默认1s 61 | PartDuration int `json:"part_duration"` // llhls part时长,默认200ms 62 | LowLatency bool `json:"low_latency"` // 是否开启llhls 63 | } 64 | 65 | type GB28181Config struct { 66 | Enable bool `json:"enable"` // gb28181使能标志 67 | ListenAddr string `json:"listen_addr"` // gb28181监听地址 68 | SipIP string `json:"sip_ip"` // sip 服务器公网IP 69 | SipPort uint16 `json:"sip_port"` // sip 服务器端口,默认 5060 70 | Serial string `json:"serial"` // sip 服务器 id, 默认 34020000002000000001 71 | Realm string `json:"realm"` // sip 服务器域,默认 3402000000 72 | Username string `json:"username"` // sip 服务器账号 73 | Password string `json:"password"` // sip 服务器密码 74 | KeepaliveInterval int `json:"keepalive_interval"` // 心跳包时长 75 | QuickLogin bool `json:"quick_login"` // 快速登陆,有keepalive就认为在线 76 | MediaConfig GB28181MediaConfig `json:"media_config"` // 媒体服务器配置 77 | } 78 | 79 | type GB28181MediaConfig struct { 80 | MediaIp string `json:"media_ip"` // 流媒体IP,用于在SDP中指定 81 | ListenPort uint16 `json:"listen_port"` // tcp,udp监听端口 默认启动 82 | MultiPortMaxIncrement uint16 `json:"multi_port_max_increment"` //多端口范围 ListenPort+1至ListenPort+MultiPortMax 83 | } 84 | 85 | type OnvifConfig struct { 86 | Enable bool `json:"enable"` // onvif使能标志 87 | } 88 | 89 | type HttpNotifyConfig struct { 90 | Enable bool `json:"enable"` 91 | UpdateIntervalSec int `json:"update_interval_sec"` 92 | OnServerStart string `json:"on_server_start"` 93 | OnUpdate string `json:"on_update"` 94 | OnPubStart string `json:"on_pub_start"` 95 | OnPubStop string `json:"on_pub_stop"` 96 | OnSubStart string `json:"on_sub_start"` 97 | OnSubStop string `json:"on_sub_stop"` 98 | OnRelayPullStart string `json:"on_relay_pull_start"` 99 | OnRelayPullStop string `json:"on_relay_pull_stop"` 100 | OnRtmpConnect string `json:"on_rtmp_connect"` 101 | OnHlsMakeTs string `json:"on_hls_make_ts"` 102 | } 103 | 104 | type HookConfig struct { 105 | GopCacheNum int `json:"gop_cache_num"` 106 | SingleGopMaxFrameNum int `json:"single_gop_max_frame_num"` 107 | } 108 | 109 | type RoomConfig struct { 110 | Enable bool `json:"enable"` // room功能使能标志 111 | APIKey string `json:"api_key"` // livekit api key 112 | APISecret string `json:"api_secret"` // livekit api secret 113 | } 114 | 115 | func Open(filepath string) error { 116 | data, err := ioutil.ReadFile(filepath) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | err = json.Unmarshal(data, &defaultConfig) 122 | if err != nil { 123 | return err 124 | } 125 | return nil 126 | } 127 | 128 | func GetConfig() *Config { 129 | return &defaultConfig 130 | } 131 | -------------------------------------------------------------------------------- /conf/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEogIBAAKCAQEA4XcsIwJ1im2kAgbTQPZm76lbDNkuzErWsPSZHGeOS/zLSsVK 3 | D/9XoKnQA7QT3mcWZVz8cfr3KMfbjtpz2fjso3nczLYHXSvIsdLtkD3TqHPPh1tH 4 | 2BijvmDlylF1KWsCIGVIZFGQIK75WVCHsJWfU9MzALDdz1LmbYPLsfld32NvJdkL 5 | rpQuUOvIvJubz1cI/EEYrcbSo+unxO1LecSHPVwLhSXL2FRUqGbgd/8TW8lXr9Xt 6 | mwHuJGKv/ibdgk2X+MuxEkLCZ9WG49IqaThafWSaud66KhBfS9I1CjcTZyR4TXOp 7 | 4lH2HoX7wL6l6v0IlAdql6WaJ2qtACAjGBW/pwIDAQABAoIBADTClml64daK4Z43 8 | yqehAWWD0/Klv/W+bY7rLgkfkoTlmwzcLgCgV/kYw7yaHywkI3GE2O4zNDMu0YoU 9 | RJf1UCrREYI19nMvE7/JBB6E2UrKDv41thIzcd3S/vLhLPGMQOsjyFTxYTDEwUTN 10 | O3NvD+GlwoGe4cjqNVHbTYdQO09SnN7lJOIhetJ5MQWvH5kiOE+xh9AxEpYOI2K9 11 | RMKKaUOBQjm0Q0ezMSZ1gfwDh7i642uDvVpeQ8uDg+1BZ/ulAJpaL6pE4KTUfCAR 12 | ygyQajvj2c93x/876ueacRBRECkvwoliOuQE8SmbjqQ5oDikHD8BNgbIwb/snZbt 13 | x3bj6kECgYEA+k5c9ZpqSkTtUK+lCVDV4zfSHOn//6J9pFr/f2o44nTsVUuO7Oqi 14 | jxikbx/BjEc6veRnLu2aqqMHuw7wV6EnS7Ikbc0lRNcXsBgN0WYbT26CLmAx0G1t 15 | 9DFyPdGT7/LZzNa11YqK/NuL2EFQ06SC7JA23+yfI5EJjOR0V6npU7sCgYEA5pgm 16 | 9PjyyfCvOF6wPbDRyGO359aXXtavKGPomOrV1FvTGt282NpbOOoOa7XlD3013A7i 17 | pRGiDYZO0FS/2ajYXz/uAaBh0LvKPhTZaXcezJ9GfA2HqeuTbQwhUsj17TLbgC1Y 18 | 7xSbwMwt8uRh3KdbLf220U/WHLPJMjXsME9nBwUCgYAnyCyeHFyoUSwmlsP0JxTX 19 | eBe84LP/PSQa6xuQdKF13H9zTv74SJJti80WnEV2thtv8s0zeDAMzrx7znQEeWh1 20 | b2q6yNATkNwC8M/BaCkPBtFJ7Z/9MGc5WGJ/0L9ic4aKN9XOiqZsabhgNoFSIeNt 21 | Fb6i+EiSroqGCgkzpZ2f4QKBgHEqrLu+zVBjyWpNtgqgk2PX5HJn8yO9EnstBQK/ 22 | BS/R3Lmrprl5+BjnbSpZO1Atr9gOihZen/wpNNazMPA+F+ou8rxjnH2XG7r5+nTy 23 | 2++qHypUbYbrsQ9sS5JYQ7EkK2stVh8HKyUkT0yL3qcujuX0RNtWZgryBMSaiA5x 24 | eWuNAoGAI/87JrId6LzL9RSJFnXtkbYDNw1Zf7OTtjcXytn5U64sG/DEQNt8m6um 25 | q/JNM32vH8Fz+JcRVVGJlN2bSsxYxpIzhd7SBS7Cq0a6gHFeRsqcTA5qidxbUPLn 26 | itJ84Oz1Zo2U5MG+Zj+2sLm4v6611RkYyOiSEMvvV6ZJ/TGBPDc= 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /conf/lalmax.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "srt_config": { 3 | "enable": true, 4 | "addr": ":6001" 5 | }, 6 | "rtc_config": { 7 | "enable": true, 8 | "ice_host_nat_to_ips": [], 9 | "ice_udp_mux_port": 4888, 10 | "ice_tcp_mux_port": 4888 11 | }, 12 | "http_config": { 13 | "http_listen_addr": ":1290", 14 | "enable_https": true, 15 | "https_listen_addr": ":1233", 16 | "https_cert_file": "./conf/cert.pem", 17 | "https_key_file": "./conf/key.pem", 18 | "ctrl_auth_whitelist": { 19 | "ips": [], 20 | "secrets": [] 21 | } 22 | }, 23 | "httpfmp4_config": { 24 | "enable": true 25 | }, 26 | "hls_config": { 27 | "enable": true 28 | }, 29 | "hook_config": { 30 | "gop_cache_num": 1, 31 | "single_gop_max_frame_num": 0 32 | }, 33 | "gb28181_config": { 34 | "enable": true, 35 | "serial": "34020000002000000001", 36 | "realm": "3402000000", 37 | "sip_ip": "192.168.254.165", 38 | "sip_port": 5060, 39 | "username": "", 40 | "media_config": { 41 | "media_ip": "192.168.254.165" 42 | }, 43 | "quick_login": true 44 | }, 45 | "onvif_config": { 46 | "enable": true 47 | }, 48 | "room_config": { 49 | "enable": true, 50 | "api_key": "lalmaxkey", 51 | "api_secret": "lalmaxsecret" 52 | }, 53 | "server_id": "1", 54 | "http_notify": { 55 | "enable": false, 56 | "update_interval_sec": 5, 57 | "on_update": "http://127.0.0.1:10101/on_update", 58 | "on_pub_start": "http://127.0.0.1:10101/on_pub_start", 59 | "on_pub_stop": "http://127.0.0.1:10101/on_pub_stop", 60 | "on_sub_start": "http://127.0.0.1:10101/on_sub_start", 61 | "on_sub_stop": "http://127.0.0.1:10101/on_sub_stop", 62 | "on_relay_pull_start": "http://127.0.0.1:10101/on_relay_pull_start", 63 | "on_relay_pull_stop": "http://127.0.0.1:10101/on_relay_pull_stop", 64 | "on_rtmp_connect": "http://127.0.0.1:10101/on_rtmp_connect", 65 | "on_server_start": "http://127.0.0.1:10101/on_server_start", 66 | "on_hls_make_ts": "http://127.0.0.1:10101/on_hls_make_ts" 67 | }, 68 | "lal_config_path:": "./conf/lalserver.conf.json" 69 | } 70 | -------------------------------------------------------------------------------- /conf/lalserver.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "# doc of config": "https://pengrl.com/lal/#/ConfigBrief", 3 | "conf_version": "v0.4.1", 4 | "rtmp": { 5 | "enable": true, 6 | "addr": ":1935", 7 | "rtmps_enable": true, 8 | "rtmps_addr": ":4935", 9 | "rtmps_cert_file": "./conf/cert.pem", 10 | "rtmps_key_file": "./conf/key.pem", 11 | "gop_num": 1, 12 | "single_gop_max_frame_num": 0, 13 | "merge_write_size": 0 14 | }, 15 | "in_session": { 16 | "add_dummy_audio_enable": false, 17 | "add_dummy_audio_wait_audio_ms": 150 18 | }, 19 | "default_http": { 20 | "http_listen_addr": ":8080", 21 | "https_listen_addr": ":4433", 22 | "https_cert_file": "./conf/cert.pem", 23 | "https_key_file": "./conf/key.pem" 24 | }, 25 | "httpflv": { 26 | "enable": true, 27 | "enable_https": true, 28 | "url_pattern": "/", 29 | "gop_num": 0, 30 | "single_gop_max_frame_num": 0 31 | }, 32 | "hls": { 33 | "enable": false, 34 | "enable_https": false, 35 | "url_pattern": "/hls/", 36 | "out_path": "./lal_record/hls/", 37 | "fragment_duration_ms": 3000, 38 | "fragment_num": 6, 39 | "delete_threshold": 6, 40 | "cleanup_mode": 1, 41 | "use_memory_as_disk_flag": false, 42 | "sub_session_timeout_ms": 30000, 43 | "sub_session_hash_key": "" 44 | }, 45 | "httpts": { 46 | "enable": false, 47 | "enable_https": false, 48 | "url_pattern": "/", 49 | "gop_num": 0, 50 | "single_gop_max_frame_num": 0 51 | }, 52 | "rtsp": { 53 | "enable": true, 54 | "addr": ":5544", 55 | "rtsps_enable": true, 56 | "rtsps_addr": ":5322", 57 | "rtsps_cert_file": "./conf/cert.pem", 58 | "rtsps_key_file": "./conf/key.pem", 59 | "out_wait_key_frame_flag": true, 60 | "auth_enable": false, 61 | "auth_method": 1, 62 | "username": "q191201771", 63 | "password": "pengrl" 64 | }, 65 | "record": { 66 | "enable_flv": false, 67 | "flv_out_path": "./lal_record/flv/", 68 | "enable_mpegts": false, 69 | "mpegts_out_path": "./lal_record/mpegts" 70 | }, 71 | "relay_push": { 72 | "enable": false, 73 | "addr_list":[ 74 | ] 75 | }, 76 | "static_relay_pull": { 77 | "enable": false, 78 | "addr": "" 79 | }, 80 | "http_api": { 81 | "enable": true, 82 | "addr": ":8083" 83 | }, 84 | "server_id": "1", 85 | "http_notify": { 86 | "enable": false, 87 | "update_interval_sec": 5, 88 | "on_update": "http://127.0.0.1:10101/on_update", 89 | "on_pub_start": "http://127.0.0.1:10101/on_pub_start", 90 | "on_pub_stop": "http://127.0.0.1:10101/on_pub_stop", 91 | "on_sub_start": "http://127.0.0.1:10101/on_sub_start", 92 | "on_sub_stop": "http://127.0.0.1:10101/on_sub_stop", 93 | "on_relay_pull_start": "http://127.0.0.1:10101/on_relay_pull_start", 94 | "on_relay_pull_stop": "http://127.0.0.1:10101/on_relay_pull_stop", 95 | "on_rtmp_connect": "http://127.0.0.1:10101/on_rtmp_connect", 96 | "on_server_start": "http://127.0.0.1:10101/on_server_start", 97 | "on_hls_make_ts": "http://127.0.0.1:10101/on_hls_make_ts" 98 | }, 99 | "simple_auth": { 100 | "key": "q191201771", 101 | "dangerous_lal_secret": "pengrl", 102 | "pub_rtmp_enable": false, 103 | "sub_rtmp_enable": false, 104 | "sub_httpflv_enable": false, 105 | "sub_httpts_enable": false, 106 | "pub_rtsp_enable": false, 107 | "sub_rtsp_enable": false, 108 | "hls_m3u8_enable": false 109 | }, 110 | "pprof": { 111 | "enable": true, 112 | "addr": ":8084" 113 | }, 114 | "log": { 115 | "level": 1, 116 | "filename": "./logs/lalmax.log", 117 | "is_to_stdout": true, 118 | "is_rotate_daily": true, 119 | "short_file_flag": true, 120 | "timestamp_flag": true, 121 | "timestamp_with_ms_flag": true, 122 | "level_flag": true, 123 | "assert_behavior": 1 124 | }, 125 | "debug": { 126 | "log_group_interval_sec": 30, 127 | "log_group_max_group_num": 10, 128 | "log_group_max_sub_num_per_group": 10 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /document/config.md: -------------------------------------------------------------------------------- 1 | # srt_config 2 | 主要用于设置srt相关的配置 3 | - enable: srt服务使能配置,设置为true才可以使用srt功能 4 | 5 | *类型*: bool 6 | 7 | *值举例*: true 8 | 9 | - addr[string]: srt服务监听地址,srt服务监听的是UDP端口 10 | 11 | *类型*: string 12 | 13 | *值举例*: ":6001" 14 | 15 | # rtc_config 16 | 主要用于设置rtc相关的配置,目前rtc只实现了WHIP/WHEP,需要配合http_config一起使用 17 | - enable: rtc服务使能配置,设置为true才可以使用rtc功能 18 | 19 | *类型*: bool 20 | 21 | *值举例*: true 22 | 23 | - iceHostNatToIps: rtc服务内穿ip,具体为SDP中的candidate信息,不设置的话,会输出全部网卡的地址 24 | 25 | *类型*: []string 26 | 27 | *举例*: ["192.168.0.1"] 28 | 29 | - iceUdpMuxPort: rtc udp复用端口 30 | 31 | *类型*: int 32 | 33 | *值举例*: 4888 34 | 35 | - iceTcpMuxPort: rtc tcp复用端口 36 | 37 | *类型*: int 38 | 39 | *值举例*: 4888 40 | 41 | # http_config 42 | 主要用于设置http相关的配置,依赖http的协议均需要设置,涉及的协议有rtc、http-fmp4、hls(fmp4/llhls) 43 | - http_listen_addr: http服务监听地址 44 | 45 | *类型*: string 46 | 47 | *值举例*: ":1290" 48 | 49 | - enable_https: https使能 50 | 51 | *类型*: bool 52 | 53 | *值举例*: true 54 | 55 | - https_listen_addr: https监听地址 56 | 57 | *类型*: string 58 | 59 | *值举例*: ":1233" 60 | 61 | - https_cert_file: https cert文件路径 62 | 63 | *类型*: string 64 | 65 | *值举例*: "./conf/cert.pem" 66 | 67 | - https_key_file: https key文件路径 68 | 69 | *类型*: string 70 | 71 | *值举例*: "./conf/key.pem" 72 | 73 | - ctrl_auth_whitelist: 统计控制类接口鉴权,用于访问以 `/api/stat` 和 `/api/ctrl` 前缀的接口,无权限访问时 http status 将会响应 200,其 error_code 为 401。多种鉴权方式都不是零值时,必须同时满足才会通过鉴权。 74 | 75 | *类型*: object 76 | 77 | - secrets: 用户请求鉴权的方式是增加 query 参数 `token`,例如 `token=secret`,满足数组中任意匹配则通过。 78 | 79 | *类型*: []string 80 | 81 | *值举例*: ["EC3D1536-5D93-4BD6-9FBD-96A52CB1596D"] 82 | 83 | - ips: 远程 IP 白名单,空数组表示允许任意 IP 访问,无权限访问时 http status 将会响应 200,其 error_code 为 401。 84 | 85 | *类型*: []string 86 | 87 | *值举例*: ["192.168.1.2","192.168.1.3"] 88 | 89 | 90 | # http-fmp4配置 91 | 主要用于设置http-fmp4相关的配置,需要配合http_config一起使用 92 | - enable: http-fmp4服务使能配置 93 | 94 | *类型*: bool 95 | 96 | *值举例*: true 97 | 98 | # hls_config 99 | 主要用于设置hls-fmp4/llhls相关的配置,需要配合http_config一起使用,hls-ts的能力请使用lal,这里不做过多描述 100 | - enable: hls-fmp4/llhls服务使能配置 101 | 102 | *类型*: bool 103 | 104 | *值举例*: true 105 | 106 | - segmentCount: hls-fmp4 m3u8返回的切片个数,默认为7, llhls默认设置为7个(gohlslib要求) 107 | 108 | *类型*: int 109 | 110 | *值举例*: 3 111 | 112 | - segmentDuration: hls-fmp4 切片时长,默认为1s 113 | 114 | *类型*: int 115 | 116 | *值举例*: 3 117 | 118 | - partDuration:llhls part部分的时长,默认为200ms 119 | 120 | *类型*: int 121 | 122 | *值举例*: 100 123 | 124 | - lowLatency: llhls使能配置,开启此配置后则都走llhls 125 | 126 | *类型*: bool 127 | 128 | *值举例*: true 129 | 130 | # hook_config 131 | 主要用于 hook 相关的配置。 132 | 133 | - gop_cache_num: gop 缓存的数量,默认为 1 134 | 135 | *类型*: int 136 | 137 | *值举例*: 3 138 | 139 | - single_gop_max_frame_num: 一个 gop 的缓存帧数,0 表示智能识别 140 | 141 | *类型*: int 142 | 143 | *值举例*: 120 144 | 145 | 146 | # gb28181_config 147 | 148 | - enable: gb28181使能配置 149 | 150 | *类型*: bool 151 | 152 | *值举例*: true 153 | 154 | - listenAddr: gb28181监听地址 155 | 156 | *类型*: string 157 | 158 | *值举例*: "0.0.0.0" 159 | 160 | - sipNetwork: 传输协议 161 | 162 | *类型*: string 163 | 164 | *值举例*: "udp" 165 | 166 | - sipIp: sip服务器公网IP 167 | 168 | *类型*: string 169 | 170 | *值举例*: "100.100.100.101" 171 | 172 | - sipPort: sip服务器公网端口 173 | 174 | *类型*: uint16 175 | 176 | *值举例*: 5060 177 | 178 | - serial: sip服务器ID 179 | 180 | *类型*: string 181 | 182 | *值举例*: "34020000002000000001" 183 | 184 | - realm: sip服务器域 185 | 186 | *类型*: string 187 | 188 | *值举例*: "3402000000" 189 | 190 | - username: sip服务器账号 191 | 192 | *类型*: string 193 | 194 | *值举例*: "admin" 195 | 196 | - password: sip服务器密码 197 | 198 | *类型*: string 199 | 200 | *值举例*: "admin123" 201 | 202 | # onvif_config 203 | - enable: onvif使能配置 204 | 205 | *类型*: bool 206 | 207 | *值举例*: true 208 | 209 | # lal_config_path 210 | 主要设置lal配置文件的路径 211 | -------------------------------------------------------------------------------- /document/gb28181.md: -------------------------------------------------------------------------------- 1 | # GB28181 2 | 3 | lalmax的gb28181功能为单端口监听(TCP/UDP监听端口可以使用tcp_listen_port和udp_listen_port进行配置),根据INVITE消息中的ssrc来区分具体流名,详细的配置见gb28181_config 4 | 5 | # GB28181相关HTTP API 6 | 7 | 目前主要提供的API如下 8 | 9 | [/api/gb/device_infos](#apigbdevice_infos) 10 | 11 | [/api/gb/update_all_notify](#apigbupdate_all_notify) 12 | 13 | [/api/gb/update_notify](#apigbupdate_notify) 14 | 15 | [/api/gb/start_play](#apigbstart_play) 16 | 17 | [/api/gb/ptz_direction](#apigbptz_direction) 18 | 19 | [/api/gb/ptz_zoom](#apigbptz_zoom) 20 | 21 | [/api/gb/ptz_fi](#apigbptz_fi) 22 | 23 | [/api/gb/ptz_preset](#apigbptz_preset) 24 | 25 | [/api/gb/ptz_stop](#apigbptz_stop) 26 | 27 | 28 | 返回信息格式如下 29 | ``` 30 | { 31 | "code": , // 状态码 32 | "msg": , // 状态码对应的解释 33 | "data": // 具体返回信息 34 | } 35 | 36 | 其中code和msg的对应关系如下 37 | 1000: success 38 | 1001: 请求参数错误 39 | 1002: 服务繁忙 40 | 1003: 设备暂时未注册 41 | 1004: 设备停止播放错误 42 | ``` 43 | 44 | ## /api/gb/device_infos 45 | API含义: 获取注册的设备信息 46 | 47 | Method: GET 48 | 49 | data信息: 50 | ``` 51 | "data": { 52 | "device_items": [ 53 | { 54 | "device_id": , // 设备ID 55 | "channels": [ // 通道信息 56 | { 57 | "channel_id": , // 通道ID 58 | "name": , // 设备名称 59 | "manufacturer": , // 制造厂商 60 | "owner: , // 设备归属 61 | "civilCode": , // 行政区划编码 62 | "address": , // 地址 63 | "status": , // 设备状态,ON/OFF 64 | "longitude": , // 经度 65 | "latitude": // 纬度 66 | } 67 | ] 68 | } 69 | 70 | ] 71 | } 72 | ``` 73 | 74 | 示例 75 | ``` 76 | curl http://127.0.0.1:1290/api/gb/device_infos -X GET 77 | 78 | { 79 | "code":1000, 80 | "msg":"success", 81 | "data":{ 82 | "device_items":[ 83 | { 84 | "device_id":"34020000001320000001", 85 | "channels":[ 86 | { 87 | "channel_id":"34020000001320000001", 88 | "name":"Camera 01", 89 | "manufacturer":"Hikvision", 90 | "owner":"Owner", 91 | "civilCode":"3402000000", 92 | "address":"Address", 93 | "status":"ON", 94 | "longitude":"", 95 | "latitude":"" 96 | } 97 | ] 98 | } 99 | ] 100 | } 101 | } 102 | ``` 103 | 104 | 105 | ## /api/gb/update_all_notify 106 | API含义: 更新全部信息 107 | 108 | Method: POST 109 | 110 | 请求body信息: 无 111 | 112 | data信息: 无 113 | 114 | 示例: 115 | ``` 116 | curl http://127.0.0.1:1290/api/gb/update_all_notify -X POST 117 | 118 | { 119 | "code":1000, 120 | "msg":"success" 121 | } 122 | ``` 123 | 124 | ## /api/gb/update_notify 125 | API含义: 更新某个设备信息 126 | 127 | Method: POST 128 | 129 | 请求body信息 130 | ``` 131 | { 132 | "device_id": // 设备ID 133 | } 134 | ``` 135 | 136 | data信息: 无 137 | 138 | 示例: 139 | ``` 140 | curl "http://127.0.0.1:1290/api/gb/update_notify" -X POST -d '{"device_id": "34020000001320000001"}' 141 | 142 | { 143 | "code":1000, 144 | "msg":"success" 145 | } 146 | ``` 147 | 148 | ## /api/gb/start_play 149 | API含义: 播放某通道 150 | 151 | Method: POST 152 | 153 | 请求body信息: 154 | ``` 155 | { 156 | "device_id": , // 设备ID 157 | "channel_id": , // 通道ID 158 | "network": , // 传输协议类型, tcp/udp 159 | "stream_name": // 对应的流名,不指定的话就使用channel_id 160 | "single_port": // 是否单端口 161 | "dump_file_name": // dump文件路径 162 | } 163 | ``` 164 | 165 | data信息: 166 | ``` 167 | { 168 | "stream_name": // 流名 169 | } 170 | ``` 171 | 172 | 示例: 173 | ``` 174 | curl "http://127.0.0.1:1290/api/gb/start_play" -X POST -d '{"device_id": "34020000001320000001", "channel_id": "34020000001320000001", "network": "udp", "stream_name": "test001}' 175 | 176 | { 177 | "code":1000, 178 | "msg":"success" 179 | "data": { 180 | "stream_name": "test001" 181 | } 182 | } 183 | ``` 184 | 185 | ## /api/gb/stop_play 186 | 187 | API含义: 停止播放某通道 188 | 189 | Method: POST 190 | 191 | 请求body信息: 192 | ``` 193 | { 194 | "device_id": , // 设备ID 195 | "channel_id": , // 通道ID 196 | "stream_name": // ssrc对应的流名,不指定的话就使用channel_id 197 | } 198 | ``` 199 | 200 | data信息: 无 201 | 202 | 示例: 203 | ``` 204 | curl "http://127.0.0.1:1290/api/gb/stop_play" -X POST -d '{"device_id": "34020000001320000001", "channel_id": "34020000001320000001", "stream_name": "test001}' 205 | 206 | { 207 | "code":1000, 208 | "msg":"success" 209 | } 210 | ``` 211 | 212 | ## /api/gb/ptz_direction 213 | API含义: ptz 方向控制 214 | 215 | Method: POST 216 | 217 | 请求body信息: 218 | ``` 219 | { 220 | "device_id": , // 设备ID 221 | "channel_id": , // 通道ID 222 | "up": , // 上 223 | "down": // 下 224 | "left": // 左 225 | "right": // 右 226 | "speed": // 步长,1~8 227 | } 228 | ``` 229 | 230 | ## /api/gb/ptz_zoom 231 | API含义: 镜头变倍 232 | 233 | Method: POST 234 | 235 | 请求body信息: 236 | ``` 237 | { 238 | "device_id": , // 设备ID 239 | "channel_id": , // 通道ID 240 | "zoom_out": , // 缩小 241 | "zoom_in": // 放大 242 | "speed": // 步长,1~8 243 | } 244 | ``` 245 | ## /api/gb/ptz_fi 246 | API含义: 光圈控制和聚焦控制 247 | 248 | Method: POST 249 | 250 | 请求body信息: 251 | ``` 252 | { 253 | "device_id": , // 设备ID 254 | "channel_id": , // 通道ID 255 | "iris_in": , // 光圈小 256 | "iris_out": // 光圈大 257 | "focus_near": // 聚焦近 258 | "focus_far": // 聚焦远 259 | "speed": // 步长,1~8 260 | } 261 | ``` 262 | ## /api/gb/ptz_preset 263 | API含义: 预置位操作 264 | 265 | Method: POST 266 | 267 | 请求body信息: 268 | ``` 269 | { 270 | "device_id": , // 设备ID 271 | "channel_id": , // 通道ID 272 | "cmd": , // 0:添加,1:删除,2:调用 273 | "point": // 预置点 274 | } 275 | ``` 276 | ## /api/gb/ptz_stop 277 | API含义: 停止ptz 278 | 279 | Method: POST 280 | 281 | 请求body信息: 282 | ``` 283 | { 284 | "device_id": , // 设备ID 285 | "channel_id": , // 通道ID 286 | } 287 | ``` 288 | # 海康设备接入 289 | 290 | ![图片](../image/gb-hk.png) 291 | -------------------------------------------------------------------------------- /document/rtc.md: -------------------------------------------------------------------------------- 1 | # WebRTC(WHIP/WHEP) 2 | 3 | WebRTC在刚发布的时候仅仅专注于VoIP和点对点用例,它仅限于几个并发的浏览器,并且不能扩展,缺少标准信令交互,故很难用于直播场景。 4 | 5 | 在此背景下,WHIP和WHEP这2个标准的提出,补齐了信令交互这一个环节,使WebRTC可以运用在直播场景。 6 | 7 | ## WHIP(WebRTC-HTTP Ingestion Protocol) 8 | 协议链接:https://datatracker.ietf.org/doc/html/draft-murillo-whip-02 9 | 10 | WHIP(WebRTC-HTTP Ingestion Protocol)是Milicast的技术团队提出的,在与媒体服务器通信时,WHIP提供了使用标准信令协议的编码软件和硬件,这样就可以实现厂商的WebRTC推流。WHIP在WebRTC上增加了一个简单的信令层,可用于将WebRTC发布者连接到WebRTC媒体服务器,发布者只发送媒体而不接收媒体。 11 | 12 | 13 | ### 交互流程 14 | ``` 15 | +-------------+ +---------------+ +--------------+ +---------------+ 16 | | WHIP client | | WHIP endpoint | | Media Server | | WHIP Resource | 17 | +--+----------+ +---------+-----+ +------+-------+ +--------|------+ 18 | | | | | 19 | | | | | 20 | |HTTP POST (SDP Offer) | | | 21 | +------------------------>+ | | 22 | |201 Created (SDP answer) | | | 23 | +<------------------------+ | | 24 | | ICE REQUEST | | 25 | +--------------------------------------->+ | 26 | | ICE RESPONSE | | 27 | |<---------------------------------------+ | 28 | | DTLS SETUP | | 29 | |<======================================>| | 30 | | RTP/RTCP FLOW | | 31 | +<-------------------------------------->+ | 32 | | HTTP DELETE | 33 | +---------------------------------------------------------->+ 34 | | 200 OK | 35 | <-----------------------------------------------------------x 36 | ``` 37 | 38 | (1)WHIP client使用HTTP POST请求执行单次SDP Offer/Answer,以便在编码器/媒体生产者(WHIP客户端)和广播接收端点(媒体服务器)之间建立ICE/DTLS会话。 39 | 40 | (2)一旦ICE/DTLS会话建立,媒体将从编码器/媒体生成器(WHIP客户端)单向流向广播接收端点(媒体服务器)。为了降低复杂性,不支持SDP重新协商,因此在完成通过HTTP的初始SDP Offer/Answer后,不能添加或删除任何track或stream。 41 | 42 | (3)HTTP POST请求的内容类型为“application/sdp”,并包含作为主体的SDP Offer。WHIP端点将生成一个SDP Answer并返回一个“201 Created”响应,内容类型为“application/SDP”。 43 | 44 | ## WHEP(WebRTC-HTTP Egress Protocol) 45 | 协议链接:https://datatracker.ietf.org/doc/html/draft-murillo-whep-02 46 | 47 | WHEP(WebRTC-HTTP Egress Protocol)也是在WebRTC上增加了一个简单的信令层,可用于将WebRTC播放者连接到WebRTC媒体服务器,播放者只接收媒体,不发送媒体。 48 | 49 | ``` 50 | +-------------+ +---------------+ +--------------+ +---------------+ 51 | | WHEP Player | | WHEP endpoint | | Media Server | | WHEP Resource | 52 | +--+----------+ +---------+-----+ +------+-------+ +--------|------+ 53 | | | | | 54 | | | | | 55 | |HTTP POST (SDP Offer) | | | 56 | +------------------------>+ | | 57 | |201 Created (SDP answer) | | | 58 | +<------------------------+ | | 59 | | ICE REQUEST | | 60 | +--------------------------------------->+ | 61 | | ICE RESPONSE | | 62 | |<---------------------------------------+ | 63 | | DTLS SETUP | | 64 | |<======================================>| | 65 | | RTP/RTCP FLOW | | 66 | +<-------------------------------------->+ | 67 | | HTTP DELETE | 68 | +---------------------------------------------------------->+ 69 | | 200 OK | 70 | <-----------------------------------------------------------x 71 | ``` 72 | 73 | (1)WHEP Player使用HTTP POST请求执行单次SDP Offer/Answer,以便在WHEP Player和媒体服务器之间建立ICE/DTLS会话。 74 | 75 | (2)一旦ICE/DTLS会话建立,媒体将从媒体服务器流向WHEP Player。为了降低复杂性,不支持SDP重新协商,因此在完成通过HTTP的初始SDP Offer/Answer后,不能添加或删除任何track或stream。 76 | 77 | (3)HTTP POST请求的内容类型为“application/sdp”,并包含作为主体的SDP Offer。WHEP端点将生成一个SDP Answer并返回一个“201 Created”响应,内容类型为“application/SDP”。 78 | 79 | ## lalmax RTC 80 | lalmax支持WHIP推流和WHEP拉流 81 | 82 | 视频:H264 83 | 84 | 音频:G711A/G711U 85 | 86 | WHIP可以使用[vue-wish](https://github.com/zllovesuki/vue-wish)、[OBS](https://github.com/obsproject/obs-studio/actions/runs/5227109208?pr=7926)测试 87 | 88 | WHEP拉流可以使用[vue-wish](https://github.com/zllovesuki/vue-wish)测试 89 | 90 | ### OBS测试效果 91 | 使用OBS进行whip推流到lalmax中,并用vue-wish拉流,测试延时可以做到200ms以内 92 | 93 | OBS推流配置 94 | 95 | ![图片](../image/rtc_01.jpeg) 96 | 97 | vue-wish拉流效果 98 | 99 | ![图片](../image/rtc_02.png) 100 | 101 | -------------------------------------------------------------------------------- /document/srt.md: -------------------------------------------------------------------------------- 1 | # SRT 2 | 3 | SRT(Secure Reliable Transport)的简称,主要优化在不可靠网络(非阻塞导致的丢包)环境下实时音视频的传输性能 4 | 5 | ## 特点 6 | (1) 基于UDP的用户态协议栈 7 | 8 | (2) 抗丢包能力强&低延时 9 | 10 | (3) 传输负载无关 11 | 12 | (4) 传输加密 13 | 14 | ## 应用场景 15 | (1) 上行最后一公里推流加速 16 | 17 | (2) CDN内部传输分发加速 18 | 19 | (3) 丢包重传率比较高的场景 20 | 21 | ## 支持的流媒体服务和工具 22 | (1) OBS 23 | 24 | (2) VLC 25 | 26 | (3) FFmpeg,编译需集成libsrt 27 | 28 | (4) SRS 29 | 30 | (5) ZLMediaKit 31 | 32 | (6) LALMax 33 | 34 | ## 测试 35 | (1) 启动LalMax服务 36 | 37 | (2) 使用OBS进行推流,在"直播"中输入srt的推流地址 38 | ![图片](../image/srt_0.png) 39 | 40 | (3) VLC进行播放 41 | 42 | 在VLC中设置streamid,这部分填streamid后面的所有信息 43 | ![图片](../image/srt_1.png) 44 | 45 | 输入streamid前面的部分进行拉流 46 | ![图片](../image/srt_2.png) 47 | 48 | ![图片](../image/srt_3.png) -------------------------------------------------------------------------------- /fmp4/hls/server.go: -------------------------------------------------------------------------------- 1 | package hls 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | config "github.com/q191201771/lalmax/conf" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/q191201771/lal/pkg/base" 11 | "github.com/q191201771/naza/pkg/nazalog" 12 | ) 13 | 14 | type HlsServer struct { 15 | sessions sync.Map 16 | conf config.HlsConfig 17 | invalidSessions sync.Map 18 | } 19 | 20 | func NewHlsServer(conf config.HlsConfig) *HlsServer { 21 | svr := &HlsServer{ 22 | conf: conf, 23 | } 24 | 25 | go svr.cleanInvalidSession() 26 | 27 | return svr 28 | } 29 | 30 | func (s *HlsServer) NewHlsSession(streamName string) { 31 | nazalog.Info("new hls session, streamName:", streamName) 32 | session := NewHlsSession(streamName, s.conf) 33 | s.sessions.Store(streamName, session) 34 | } 35 | 36 | func (s *HlsServer) OnMsg(streamName string, msg base.RtmpMsg) { 37 | value, ok := s.sessions.Load(streamName) 38 | if ok { 39 | session := value.(*HlsSession) 40 | session.OnMsg(msg) 41 | } 42 | } 43 | 44 | func (s *HlsServer) OnStop(streamName string) { 45 | value, ok := s.sessions.Load(streamName) 46 | if ok { 47 | session := value.(*HlsSession) 48 | s.invalidSessions.Store(session.SessionId, session) 49 | s.sessions.Delete(streamName) 50 | } 51 | } 52 | 53 | func (s *HlsServer) HandleRequest(ctx *gin.Context) { 54 | streamName := ctx.Param("streamid") 55 | value, ok := s.sessions.Load(streamName) 56 | if ok { 57 | session := value.(*HlsSession) 58 | session.HandleRequest(ctx) 59 | } 60 | } 61 | 62 | func (s *HlsServer) cleanInvalidSession() { 63 | ticker := time.NewTicker(30 * time.Second) 64 | defer ticker.Stop() 65 | for range ticker.C { 66 | s.invalidSessions.Range(func(k, v interface{}) bool { 67 | session := v.(*HlsSession) 68 | nazalog.Info("clean invalid session, streamName:", session.streamName, " sessionId:", k) 69 | session.OnStop() 70 | s.invalidSessions.Delete(k) 71 | return true 72 | }) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /fmp4/http-fmp4/server.go: -------------------------------------------------------------------------------- 1 | package httpfmp4 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | type HttpFmp4Server struct { 8 | } 9 | 10 | func NewHttpFmp4Server() *HttpFmp4Server { 11 | svr := &HttpFmp4Server{} 12 | 13 | return svr 14 | } 15 | 16 | func (s *HttpFmp4Server) HandleRequest(c *gin.Context) { 17 | streamid := c.Param("streamid") 18 | 19 | session := NewHttpFmp4Session(streamid) 20 | session.handleSession(c) 21 | } 22 | -------------------------------------------------------------------------------- /gb28181/auth.go: -------------------------------------------------------------------------------- 1 | package gb28181 2 | 3 | import ( 4 | "crypto/md5" 5 | "fmt" 6 | 7 | "github.com/ghettovoice/gosip/sip" 8 | "github.com/q191201771/naza/pkg/nazalog" 9 | ) 10 | 11 | type Authorization struct { 12 | *sip.Authorization 13 | } 14 | 15 | func (a *Authorization) Verify(username, passwd, realm, nonce string) bool { 16 | 17 | //1、将 username,realm,password 依次组合获取 1 个字符串,并用算法加密的到密文 r1 18 | s1 := fmt.Sprintf("%s:%s:%s", username, realm, passwd) 19 | r1 := a.getDigest(s1) 20 | //2、将 method,即REGISTER ,uri 依次组合获取 1 个字符串,并对这个字符串使用算法 加密得到密文 r2 21 | s2 := fmt.Sprintf("REGISTER:%s", a.Uri()) 22 | r2 := a.getDigest(s2) 23 | 24 | if r1 == "" || r2 == "" { 25 | nazalog.Error("Authorization algorithm wrong") 26 | return false 27 | } 28 | //3、将密文 1,nonce 和密文 2 依次组合获取 1 个字符串,并对这个字符串使用算法加密,获得密文 r3,即Response 29 | s3 := fmt.Sprintf("%s:%s:%s", r1, nonce, r2) 30 | r3 := a.getDigest(s3) 31 | 32 | //4、计算服务端和客户端上报的是否相等 33 | return r3 == a.Response() 34 | } 35 | 36 | func (a *Authorization) getDigest(raw string) string { 37 | switch a.Algorithm() { 38 | case "MD5": 39 | return fmt.Sprintf("%x", md5.Sum([]byte(raw))) 40 | default: //如果没有算法,默认使用MD5 41 | return fmt.Sprintf("%x", md5.Sum([]byte(raw))) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /gb28181/avail_conn_pool.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020, Chef. All rights reserved. 2 | // https://github.com/q191201771/naza 3 | // 4 | // Use of this source code is governed by a MIT-style license 5 | // that can be found in the License file. 6 | // 7 | // Author: Chef (191201771@qq.com) 8 | //根据naza修改,新增tcp 9 | 10 | package gb28181 11 | 12 | import ( 13 | "errors" 14 | "net" 15 | "sync" 16 | ) 17 | 18 | var ErrNazaNet = errors.New("gb28181: fxxk") 19 | 20 | type OnListenWithPort func(port uint16) (net.Listener, error) 21 | 22 | // 从指定的端口范围内,寻找可绑定监听的端口,绑定监听并返回 23 | type AvailConnPool struct { 24 | minPort uint16 25 | maxPort uint16 26 | 27 | m sync.Mutex 28 | lastPort uint16 29 | onListenWithPort OnListenWithPort 30 | } 31 | 32 | func NewAvailConnPool(minPort uint16, maxPort uint16) *AvailConnPool { 33 | return &AvailConnPool{ 34 | minPort: minPort, 35 | maxPort: maxPort, 36 | lastPort: minPort, 37 | } 38 | } 39 | func (a *AvailConnPool) WithListenWithPort(listenWithPort OnListenWithPort) { 40 | a.onListenWithPort = listenWithPort 41 | } 42 | func (a *AvailConnPool) Acquire() (net.Listener, uint16, error) { 43 | a.m.Lock() 44 | defer a.m.Unlock() 45 | 46 | loopFirstFlag := true 47 | p := a.lastPort 48 | for { 49 | // 找了一轮也没有可用的,返回错误 50 | if !loopFirstFlag && p == a.lastPort { 51 | return nil, 0, ErrNazaNet 52 | } 53 | loopFirstFlag = false 54 | if a.onListenWithPort == nil { 55 | return nil, 0, ErrNazaNet 56 | } 57 | listener, err := a.onListenWithPort(p) 58 | 59 | // 绑定失败,尝试下一个端口 60 | if err != nil { 61 | p = a.nextPort(p) 62 | continue 63 | } 64 | 65 | // 绑定成功,更新last,返回结果 66 | a.lastPort = a.nextPort(p) 67 | return listener, p, nil 68 | } 69 | } 70 | 71 | // 通过Acquire获取到可用net.UDPConn对象后,将对象关闭,只返回可用的端口 72 | func (a *AvailConnPool) Peek() (uint16, error) { 73 | conn, port, err := a.Acquire() 74 | if err == nil { 75 | err = conn.Close() 76 | } 77 | return port, err 78 | } 79 | func (a *AvailConnPool) ListenWithPort(port uint16) (net.Listener, error) { 80 | if a.onListenWithPort == nil { 81 | return nil, ErrNazaNet 82 | } 83 | return a.onListenWithPort(port) 84 | } 85 | func (a *AvailConnPool) nextPort(p uint16) uint16 { 86 | if p == a.maxPort { 87 | return a.minPort 88 | } 89 | 90 | return p + 1 91 | } 92 | -------------------------------------------------------------------------------- /gb28181/http_logic.go: -------------------------------------------------------------------------------- 1 | package gb28181 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type GbLogic struct { 10 | s *GB28181Server 11 | } 12 | 13 | var gbLogic *GbLogic 14 | var once sync.Once 15 | 16 | func NewGbLogic(s *GB28181Server) *GbLogic { 17 | once.Do(func() { 18 | gbLogic = &GbLogic{ 19 | s: s, 20 | } 21 | }) 22 | return gbLogic 23 | } 24 | 25 | func (g *GbLogic) GetDeviceInfos(c *gin.Context) { 26 | deviceInfos := g.s.getDeviceInfos() 27 | ResponseSuccess(c, deviceInfos) 28 | } 29 | 30 | func (g *GbLogic) StartPlay(c *gin.Context) { 31 | var reqPlay ReqPlay 32 | if err := c.ShouldBindJSON(&reqPlay); err != nil { 33 | ResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg()) 34 | } else { 35 | ch := g.s.FindChannel(reqPlay.DeviceId, reqPlay.ChannelId) 36 | if ch == nil { 37 | ResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg()) 38 | } else { 39 | streamName := reqPlay.StreamName 40 | if len(streamName) == 0 { 41 | streamName = reqPlay.ChannelId 42 | } 43 | if len(reqPlay.NetWork) == 0 || !(reqPlay.NetWork == "udp" || reqPlay.NetWork == "tcp") { 44 | reqPlay.NetWork = "udp" 45 | } 46 | 47 | ch.TryAutoInvite(&InviteOptions{}, streamName, &reqPlay.PlayInfo) 48 | respPlay := &RespPlay{ 49 | StreamName: streamName, 50 | } 51 | ResponseSuccess(c, respPlay) 52 | } 53 | } 54 | 55 | } 56 | func (g *GbLogic) StopPlay(c *gin.Context) { 57 | var reqStop ReqStop 58 | if err := c.ShouldBindJSON(&reqStop); err != nil { 59 | ResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg()) 60 | } else { 61 | ch := g.s.FindChannel(reqStop.DeviceId, reqStop.ChannelId) 62 | if ch == nil { 63 | ResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg()) 64 | } else { 65 | streamName := reqStop.StreamName 66 | if len(streamName) == 0 { 67 | streamName = reqStop.ChannelId 68 | } 69 | if err = ch.Bye(streamName); err != nil { 70 | ResponseErrorWithMsg(c, CodeDeviceStopError, err.Error()) 71 | } else { 72 | ResponseSuccess(c, nil) 73 | } 74 | } 75 | } 76 | } 77 | func (g *GbLogic) PtzDirection(c *gin.Context) { 78 | var reqDirection PtzDirection 79 | if err := c.ShouldBindJSON(&reqDirection); err != nil { 80 | ResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg()) 81 | } else { 82 | if !(reqDirection.Speed > 0 && reqDirection.Speed <= 8) { 83 | ResponseErrorWithMsg(c, CodeInvalidParam, SpeedParamError) 84 | } 85 | ch := g.s.FindChannel(reqDirection.DeviceId, reqDirection.ChannelId) 86 | if ch == nil { 87 | ResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg()) 88 | } else { 89 | reqDirection.Speed = reqDirection.Speed * 25 90 | if err = ch.PtzDirection(&reqDirection); err != nil { 91 | ResponseErrorWithMsg(c, CodeDeviceStopError, err.Error()) 92 | } else { 93 | ResponseSuccess(c, nil) 94 | } 95 | } 96 | } 97 | } 98 | func (g *GbLogic) PtzZoom(c *gin.Context) { 99 | var reqZoom PtzZoom 100 | if err := c.ShouldBindJSON(&reqZoom); err != nil { 101 | ResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg()) 102 | } else { 103 | if !(reqZoom.Speed > 0 && reqZoom.Speed <= 8) { 104 | ResponseErrorWithMsg(c, CodeInvalidParam, SpeedParamError) 105 | } 106 | ch := g.s.FindChannel(reqZoom.DeviceId, reqZoom.ChannelId) 107 | if ch == nil { 108 | ResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg()) 109 | } else { 110 | reqZoom.Speed = reqZoom.Speed * 25 111 | if err = ch.PtzZoom(&reqZoom); err != nil { 112 | ResponseErrorWithMsg(c, CodeDeviceStopError, err.Error()) 113 | } else { 114 | ResponseSuccess(c, nil) 115 | } 116 | } 117 | } 118 | } 119 | func (g *GbLogic) PtzFi(c *gin.Context) { 120 | var reqFi PtzFi 121 | if err := c.ShouldBindJSON(&reqFi); err != nil { 122 | ResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg()) 123 | } else { 124 | if !(reqFi.Speed > 0 && reqFi.Speed <= 8) { 125 | ResponseErrorWithMsg(c, CodeInvalidParam, SpeedParamError) 126 | } 127 | ch := g.s.FindChannel(reqFi.DeviceId, reqFi.ChannelId) 128 | if ch == nil { 129 | ResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg()) 130 | } else { 131 | reqFi.Speed = reqFi.Speed * 25 132 | if err = ch.PtzFi(&reqFi); err != nil { 133 | ResponseErrorWithMsg(c, CodeDeviceStopError, err.Error()) 134 | } else { 135 | ResponseSuccess(c, nil) 136 | } 137 | } 138 | } 139 | } 140 | func (g *GbLogic) PtzPreset(c *gin.Context) { 141 | var reqPreset PtzPreset 142 | if err := c.ShouldBindJSON(&reqPreset); err != nil { 143 | ResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg()) 144 | } else { 145 | if !(reqPreset.Point > 0 && reqPreset.Point <= 50) { 146 | ResponseErrorWithMsg(c, CodeInvalidParam, PointParamError) 147 | } 148 | ch := g.s.FindChannel(reqPreset.DeviceId, reqPreset.ChannelId) 149 | if ch == nil { 150 | ResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg()) 151 | } else { 152 | if err = ch.PtzPreset(&reqPreset); err != nil { 153 | ResponseErrorWithMsg(c, CodeDeviceStopError, err.Error()) 154 | } else { 155 | ResponseSuccess(c, nil) 156 | } 157 | } 158 | } 159 | } 160 | func (g *GbLogic) PtzStop(c *gin.Context) { 161 | var reqStop PtzStop 162 | if err := c.ShouldBindJSON(&reqStop); err != nil { 163 | ResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg()) 164 | } else { 165 | ch := g.s.FindChannel(reqStop.DeviceId, reqStop.ChannelId) 166 | if ch == nil { 167 | ResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg()) 168 | } else { 169 | if err = ch.PtzStop(&reqStop); err != nil { 170 | ResponseErrorWithMsg(c, CodeDeviceStopError, err.Error()) 171 | } else { 172 | ResponseSuccess(c, nil) 173 | } 174 | } 175 | } 176 | } 177 | func (g *GbLogic) UpdateAllNotify(c *gin.Context) { 178 | g.s.GetAllSyncChannels() 179 | ResponseSuccess(c, nil) 180 | } 181 | func (g *GbLogic) UpdateNotify(c *gin.Context) { 182 | var reqUpdateNotify ReqUpdateNotify 183 | if err := c.ShouldBindJSON(&reqUpdateNotify); err != nil { 184 | ResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg()) 185 | } else { 186 | if g.s.GetSyncChannels(reqUpdateNotify.DeviceId) { 187 | ResponseSuccess(c, nil) 188 | } else { 189 | ResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg()) 190 | } 191 | 192 | } 193 | 194 | } 195 | -------------------------------------------------------------------------------- /gb28181/inviteoption.go: -------------------------------------------------------------------------------- 1 | package gb28181 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | type InviteOptions struct { 9 | Start int 10 | End int 11 | ssrc string 12 | SSRC uint32 13 | MediaPort uint16 14 | } 15 | 16 | func (o InviteOptions) IsLive() bool { 17 | return o.Start == 0 || o.End == 0 18 | } 19 | 20 | func (o InviteOptions) String() string { 21 | return fmt.Sprintf("t=%d %d", o.Start, o.End) 22 | } 23 | 24 | func (o *InviteOptions) CreateSSRC(serial string, number uint16) { 25 | //不按gb生成标准,取ID最后六位,然后按顺序生成,一个channel最大999 26 | o.ssrc = fmt.Sprintf("%d%s%03d", 0, serial, number) 27 | _ssrc, _ := strconv.ParseInt(o.ssrc, 10, 0) 28 | o.SSRC = uint32(_ssrc) 29 | } 30 | -------------------------------------------------------------------------------- /gb28181/mediaserver/conn.go: -------------------------------------------------------------------------------- 1 | package mediaserver 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net" 10 | "sync" 11 | "time" 12 | 13 | "github.com/q191201771/lalmax/gb28181/mpegps" 14 | 15 | "github.com/pion/rtp" 16 | "github.com/q191201771/lal/pkg/base" 17 | "github.com/q191201771/lal/pkg/logic" 18 | "github.com/q191201771/naza/pkg/nazalog" 19 | ) 20 | 21 | var ( 22 | ErrInvalidPsData = errors.New("invalid mpegps data") 23 | ) 24 | 25 | type Frame struct { 26 | buffer *bytes.Buffer 27 | pts uint64 28 | dts uint64 29 | initPts uint64 30 | initDts uint64 31 | } 32 | 33 | type Conn struct { 34 | conn net.Conn 35 | r io.Reader 36 | check bool 37 | demuxer *mpegps.PsDemuxer 38 | streamName string 39 | lalServer logic.ILalServer 40 | lalSession logic.ICustomizePubSessionContext 41 | videoFrame Frame 42 | audioFrame Frame 43 | 44 | observer IGbObserver 45 | 46 | rtpPts uint64 47 | psPtsZeroTimes int64 48 | 49 | psDumpFile *base.DumpFile 50 | 51 | buffer *bytes.Buffer 52 | key string 53 | 54 | mediaServer *GB28181MediaServer 55 | one sync.Once 56 | oneSaveConn sync.Once 57 | } 58 | 59 | func NewConn(conn net.Conn, observer IGbObserver, lal logic.ILalServer) *Conn { 60 | c := &Conn{ 61 | conn: conn, 62 | r: conn, 63 | demuxer: mpegps.NewPsDemuxer(), 64 | observer: observer, 65 | lalServer: lal, 66 | buffer: bytes.NewBuffer(nil), 67 | } 68 | 69 | c.demuxer.OnFrame = c.OnFrame 70 | 71 | return c 72 | } 73 | func (c *Conn) SetMediaServer(mediaServer *GB28181MediaServer) { 74 | c.mediaServer = mediaServer 75 | } 76 | func (c *Conn) SetKey(key string) { 77 | c.key = key 78 | } 79 | func (c *Conn) Serve() (err error) { 80 | defer func() { 81 | nazalog.Info("conn close, err:", err) 82 | c.Close() 83 | 84 | if c.observer != nil { 85 | c.observer.NotifyClose(c.streamName) 86 | } 87 | if c.psDumpFile != nil { 88 | c.psDumpFile.Close() 89 | } 90 | if c.lalSession != nil { 91 | c.lalServer.DelCustomizePubSession(c.lalSession) 92 | } 93 | }() 94 | 95 | nazalog.Info("gb28181 conn, remoteaddr:", c.conn.RemoteAddr().String(), " localaddr:", c.conn.LocalAddr().String()) 96 | 97 | for { 98 | c.conn.SetReadDeadline(time.Now().Add(10 * time.Second)) 99 | pkt := &rtp.Packet{} 100 | if c.conn.RemoteAddr().Network() == "udp" { 101 | buf := make([]byte, 1472*4) 102 | n, err := c.conn.Read(buf) 103 | if err != nil { 104 | nazalog.Error("conn read failed, err:", err) 105 | return err 106 | } 107 | 108 | err = pkt.Unmarshal(buf[:n]) 109 | if err != nil { 110 | return err 111 | } 112 | } else { 113 | len := make([]byte, 2) 114 | _, err := io.ReadFull(c.r, len) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | size := binary.BigEndian.Uint16(len) 120 | buf := make([]byte, size) 121 | _, err = io.ReadFull(c.r, buf) 122 | if err != nil { 123 | return err 124 | } 125 | 126 | err = pkt.Unmarshal(buf) 127 | if err != nil { 128 | return err 129 | } 130 | } 131 | 132 | if !c.check && c.observer != nil { 133 | var mediaInfo *MediaInfo 134 | var ok bool 135 | if pkt.SSRC != 0 { 136 | mediaInfo, ok = c.observer.CheckSsrc(pkt.SSRC) 137 | if !ok { 138 | nazalog.Error("invalid ssrc:", pkt.SSRC) 139 | return fmt.Errorf("invalid ssrc:%d", pkt.SSRC) 140 | } 141 | } else { 142 | mediaInfo, ok = c.observer.GetMediaInfoByKey(c.key) 143 | if !ok { 144 | nazalog.Error("get mediaInfo :", c.key) 145 | return fmt.Errorf("get mediaInfo:%d", c.key) 146 | } 147 | } 148 | c.check = true 149 | c.streamName = mediaInfo.StreamName 150 | c.oneSaveConn.Do(func() { 151 | if c.mediaServer != nil { 152 | c.mediaServer.conns.Store(c.streamName, c) 153 | } 154 | }) 155 | if len(mediaInfo.DumpFileName) > 0 { 156 | c.psDumpFile = base.NewDumpFile() 157 | if err = c.psDumpFile.OpenToWrite(mediaInfo.DumpFileName); err != nil { 158 | nazalog.Errorf("gb con dump file:%s", err.Error()) 159 | } 160 | } 161 | nazalog.Info("gb28181 ssrc check success, streamName:", c.streamName) 162 | 163 | session, err := c.lalServer.AddCustomizePubSession(mediaInfo.StreamName) 164 | if err != nil { 165 | nazalog.Error("lal server AddCustomizePubSession failed, err:", err) 166 | return err 167 | } 168 | 169 | session.WithOption(func(option *base.AvPacketStreamOption) { 170 | option.VideoFormat = base.AvPacketStreamVideoFormatAnnexb 171 | option.AudioFormat = base.AvPacketStreamAudioFormatAdtsAac 172 | }) 173 | 174 | c.lalSession = session 175 | } 176 | c.rtpPts = uint64(pkt.Header.Timestamp) 177 | if c.demuxer != nil { 178 | if c.psDumpFile != nil { 179 | c.psDumpFile.WriteWithType(pkt.Payload, base.DumpTypePsRtpData) 180 | } 181 | c.demuxer.Input(pkt.Payload) 182 | } 183 | } 184 | return 185 | } 186 | 187 | func (c *Conn) Demuxer(data []byte) error { 188 | c.buffer.Write(data) 189 | 190 | buf := c.buffer.Bytes() 191 | if len(buf) < 4 { 192 | return nil 193 | } 194 | 195 | if buf[0] != 0x00 && buf[1] != 0x00 && buf[2] != 0x01 && buf[3] != 0xBA { 196 | return ErrInvalidPsData 197 | } 198 | 199 | packets := splitPsPackets(buf) 200 | if len(packets) <= 1 { 201 | return nil 202 | } 203 | 204 | for i, packet := range packets { 205 | if i == len(packets)-1 { 206 | c.buffer = bytes.NewBuffer(packet) 207 | return nil 208 | } 209 | 210 | if c.demuxer != nil { 211 | c.demuxer.Input(packet) 212 | } 213 | } 214 | 215 | return nil 216 | } 217 | 218 | func (c *Conn) OnFrame(frame []byte, cid mpegps.PsStreamType, pts uint64, dts uint64) { 219 | payloadType := getPayloadType(cid) 220 | if payloadType == base.AvPacketPtUnknown { 221 | return 222 | } 223 | //当ps流解析出pts为0时,计数超过10则用rtp的时间戳 224 | if pts == 0 { 225 | if c.psPtsZeroTimes >= 0 { 226 | c.psPtsZeroTimes++ 227 | } 228 | if c.psPtsZeroTimes > 10 { 229 | pts = c.rtpPts 230 | dts = c.rtpPts 231 | } 232 | } else { 233 | c.psPtsZeroTimes = -1 234 | } 235 | if payloadType == base.AvPacketPtAac || payloadType == base.AvPacketPtG711A || payloadType == base.AvPacketPtG711U { 236 | if c.audioFrame.initDts == 0 { 237 | c.audioFrame.initDts = dts 238 | } 239 | 240 | if c.audioFrame.initPts == 0 { 241 | c.audioFrame.initPts = pts 242 | } 243 | 244 | var pkt base.AvPacket 245 | pkt.PayloadType = payloadType 246 | pkt.Timestamp = int64(dts - c.audioFrame.initDts) 247 | pkt.Pts = int64(pts - c.audioFrame.initPts) 248 | pkt.Payload = append(pkt.Payload, frame...) 249 | c.lalSession.FeedAvPacket(pkt) 250 | 251 | } else { 252 | if c.videoFrame.initPts == 0 { 253 | c.videoFrame.initPts = pts 254 | } 255 | 256 | if c.videoFrame.initDts == 0 { 257 | c.videoFrame.initDts = dts 258 | } 259 | 260 | // 塞入lal中 261 | c.videoFrame.pts = pts - c.videoFrame.initPts 262 | c.videoFrame.dts = dts - c.videoFrame.initDts 263 | var pkt base.AvPacket 264 | pkt.PayloadType = payloadType 265 | pkt.Timestamp = int64(c.videoFrame.dts) 266 | pkt.Pts = int64(c.videoFrame.pts) 267 | pkt.Payload = frame 268 | c.lalSession.FeedAvPacket(pkt) 269 | } 270 | } 271 | func (c *Conn) Close() { 272 | c.one.Do(func() { 273 | c.conn.Close() 274 | }) 275 | } 276 | func getPayloadType(cid mpegps.PsStreamType) base.AvPacketPt { 277 | switch cid { 278 | case mpegps.PsStreamAac: 279 | return base.AvPacketPtAac 280 | case mpegps.PsStreamG711A: 281 | return base.AvPacketPtG711A 282 | case mpegps.PsStreamG711U: 283 | return base.AvPacketPtG711U 284 | case mpegps.PsStreamH264: 285 | return base.AvPacketPtAvc 286 | case mpegps.PsStreamH265: 287 | return base.AvPacketPtHevc 288 | } 289 | 290 | return base.AvPacketPtUnknown 291 | } 292 | 293 | func splitPsPackets(data []byte) [][]byte { 294 | startCode := []byte{0x00, 0x00, 0x01, 0xBA} 295 | start := 0 296 | var packets [][]byte 297 | for i := 0; i < len(data); i++ { 298 | if i+len(startCode) <= len(data) && bytes.Equal(data[i:i+len(startCode)], startCode) { 299 | if i == 0 { 300 | continue 301 | } 302 | packets = append(packets, data[start:i]) 303 | start = i 304 | } 305 | } 306 | packets = append(packets, data[start:]) 307 | 308 | return packets 309 | } 310 | -------------------------------------------------------------------------------- /gb28181/mediaserver/mediaserver_t.go: -------------------------------------------------------------------------------- 1 | package mediaserver 2 | 3 | type MediaInfo struct { 4 | IsInvite bool 5 | Ssrc uint32 6 | StreamName string 7 | SinglePort bool 8 | DumpFileName string 9 | MediaKey string 10 | } 11 | 12 | func (m *MediaInfo) Clear() (err error) { 13 | m.IsInvite = false 14 | m.Ssrc = 0 15 | m.StreamName = "" 16 | m.SinglePort = false 17 | m.DumpFileName = "" 18 | 19 | return 20 | } 21 | -------------------------------------------------------------------------------- /gb28181/mediaserver/server.go: -------------------------------------------------------------------------------- 1 | package mediaserver 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "sync" 7 | "time" 8 | 9 | "github.com/q191201771/lal/pkg/logic" 10 | "github.com/q191201771/naza/pkg/nazalog" 11 | ) 12 | 13 | type IGbObserver interface { 14 | CheckSsrc(ssrc uint32) (*MediaInfo, bool) 15 | GetMediaInfoByKey(key string) (*MediaInfo, bool) 16 | NotifyClose(streamName string) 17 | } 18 | 19 | type GB28181MediaServer struct { 20 | listenPort int 21 | lalServer logic.ILalServer 22 | 23 | listener net.Listener 24 | 25 | disposeOnce sync.Once 26 | observer IGbObserver 27 | mediaKey string 28 | 29 | conns sync.Map //增加链接对象,目前只适用于多端口 30 | } 31 | 32 | func NewGB28181MediaServer(listenPort int, mediaKey string, observer IGbObserver, lal logic.ILalServer) *GB28181MediaServer { 33 | return &GB28181MediaServer{ 34 | listenPort: listenPort, 35 | lalServer: lal, 36 | observer: observer, 37 | mediaKey: mediaKey, 38 | } 39 | } 40 | func (s *GB28181MediaServer) GetListenerPort() uint16 { 41 | return uint16(s.listenPort) 42 | } 43 | func (s *GB28181MediaServer) Start(listener net.Listener) (err error) { 44 | s.listener = listener 45 | if s.listener != nil { 46 | go func() { 47 | for { 48 | if s.listener == nil { 49 | return 50 | } 51 | conn, err := s.listener.Accept() 52 | if err != nil { 53 | var ne net.Error 54 | if ok := errors.As(err, &ne); ok && ne.Timeout() { 55 | nazalog.Error("Accept failed: timeout error, retrying...") 56 | time.Sleep(time.Second / 20) 57 | } else { 58 | break 59 | } 60 | } 61 | 62 | c := NewConn(conn, s.observer, s.lalServer) 63 | c.SetKey(s.mediaKey) 64 | c.SetMediaServer(s) 65 | go func() { 66 | c.Serve() 67 | s.conns.Delete(c.streamName) 68 | }() 69 | } 70 | }() 71 | } 72 | return 73 | } 74 | func (s *GB28181MediaServer) CloseConn(streamName string) { 75 | if v, ok := s.conns.Load(streamName); ok { 76 | conn := v.(*Conn) 77 | conn.Close() 78 | } 79 | } 80 | func (s *GB28181MediaServer) Dispose() { 81 | s.disposeOnce.Do(func() { 82 | s.conns.Range(func(_, value any) bool { 83 | conn := value.(*Conn) 84 | conn.Close() 85 | return true 86 | }) 87 | if s.listener != nil { 88 | s.listener.Close() 89 | s.listener = nil 90 | } 91 | }) 92 | } 93 | -------------------------------------------------------------------------------- /gb28181/mpegps/bitstream.go: -------------------------------------------------------------------------------- 1 | package mpegps 2 | 3 | import ( 4 | "encoding/binary" 5 | ) 6 | 7 | var BitMask [8]byte = [8]byte{0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF} 8 | 9 | type BitStream struct { 10 | bits []byte 11 | bytesOffset int 12 | bitsOffset int 13 | bitsmark int 14 | bytemark int 15 | } 16 | 17 | func NewBitStream(buf []byte) *BitStream { 18 | return &BitStream{ 19 | bits: buf, 20 | bytesOffset: 0, 21 | bitsOffset: 0, 22 | bitsmark: 0, 23 | bytemark: 0, 24 | } 25 | } 26 | 27 | func (bs *BitStream) Uint8(n int) uint8 { 28 | return uint8(bs.GetBits(n)) 29 | } 30 | 31 | func (bs *BitStream) Uint16(n int) uint16 { 32 | return uint16(bs.GetBits(n)) 33 | } 34 | 35 | func (bs *BitStream) Uint32(n int) uint32 { 36 | return uint32(bs.GetBits(n)) 37 | } 38 | 39 | func (bs *BitStream) GetBytes(n int) []byte { 40 | if bs.bytesOffset+n > len(bs.bits) { 41 | panic("OUT OF RANGE") 42 | } 43 | if bs.bitsOffset != 0 { 44 | panic("invaild operation") 45 | } 46 | data := make([]byte, n) 47 | copy(data, bs.bits[bs.bytesOffset:bs.bytesOffset+n]) 48 | bs.bytesOffset += n 49 | return data 50 | } 51 | 52 | // n <= 64 53 | func (bs *BitStream) GetBits(n int) uint64 { 54 | if bs.bytesOffset >= len(bs.bits) { 55 | panic("OUT OF RANGE") 56 | } 57 | var ret uint64 = 0 58 | if 8-bs.bitsOffset >= n { 59 | ret = uint64((bs.bits[bs.bytesOffset] >> (8 - bs.bitsOffset - n)) & BitMask[n-1]) 60 | bs.bitsOffset += n 61 | if bs.bitsOffset == 8 { 62 | bs.bytesOffset++ 63 | bs.bitsOffset = 0 64 | } 65 | } else { 66 | ret = uint64(bs.bits[bs.bytesOffset] & BitMask[8-bs.bitsOffset-1]) 67 | bs.bytesOffset++ 68 | n -= 8 - bs.bitsOffset 69 | bs.bitsOffset = 0 70 | for n > 0 { 71 | if bs.bytesOffset >= len(bs.bits) { 72 | panic("OUT OF RANGE") 73 | } 74 | if n >= 8 { 75 | ret = ret<<8 | uint64(bs.bits[bs.bytesOffset]) 76 | bs.bytesOffset++ 77 | n -= 8 78 | } else { 79 | ret = (ret << n) | uint64((bs.bits[bs.bytesOffset]>>(8-n))&BitMask[n-1]) 80 | bs.bitsOffset = n 81 | break 82 | } 83 | } 84 | } 85 | return ret 86 | } 87 | 88 | func (bs *BitStream) GetBit() uint8 { 89 | if bs.bytesOffset >= len(bs.bits) { 90 | panic("OUT OF RANGE") 91 | } 92 | ret := bs.bits[bs.bytesOffset] >> (7 - bs.bitsOffset) & 0x01 93 | bs.bitsOffset++ 94 | if bs.bitsOffset >= 8 { 95 | bs.bytesOffset++ 96 | bs.bitsOffset = 0 97 | } 98 | return ret 99 | } 100 | 101 | func (bs *BitStream) SkipBits(n int) { 102 | bytecount := n / 8 103 | bitscount := n % 8 104 | bs.bytesOffset += bytecount 105 | if bs.bitsOffset+bitscount < 8 { 106 | bs.bitsOffset += bitscount 107 | } else { 108 | bs.bytesOffset += 1 109 | bs.bitsOffset += bitscount - 8 110 | } 111 | } 112 | 113 | func (bs *BitStream) Markdot() { 114 | bs.bitsmark = bs.bitsOffset 115 | bs.bytemark = bs.bytesOffset 116 | } 117 | 118 | func (bs *BitStream) DistanceFromMarkDot() int { 119 | bytecount := bs.bytesOffset - bs.bytemark - 1 120 | bitscount := bs.bitsOffset + (8 - bs.bitsmark) 121 | return bytecount*8 + bitscount 122 | } 123 | 124 | func (bs *BitStream) RemainBytes() int { 125 | if bs.bitsOffset > 0 { 126 | return len(bs.bits) - bs.bytesOffset - 1 127 | } else { 128 | return len(bs.bits) - bs.bytesOffset 129 | } 130 | } 131 | 132 | func (bs *BitStream) RemainBits() int { 133 | if bs.bitsOffset > 0 { 134 | return bs.RemainBytes()*8 + 8 - bs.bitsOffset 135 | } else { 136 | return bs.RemainBytes() * 8 137 | } 138 | 139 | } 140 | 141 | func (bs *BitStream) Bits() []byte { 142 | return bs.bits 143 | } 144 | 145 | func (bs *BitStream) RemainData() []byte { 146 | return bs.bits[bs.bytesOffset:] 147 | } 148 | 149 | // 无符号哥伦布熵编码 150 | func (bs *BitStream) ReadUE() uint64 { 151 | leadingZeroBits := 0 152 | for bs.GetBit() == 0 { 153 | leadingZeroBits++ 154 | } 155 | if leadingZeroBits == 0 { 156 | return 0 157 | } 158 | info := bs.GetBits(leadingZeroBits) 159 | return uint64(1)<= 8 { 182 | bs.bytesOffset-- 183 | least -= 8 184 | } 185 | if least > 0 { 186 | bs.bytesOffset-- 187 | bs.bitsOffset = 8 - least 188 | } 189 | } 190 | } 191 | 192 | func (bs *BitStream) NextBits(n int) uint64 { 193 | r := bs.GetBits(n) 194 | bs.UnRead(n) 195 | return r 196 | } 197 | 198 | func (bs *BitStream) EOS() bool { 199 | return bs.bytesOffset == len(bs.bits) && bs.bitsOffset == 0 200 | } 201 | 202 | type BitStreamWriter struct { 203 | bits []byte 204 | byteoffset int 205 | bitsoffset int 206 | bitsmark int 207 | bytemark int 208 | } 209 | 210 | func NewBitStreamWriter(n int) *BitStreamWriter { 211 | return &BitStreamWriter{ 212 | bits: make([]byte, n), 213 | byteoffset: 0, 214 | bitsoffset: 0, 215 | bitsmark: 0, 216 | bytemark: 0, 217 | } 218 | } 219 | 220 | func (bsw *BitStreamWriter) expandSpace(n int) { 221 | if (len(bsw.bits)-bsw.byteoffset-1)*8+8-bsw.bitsoffset < n { 222 | newlen := 0 223 | if len(bsw.bits)*8 < n { 224 | newlen = len(bsw.bits) + n/8 + 1 225 | } else { 226 | newlen = len(bsw.bits) * 2 227 | } 228 | tmp := make([]byte, newlen) 229 | copy(tmp, bsw.bits) 230 | bsw.bits = tmp 231 | } 232 | } 233 | 234 | func (bsw *BitStreamWriter) ByteOffset() int { 235 | return bsw.byteoffset 236 | } 237 | 238 | func (bsw *BitStreamWriter) BitOffset() int { 239 | return bsw.bitsoffset 240 | } 241 | 242 | func (bsw *BitStreamWriter) Markdot() { 243 | bsw.bitsmark = bsw.bitsoffset 244 | bsw.bytemark = bsw.byteoffset 245 | } 246 | 247 | func (bsw *BitStreamWriter) DistanceFromMarkDot() int { 248 | bytecount := bsw.byteoffset - bsw.bytemark - 1 249 | bitscount := bsw.bitsoffset + (8 - bsw.bitsmark) 250 | return bytecount*8 + bitscount 251 | } 252 | 253 | func (bsw *BitStreamWriter) PutByte(v byte) { 254 | bsw.expandSpace(8) 255 | if bsw.bitsoffset == 0 { 256 | bsw.bits[bsw.byteoffset] = v 257 | bsw.byteoffset++ 258 | } else { 259 | bsw.bits[bsw.byteoffset] |= v >> byte(bsw.bitsoffset) 260 | bsw.byteoffset++ 261 | bsw.bits[bsw.byteoffset] = v & BitMask[bsw.bitsoffset-1] 262 | } 263 | } 264 | 265 | func (bsw *BitStreamWriter) PutBytes(v []byte) { 266 | if bsw.bitsoffset != 0 { 267 | panic("bsw.bitsoffset > 0") 268 | } 269 | bsw.expandSpace(8 * len(v)) 270 | copy(bsw.bits[bsw.byteoffset:], v) 271 | bsw.byteoffset += len(v) 272 | } 273 | 274 | func (bsw *BitStreamWriter) PutRepetValue(v byte, n int) { 275 | if bsw.bitsoffset != 0 { 276 | panic("bsw.bitsoffset > 0") 277 | } 278 | bsw.expandSpace(8 * n) 279 | for i := 0; i < n; i++ { 280 | bsw.bits[bsw.byteoffset] = v 281 | bsw.byteoffset++ 282 | } 283 | } 284 | 285 | func (bsw *BitStreamWriter) PutUint8(v uint8, n int) { 286 | bsw.PutUint64(uint64(v), n) 287 | } 288 | 289 | func (bsw *BitStreamWriter) PutUint16(v uint16, n int) { 290 | bsw.PutUint64(uint64(v), n) 291 | } 292 | 293 | func (bsw *BitStreamWriter) PutUint32(v uint32, n int) { 294 | bsw.PutUint64(uint64(v), n) 295 | } 296 | 297 | func (bsw *BitStreamWriter) PutUint64(v uint64, n int) { 298 | bsw.expandSpace(n) 299 | if 8-bsw.bitsoffset >= n { 300 | bsw.bits[bsw.byteoffset] |= uint8(v) & BitMask[n-1] << (8 - bsw.bitsoffset - n) 301 | bsw.bitsoffset += n 302 | if bsw.bitsoffset == 8 { 303 | bsw.bitsoffset = 0 304 | bsw.byteoffset++ 305 | } 306 | } else { 307 | bsw.bits[bsw.byteoffset] |= uint8(v>>(n-int(8-bsw.bitsoffset))) & BitMask[8-bsw.bitsoffset-1] 308 | bsw.byteoffset++ 309 | n -= 8 - bsw.bitsoffset 310 | for n-8 >= 0 { 311 | bsw.bits[bsw.byteoffset] = uint8(v>>(n-8)) & 0xFF 312 | bsw.byteoffset++ 313 | n -= 8 314 | } 315 | bsw.bitsoffset = n 316 | if n > 0 { 317 | bsw.bits[bsw.byteoffset] |= (uint8(v) & BitMask[n-1]) << (8 - n) 318 | } 319 | } 320 | } 321 | 322 | func (bsw *BitStreamWriter) SetByte(v byte, where int) { 323 | bsw.bits[where] = v 324 | } 325 | 326 | func (bsw *BitStreamWriter) SetUint16(v uint16, where int) { 327 | binary.BigEndian.PutUint16(bsw.bits[where:where+2], v) 328 | } 329 | 330 | func (bsw *BitStreamWriter) Bits() []byte { 331 | if bsw.byteoffset == len(bsw.bits) { 332 | return bsw.bits 333 | } 334 | if bsw.bitsoffset > 0 { 335 | return bsw.bits[0 : bsw.byteoffset+1] 336 | } else { 337 | return bsw.bits[0:bsw.byteoffset] 338 | } 339 | } 340 | 341 | // 用v 填充剩余字节 342 | func (bsw *BitStreamWriter) FillRemainData(v byte) { 343 | for i := bsw.byteoffset; i < len(bsw.bits); i++ { 344 | bsw.bits[i] = v 345 | } 346 | bsw.byteoffset = len(bsw.bits) 347 | bsw.bitsoffset = 0 348 | } 349 | 350 | func (bsw *BitStreamWriter) Reset() { 351 | for i := 0; i < len(bsw.bits); i++ { 352 | bsw.bits[i] = 0 353 | } 354 | bsw.bitsmark = 0 355 | bsw.bytemark = 0 356 | bsw.bitsoffset = 0 357 | bsw.byteoffset = 0 358 | } 359 | -------------------------------------------------------------------------------- /gb28181/mpegps/ps_demuxer.go: -------------------------------------------------------------------------------- 1 | package mpegps 2 | 3 | //单元来源于https://github.com/yapingcat/gomedia 4 | import ( 5 | "errors" 6 | "github.com/q191201771/lal/pkg/avc" 7 | "github.com/q191201771/lal/pkg/hevc" 8 | "github.com/q191201771/naza/pkg/nazalog" 9 | ) 10 | 11 | type psStream struct { 12 | sid uint8 13 | cid PsStreamType 14 | pts uint64 15 | dts uint64 16 | streamBuf []byte 17 | } 18 | 19 | func newPsStream(sid uint8, cid PsStreamType) *psStream { 20 | return &psStream{ 21 | sid: sid, 22 | cid: cid, 23 | streamBuf: make([]byte, 0, 4096), 24 | } 25 | } 26 | func (p *psStream) setCid(cid PsStreamType) { 27 | p.cid = cid 28 | } 29 | 30 | func (p *psStream) clearBuf() { 31 | p.streamBuf = p.streamBuf[:0] 32 | } 33 | 34 | type PsDemuxer struct { 35 | streamMap map[uint8]*psStream 36 | pkg *PsPacket 37 | mpeg1 bool 38 | cache []byte 39 | OnFrame func(frame []byte, cid PsStreamType, pts uint64, dts uint64) 40 | //解ps包过程中,解码回调psm,system header,pes包等 41 | //decodeResult 解码ps包时的产生的错误 42 | //这个回调主要用于debug,查看是否ps包存在问题 43 | OnPacket func(pkg Display, decodeResult error) 44 | 45 | verifyBuf []byte 46 | 47 | log nazalog.Logger 48 | } 49 | 50 | func NewPsDemuxer() *PsDemuxer { 51 | psDemuxer := &PsDemuxer{ 52 | streamMap: make(map[uint8]*psStream), 53 | pkg: new(PsPacket), 54 | cache: make([]byte, 0, 256), 55 | OnFrame: nil, 56 | OnPacket: nil, 57 | } 58 | return psDemuxer 59 | } 60 | 61 | func (psDemuxer *PsDemuxer) Input(data []byte) error { 62 | var bs *BitStream 63 | if len(psDemuxer.cache) > 0 { 64 | psDemuxer.cache = append(psDemuxer.cache, data...) 65 | bs = NewBitStream(psDemuxer.cache) 66 | } else { 67 | bs = NewBitStream(data) 68 | } 69 | 70 | saveReseved := func() { 71 | tmpcache := make([]byte, bs.RemainBytes()) 72 | copy(tmpcache, bs.RemainData()) 73 | psDemuxer.cache = tmpcache 74 | } 75 | 76 | var ret error = nil 77 | for !bs.EOS() { 78 | if mpegerr, ok := ret.(Error); ok { 79 | if mpegerr.NeedMore() { 80 | saveReseved() 81 | } 82 | break 83 | } 84 | if bs.RemainBits() < 32 { 85 | ret = errNeedMore 86 | saveReseved() 87 | break 88 | } 89 | prefix_code := bs.NextBits(32) 90 | switch prefix_code { 91 | case 0x000001BA: //pack header 92 | if psDemuxer.pkg.Header == nil { 93 | psDemuxer.pkg.Header = new(PsPackHeader) 94 | } 95 | ret = psDemuxer.pkg.Header.Decode(bs) 96 | psDemuxer.mpeg1 = psDemuxer.pkg.Header.IsMpeg1 97 | if psDemuxer.OnPacket != nil { 98 | psDemuxer.OnPacket(psDemuxer.pkg.Header, ret) 99 | } 100 | case 0x000001BB: //system header 101 | if psDemuxer.pkg.Header == nil { 102 | return errors.New("PsDemuxer.pkg.Header must not be nil") 103 | } 104 | if psDemuxer.pkg.System == nil { 105 | psDemuxer.pkg.System = new(SystemHeader) 106 | } 107 | ret = psDemuxer.pkg.System.Decode(bs) 108 | if psDemuxer.OnPacket != nil { 109 | psDemuxer.OnPacket(psDemuxer.pkg.System, ret) 110 | } 111 | case 0x000001BC: //program stream map 112 | if psDemuxer.pkg.Psm == nil { 113 | psDemuxer.pkg.Psm = new(ProgramStreamMap) 114 | } 115 | if ret = psDemuxer.pkg.Psm.Decode(bs); ret == nil { 116 | for _, streaminfo := range psDemuxer.pkg.Psm.StreamMap { 117 | if _, found := psDemuxer.streamMap[streaminfo.ElementaryStreamId]; !found { 118 | stream := newPsStream(streaminfo.ElementaryStreamId, PsStreamType(streaminfo.StreamType)) 119 | psDemuxer.streamMap[stream.sid] = stream 120 | } else { 121 | stream := psDemuxer.streamMap[streaminfo.ElementaryStreamId] 122 | stream.setCid(PsStreamType(streaminfo.StreamType)) 123 | } 124 | } 125 | } 126 | if psDemuxer.OnPacket != nil { 127 | psDemuxer.OnPacket(psDemuxer.pkg.Psm, ret) 128 | } 129 | case 0x000001BD, 0x000001BE, 0x000001BF, 0x000001F0, 0x000001F1, 130 | 0x000001F2, 0x000001F3, 0x000001F4, 0x000001F5, 0x000001F6, 131 | 0x000001F7, 0x000001F8, 0x000001F9, 0x000001FA, 0x000001FB: 132 | if psDemuxer.pkg.CommPes == nil { 133 | psDemuxer.pkg.CommPes = new(CommonPesPacket) 134 | } 135 | ret = psDemuxer.pkg.CommPes.Decode(bs) 136 | case 0x000001FF: //program stream directory 137 | if psDemuxer.pkg.Psd == nil { 138 | psDemuxer.pkg.Psd = new(ProgramStreamDirectory) 139 | } 140 | ret = psDemuxer.pkg.Psd.Decode(bs) 141 | case 0x000001B9: //MPEG_program_end_code 142 | continue 143 | default: 144 | if prefix_code&0xFFFFFFE0 == 0x000001C0 || prefix_code&0xFFFFFFE0 == 0x000001E0 { 145 | if psDemuxer.pkg.Pes == nil { 146 | psDemuxer.pkg.Pes = NewPesPacket() 147 | } 148 | if psDemuxer.mpeg1 { 149 | ret = psDemuxer.pkg.Pes.DecodeMpeg1(bs) 150 | } else { 151 | ret = psDemuxer.pkg.Pes.Decode(bs) 152 | } 153 | if psDemuxer.OnPacket != nil { 154 | psDemuxer.OnPacket(psDemuxer.pkg.Pes, ret) 155 | } 156 | if ret == nil { 157 | if stream, found := psDemuxer.streamMap[psDemuxer.pkg.Pes.StreamId]; found { 158 | if psDemuxer.mpeg1 && stream.cid == PsStreamUnknow { 159 | psDemuxer.guessCodecid(stream) 160 | } 161 | psDemuxer.demuxPespacket(stream, psDemuxer.pkg.Pes) 162 | } else { 163 | if psDemuxer.mpeg1 { 164 | stream := newPsStream(psDemuxer.pkg.Pes.StreamId, PsStreamUnknow) 165 | psDemuxer.streamMap[stream.sid] = stream 166 | stream.streamBuf = append(stream.streamBuf, psDemuxer.pkg.Pes.PesPayload...) 167 | stream.pts = psDemuxer.pkg.Pes.Pts 168 | stream.dts = psDemuxer.pkg.Pes.Dts 169 | } else if psDemuxer.pkg.Pes.StreamId == uint8(PesStreamVideo) { 170 | if len(psDemuxer.verifyBuf) > 256 { 171 | psDemuxer.verifyBuf = psDemuxer.verifyBuf[:0] 172 | } 173 | psDemuxer.verifyBuf = append(psDemuxer.verifyBuf, psDemuxer.pkg.Pes.PesPayload...) 174 | if h26x, err := mpegH26xVerify(psDemuxer.verifyBuf); err == nil { 175 | switch h26x { 176 | case CodecUnknown: 177 | case CodecH264: 178 | streamH264 := newPsStream(uint8(PesStreamVideo), PsStreamH264) 179 | psDemuxer.streamMap[streamH264.sid] = streamH264 180 | psDemuxer.demuxPespacket(streamH264, psDemuxer.pkg.Pes) 181 | case CodecH265: 182 | streamH265 := newPsStream(uint8(PesStreamVideo), PsStreamH265) 183 | psDemuxer.streamMap[streamH265.sid] = streamH265 184 | psDemuxer.demuxPespacket(streamH265, psDemuxer.pkg.Pes) 185 | } 186 | } 187 | } else if psDemuxer.pkg.Pes.StreamId == uint8(PesStreamAudio) { 188 | if _, found = psDemuxer.streamMap[uint8(PesStreamVideo)]; found { 189 | psStreamType := audioVerify(psDemuxer.pkg.Pes.PesPayload) 190 | streamAudio := newPsStream(uint8(PesStreamAudio), psStreamType) 191 | psDemuxer.streamMap[streamAudio.sid] = streamAudio 192 | psDemuxer.demuxPespacket(streamAudio, psDemuxer.pkg.Pes) 193 | } 194 | } 195 | } 196 | } 197 | } else { 198 | bs.SkipBits(8) 199 | } 200 | } 201 | } 202 | 203 | if ret == nil && len(psDemuxer.cache) > 0 { 204 | psDemuxer.cache = nil 205 | } 206 | 207 | return ret 208 | } 209 | 210 | func (psDemuxer *PsDemuxer) Flush() { 211 | for _, stream := range psDemuxer.streamMap { 212 | if len(stream.streamBuf) == 0 { 213 | continue 214 | } 215 | if psDemuxer.OnFrame != nil { 216 | psDemuxer.OnFrame(stream.streamBuf, stream.cid, stream.pts/90, stream.dts/90) 217 | } 218 | } 219 | } 220 | 221 | func (psDemuxer *PsDemuxer) guessCodecid(stream *psStream) { 222 | if stream.sid&0xE0 == uint8(PesStreamAudio) { 223 | psStreamType := audioVerify(psDemuxer.pkg.Pes.PesPayload) 224 | stream.cid = psStreamType 225 | } else if stream.sid&0xE0 == uint8(PesStreamVideo) { 226 | if h26x, err := mpegH26xVerify(stream.streamBuf); err == nil { 227 | switch h26x { 228 | case CodecUnknown: 229 | case CodecH264: 230 | stream.cid = PsStreamH264 231 | case CodecH265: 232 | stream.cid = PsStreamH265 233 | } 234 | } 235 | } 236 | } 237 | 238 | func (psDemuxer *PsDemuxer) demuxPespacket(stream *psStream, pes *PesPacket) error { 239 | switch stream.cid { 240 | case PsStreamAac, PsStreamG711A, PsStreamG711U: 241 | return psDemuxer.demuxAudio(stream, pes) 242 | case PsStreamH264, PsStreamH265: 243 | return psDemuxer.demuxH26x(stream, pes) 244 | case PsStreamUnknow: 245 | if stream.pts != pes.Pts { 246 | stream.streamBuf = nil 247 | } 248 | stream.streamBuf = append(stream.streamBuf, pes.PesPayload...) 249 | stream.pts = pes.Pts 250 | stream.dts = pes.Dts 251 | } 252 | return nil 253 | } 254 | 255 | func (psDemuxer *PsDemuxer) demuxAudio(stream *psStream, pes *PesPacket) error { 256 | if psDemuxer.OnFrame != nil { 257 | psDemuxer.OnFrame(pes.PesPayload, stream.cid, pes.Pts/90, pes.Dts/90) 258 | } 259 | return nil 260 | } 261 | 262 | func (psDemuxer *PsDemuxer) demuxH26x(stream *psStream, pes *PesPacket) error { 263 | if stream.pts == 0 { 264 | stream.streamBuf = append(stream.streamBuf, pes.PesPayload...) 265 | stream.pts = pes.Pts 266 | stream.dts = pes.Dts 267 | } else if stream.pts == pes.Pts || pes.Pts == 0 { 268 | stream.streamBuf = append(stream.streamBuf, pes.PesPayload...) 269 | } else { 270 | start, sc := FindStartCode(stream.streamBuf, 0) 271 | for start >= 0 && start < len(stream.streamBuf) { 272 | end, sc2 := FindStartCode(stream.streamBuf, start+int(sc)) 273 | if end < 0 { 274 | end = len(stream.streamBuf) 275 | } 276 | if stream.cid == PsStreamH264 { 277 | naluType := H264NaluType(stream.streamBuf[start:]) 278 | if naluType != avc.NaluTypeAud { 279 | if psDemuxer.OnFrame != nil { 280 | psDemuxer.OnFrame(stream.streamBuf[start:end], stream.cid, stream.pts/90, stream.dts/90) 281 | } 282 | } 283 | } else if stream.cid == PsStreamH265 { 284 | naluType := H265NaluType(stream.streamBuf[start:]) 285 | if naluType != hevc.NaluTypeAud { 286 | if psDemuxer.OnFrame != nil { 287 | psDemuxer.OnFrame(stream.streamBuf[start:end], stream.cid, stream.pts/90, stream.dts/90) 288 | } 289 | } 290 | } 291 | start = end 292 | sc = sc2 293 | } 294 | stream.streamBuf = nil 295 | stream.streamBuf = append(stream.streamBuf, pes.PesPayload...) 296 | stream.pts = pes.Pts 297 | stream.dts = pes.Dts 298 | } 299 | 300 | return nil 301 | } 302 | -------------------------------------------------------------------------------- /gb28181/mpegps/ps_demuxer_test.go: -------------------------------------------------------------------------------- 1 | package mpegps 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "github.com/q191201771/lal/pkg/base" 7 | "github.com/q191201771/lal/pkg/rtprtcp" 8 | "github.com/q191201771/naza/pkg/nazabytes" 9 | "github.com/q191201771/naza/pkg/nazalog" 10 | "io" 11 | "os" 12 | "testing" 13 | ) 14 | 15 | var ps1 []byte = []byte{0x00, 0x00, 0x01, 0xBA} 16 | var ps2 []byte = []byte{0x00, 0x00, 0x01, 0xBA, 0x40, 0x01, 0x00, 0x01, 0x33, 0x44, 0xFF, 0xFF, 0xFF, 0xF1, 0xFF} 17 | 18 | var ps3 []byte = []byte{0x00, 0x00, 0x01, 0xBA, 0x40, 0x01, 0x00, 0x01, 0x33, 0x44, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x01, 0xBB} 19 | var ps4 []byte = []byte{0x00, 0x00, 0x01, 0xBA, 0x40, 0x01, 0x00, 0x01, 0x33, 0x44, 0xFF, 0xFF, 0xFF, 0xF1, 0x34, 0x00, 0x00, 0x01, 0xBB, 0x00, 0x01, 0x00, 0x01, 0x33, 0x44, 0xFF, 0x34} 20 | var ps5 []byte = []byte{0x00, 0x00, 0x01, 0xBA, 0x40, 0x01, 0x00, 0x01, 0x33, 0x44, 0xFF, 0xFF, 0xFF, 0xF1, 0x34, 0x00, 0x00, 0x01, 0xBB, 0x00, 0x09, 0x00, 0x01, 0x33, 0x44, 0xFF, 0x34, 0x81, 0x00, 0x00} 21 | var ps6 []byte = []byte{0x00, 0x00, 0x01, 0xBC, 0x40, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x34, 0x81, 0x00, 0x00} 22 | var ps7 []byte = []byte{0x00, 0x00, 0x01, 0xBA, 0x20, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03} 23 | 24 | func TestPSDemuxer_Input(t *testing.T) { 25 | type fields struct { 26 | streamMap map[uint8]*psStream 27 | pkg *PsPacket 28 | cache []byte 29 | OnPacket func(pkg Display, decodeResult error) 30 | OnFrame func(frame []byte, cid PsStreamType, pts uint64, dts uint64) 31 | } 32 | type args struct { 33 | data []byte 34 | } 35 | tests := []struct { 36 | name string 37 | fields fields 38 | args args 39 | wantErr bool 40 | }{ 41 | {name: "test1", fields: fields{ 42 | streamMap: make(map[uint8]*psStream), 43 | pkg: new(PsPacket), 44 | }, args: args{data: ps1}, wantErr: true}, 45 | 46 | {name: "test2", fields: fields{ 47 | streamMap: make(map[uint8]*psStream), 48 | pkg: new(PsPacket), 49 | }, args: args{data: ps2}, wantErr: false}, 50 | 51 | {name: "test3", fields: fields{ 52 | streamMap: make(map[uint8]*psStream), 53 | pkg: new(PsPacket), 54 | }, args: args{data: ps3}, wantErr: true}, 55 | 56 | {name: "test4", fields: fields{ 57 | streamMap: make(map[uint8]*psStream), 58 | pkg: new(PsPacket), 59 | }, args: args{data: ps4}, wantErr: true}, 60 | 61 | {name: "test5", fields: fields{ 62 | streamMap: make(map[uint8]*psStream), 63 | pkg: new(PsPacket), 64 | }, args: args{data: ps5}, wantErr: false}, 65 | {name: "test6", fields: fields{ 66 | streamMap: make(map[uint8]*psStream), 67 | pkg: new(PsPacket), 68 | }, args: args{data: ps6}, wantErr: false}, 69 | {name: "test-mpeg1", fields: fields{ 70 | streamMap: make(map[uint8]*psStream), 71 | pkg: new(PsPacket), 72 | }, args: args{data: ps7}, wantErr: false}, 73 | } 74 | for _, tt := range tests { 75 | t.Run(tt.name, func(t *testing.T) { 76 | psdemuxer := &PsDemuxer{ 77 | streamMap: tt.fields.streamMap, 78 | pkg: tt.fields.pkg, 79 | cache: tt.fields.cache, 80 | OnPacket: tt.fields.OnPacket, 81 | OnFrame: tt.fields.OnFrame, 82 | } 83 | if err := psdemuxer.Input(tt.args.data); (err != nil) != tt.wantErr { 84 | t.Errorf("PSDemuxer.Input() error = %v, wantErr %v", err, tt.wantErr) 85 | } 86 | }) 87 | } 88 | } 89 | func TestPSDemuxer(t *testing.T) { 90 | var psUnpacker *PsDemuxer 91 | os.Remove("h.ps") 92 | os.Remove("h.h264") 93 | os.Remove("ps_demux_result") 94 | dumpFile := base.NewDumpFile() 95 | err := dumpFile.OpenToRead("C:\\Users\\Administrator\\Desktop\\dump_37060000001320000001001.raw") 96 | if err != nil { 97 | fmt.Println(err) 98 | return 99 | } 100 | psUnpacker = NewPsDemuxer() 101 | psUnpacker.OnFrame = func(frame []byte, cid PsStreamType, pts uint64, dts uint64) { 102 | if cid == PsStreamH264 || cid == PsStreamH265 { 103 | writeFile("h.h264", frame) 104 | } else { 105 | if cid == PsStreamG711A { 106 | nazalog.Infof("存在音频g711A 大小:%d dts:%d", len(frame), dts) 107 | } else if cid == PsStreamG711U { 108 | nazalog.Infof("存在音频g711U 大小:%d dts:%d", len(frame), dts) 109 | } else { 110 | nazalog.Infof("存在音频aac 大小:%d dts:%d", len(frame), dts) 111 | } 112 | } 113 | 114 | } 115 | fd3, err := os.OpenFile("ps_demux_result", os.O_CREATE|os.O_RDWR, 0666) 116 | if err != nil { 117 | fmt.Println(err) 118 | return 119 | } 120 | defer fd3.Close() 121 | psUnpacker.OnPacket = func(pkg Display, decodeResult error) { 122 | switch value := pkg.(type) { 123 | case *PsPackHeader: 124 | fd3.WriteString("--------------PS Pack Header--------------\n") 125 | if decodeResult == nil { 126 | value.PrettyPrint(fd3) 127 | } else { 128 | fd3.WriteString(fmt.Sprintf("Decode Ps Packet Failed %s\n", decodeResult.Error())) 129 | } 130 | case *SystemHeader: 131 | fd3.WriteString("--------------System Header--------------\n") 132 | if decodeResult == nil { 133 | value.PrettyPrint(fd3) 134 | } else { 135 | fd3.WriteString(fmt.Sprintf("Decode Ps Packet Failed %s\n", decodeResult.Error())) 136 | } 137 | case *ProgramStreamMap: 138 | fd3.WriteString("--------------------PSM-------------------\n") 139 | if decodeResult == nil { 140 | value.PrettyPrint(fd3) 141 | } else { 142 | fd3.WriteString(fmt.Sprintf("Decode Ps Packet Failed %s\n", decodeResult.Error())) 143 | } 144 | case *PesPacket: 145 | fd3.WriteString("-------------------PES--------------------\n") 146 | if decodeResult == nil { 147 | value.PrettyPrint(fd3) 148 | } else { 149 | fd3.WriteString(fmt.Sprintf("Decode Ps Packet Failed %s\n", decodeResult.Error())) 150 | } 151 | } 152 | } 153 | 154 | if err != nil { 155 | return 156 | } 157 | packe := 0 158 | Seq := 0 159 | for { 160 | m, err := dumpFile.ReadOneMessage() 161 | if err == io.EOF { 162 | break 163 | } 164 | ipkt, err := rtprtcp.ParseRtpPacket(m.Body) 165 | if err != nil { 166 | nazalog.Errorf("PsUnpacker ParseRtpPacket failed. b=%s, err=%+v", 167 | hex.Dump(nazabytes.Prefix(m.Body, 64)), err) 168 | continue 169 | } 170 | packe++ 171 | if ipkt.Header.Seq-uint16(Seq) != 1 { 172 | fmt.Printf("pkt Seq:%d ssrc:%d \n", ipkt.Header.Seq, ipkt.Header.Ssrc) 173 | } 174 | Seq = int(ipkt.Header.Seq) 175 | body := ipkt.Body() 176 | writeFile("h.ps", body) 177 | fmt.Println(psUnpacker.Input(body)) 178 | } 179 | 180 | } 181 | func fileExists(fileName string) (bool, error) { 182 | _, err := os.Stat(fileName) 183 | if err == nil { 184 | return true, nil 185 | } 186 | if os.IsNotExist(err) { 187 | return false, nil 188 | } 189 | return false, err 190 | } 191 | func writeFile(filename string, buffer []byte) (err error) { 192 | var fp *os.File 193 | b, err := fileExists(filename) 194 | if err != nil { 195 | return 196 | } 197 | if !b { 198 | fp, err = os.Create(filename) 199 | if err != nil { 200 | return 201 | } 202 | } else { 203 | fp, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND, 6) 204 | if err != nil { 205 | return 206 | } 207 | } 208 | defer fp.Close() 209 | _, err = fp.Write(buffer) 210 | 211 | return 212 | } 213 | -------------------------------------------------------------------------------- /gb28181/mpegps/ps_muxer.go: -------------------------------------------------------------------------------- 1 | package mpegps 2 | 3 | //单元来源于https://github.com/yapingcat/gomedia 4 | import ( 5 | "github.com/q191201771/lal/pkg/avc" 6 | "github.com/q191201771/lal/pkg/hevc" 7 | ) 8 | 9 | type PsMuxer struct { 10 | system *SystemHeader 11 | psm *ProgramStreamMap 12 | OnPacket func(pkg []byte, pts uint64) 13 | firstframe bool 14 | } 15 | 16 | func NewPsMuxer() *PsMuxer { 17 | muxer := new(PsMuxer) 18 | muxer.firstframe = true 19 | muxer.system = new(SystemHeader) 20 | muxer.system.RateBound = 26234 21 | muxer.psm = new(ProgramStreamMap) 22 | muxer.psm.CurrentNextIndicator = 1 23 | muxer.psm.ProgramStreamMapVersion = 1 24 | muxer.OnPacket = nil 25 | return muxer 26 | } 27 | 28 | func (muxer *PsMuxer) AddStream(cid PsStreamType) uint8 { 29 | if cid == PsStreamH265 || cid == PsStreamH264 { 30 | es := NewElementaryStream(uint8(PesStreamVideo) + muxer.system.VideoBound) 31 | es.PStdBufferBoundScale = 1 32 | es.PStdBufferSizeBound = 400 33 | muxer.system.Streams = append(muxer.system.Streams, es) 34 | muxer.system.VideoBound++ 35 | muxer.psm.StreamMap = append(muxer.psm.StreamMap, NewElementaryStreamElem(uint8(cid), es.StreamId)) 36 | muxer.psm.ProgramStreamMapVersion++ 37 | return es.StreamId 38 | } else { 39 | es := NewElementaryStream(uint8(PesStreamAudio) + muxer.system.AudioBound) 40 | es.PStdBufferBoundScale = 0 41 | es.PStdBufferSizeBound = 32 42 | muxer.system.Streams = append(muxer.system.Streams, es) 43 | muxer.system.AudioBound++ 44 | muxer.psm.StreamMap = append(muxer.psm.StreamMap, NewElementaryStreamElem(uint8(cid), es.StreamId)) 45 | muxer.psm.ProgramStreamMapVersion++ 46 | return es.StreamId 47 | } 48 | } 49 | 50 | func (muxer *PsMuxer) Write(sid uint8, frame []byte, pts uint64, dts uint64) error { 51 | var stream *ElementaryStreamElem = nil 52 | for _, es := range muxer.psm.StreamMap { 53 | if es.ElementaryStreamId == sid { 54 | stream = es 55 | break 56 | } 57 | } 58 | if stream == nil { 59 | return errNotFound 60 | } 61 | if len(frame) <= 0 { 62 | return nil 63 | } 64 | var withaud bool = false 65 | var idrFlag bool = false 66 | var first bool = true 67 | var vcl bool = false 68 | if stream.StreamType == uint8(PsStreamH264) || stream.StreamType == uint8(PsStreamH265) { 69 | SplitFrame(frame, func(nalu []byte) bool { 70 | if stream.StreamType == uint8(PsStreamH264) { 71 | naluType := avc.ParseNaluType(nalu[0]) 72 | if naluType == avc.NaluTypeAud { 73 | withaud = true 74 | return false 75 | } else if naluType >= avc.NaluTypeSlice && naluType <= avc.NaluTypeIdrSlice { 76 | if naluType == avc.NaluTypeIdrSlice { 77 | idrFlag = true 78 | } 79 | vcl = true 80 | return false 81 | } 82 | return true 83 | } else { 84 | naluType := hevc.ParseNaluType(nalu[0]) 85 | if naluType == hevc.NaluTypeAud { 86 | withaud = true 87 | return false 88 | } else if naluType >= hevc.NaluTypeSliceBlaWlp && naluType <= hevc.NaluTypeSliceRsvIrapVcl23 || 89 | naluType >= hevc.NaluTypeSliceTrailN && naluType <= hevc.NaluTypeSliceRaslR { 90 | if naluType >= hevc.NaluTypeSliceBlaWlp && naluType <= hevc.NaluTypeSliceRsvIrapVcl23 { 91 | idrFlag = true 92 | } 93 | vcl = true 94 | return false 95 | } 96 | return true 97 | } 98 | }) 99 | } 100 | 101 | dts = dts * 90 102 | pts = pts * 90 103 | bsw := NewBitStreamWriter(1024) 104 | var pack PsPackHeader 105 | pack.SystemClockReferenceBase = dts - 3600 106 | pack.SystemClockReferenceExtension = 0 107 | pack.ProgramMuxRate = 6106 108 | pack.Encode(bsw) 109 | if muxer.firstframe || idrFlag { 110 | muxer.system.Encode(bsw) 111 | muxer.psm.Encode(bsw) 112 | muxer.firstframe = false 113 | } 114 | pespkg := NewPesPacket() 115 | for len(frame) > 0 { 116 | peshdrlen := 13 117 | pespkg.StreamId = sid 118 | pespkg.PtsDtsFlags = 0x03 119 | pespkg.PesHeaderDataLength = 10 120 | pespkg.Pts = pts 121 | pespkg.Dts = dts 122 | if idrFlag { 123 | pespkg.DataAlignmentIndicator = 1 124 | } 125 | if first && !withaud && vcl { 126 | if stream.StreamType == uint8(PsStreamH264) { 127 | pespkg.PesPayload = append(pespkg.PesPayload, H264AudNalu...) 128 | peshdrlen += 6 129 | } else if stream.StreamType == uint8(PsStreamH265) { 130 | pespkg.PesPayload = append(pespkg.PesPayload, H265AudNalu...) 131 | peshdrlen += 7 132 | } 133 | } 134 | if peshdrlen+len(frame) >= 0xFFFF { 135 | pespkg.PesPacketLength = 0xFFFF 136 | pespkg.PesPayload = append(pespkg.PesPayload, frame[0:0xFFFF-peshdrlen]...) 137 | frame = frame[0xFFFF-peshdrlen:] 138 | } else { 139 | pespkg.PesPacketLength = uint16(peshdrlen + len(frame)) 140 | pespkg.PesPayload = append(pespkg.PesPayload, frame[0:]...) 141 | frame = frame[:0] 142 | } 143 | pespkg.Encode(bsw) 144 | pespkg.PesPayload = pespkg.PesPayload[:0] 145 | if muxer.OnPacket != nil { 146 | muxer.OnPacket(bsw.Bits(), pts) 147 | } 148 | bsw.Reset() 149 | first = false 150 | } 151 | return nil 152 | } 153 | -------------------------------------------------------------------------------- /gb28181/mpegps/util.go: -------------------------------------------------------------------------------- 1 | package mpegps 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | ) 7 | 8 | const ( 9 | CodecUnknown = iota 10 | CodecH264 11 | CodecH265 12 | CodecH266 13 | CodecMpeg4 14 | ) 15 | 16 | var crc32table [256]uint32 = [256]uint32{ 17 | 0x00000000, 0xB71DC104, 0x6E3B8209, 0xD926430D, 0xDC760413, 0x6B6BC517, 18 | 0xB24D861A, 0x0550471E, 0xB8ED0826, 0x0FF0C922, 0xD6D68A2F, 0x61CB4B2B, 19 | 0x649B0C35, 0xD386CD31, 0x0AA08E3C, 0xBDBD4F38, 0x70DB114C, 0xC7C6D048, 20 | 0x1EE09345, 0xA9FD5241, 0xACAD155F, 0x1BB0D45B, 0xC2969756, 0x758B5652, 21 | 0xC836196A, 0x7F2BD86E, 0xA60D9B63, 0x11105A67, 0x14401D79, 0xA35DDC7D, 22 | 0x7A7B9F70, 0xCD665E74, 0xE0B62398, 0x57ABE29C, 0x8E8DA191, 0x39906095, 23 | 0x3CC0278B, 0x8BDDE68F, 0x52FBA582, 0xE5E66486, 0x585B2BBE, 0xEF46EABA, 24 | 0x3660A9B7, 0x817D68B3, 0x842D2FAD, 0x3330EEA9, 0xEA16ADA4, 0x5D0B6CA0, 25 | 0x906D32D4, 0x2770F3D0, 0xFE56B0DD, 0x494B71D9, 0x4C1B36C7, 0xFB06F7C3, 26 | 0x2220B4CE, 0x953D75CA, 0x28803AF2, 0x9F9DFBF6, 0x46BBB8FB, 0xF1A679FF, 27 | 0xF4F63EE1, 0x43EBFFE5, 0x9ACDBCE8, 0x2DD07DEC, 0x77708634, 0xC06D4730, 28 | 0x194B043D, 0xAE56C539, 0xAB068227, 0x1C1B4323, 0xC53D002E, 0x7220C12A, 29 | 0xCF9D8E12, 0x78804F16, 0xA1A60C1B, 0x16BBCD1F, 0x13EB8A01, 0xA4F64B05, 30 | 0x7DD00808, 0xCACDC90C, 0x07AB9778, 0xB0B6567C, 0x69901571, 0xDE8DD475, 31 | 0xDBDD936B, 0x6CC0526F, 0xB5E61162, 0x02FBD066, 0xBF469F5E, 0x085B5E5A, 32 | 0xD17D1D57, 0x6660DC53, 0x63309B4D, 0xD42D5A49, 0x0D0B1944, 0xBA16D840, 33 | 0x97C6A5AC, 0x20DB64A8, 0xF9FD27A5, 0x4EE0E6A1, 0x4BB0A1BF, 0xFCAD60BB, 34 | 0x258B23B6, 0x9296E2B2, 0x2F2BAD8A, 0x98366C8E, 0x41102F83, 0xF60DEE87, 35 | 0xF35DA999, 0x4440689D, 0x9D662B90, 0x2A7BEA94, 0xE71DB4E0, 0x500075E4, 36 | 0x892636E9, 0x3E3BF7ED, 0x3B6BB0F3, 0x8C7671F7, 0x555032FA, 0xE24DF3FE, 37 | 0x5FF0BCC6, 0xE8ED7DC2, 0x31CB3ECF, 0x86D6FFCB, 0x8386B8D5, 0x349B79D1, 38 | 0xEDBD3ADC, 0x5AA0FBD8, 0xEEE00C69, 0x59FDCD6D, 0x80DB8E60, 0x37C64F64, 39 | 0x3296087A, 0x858BC97E, 0x5CAD8A73, 0xEBB04B77, 0x560D044F, 0xE110C54B, 40 | 0x38368646, 0x8F2B4742, 0x8A7B005C, 0x3D66C158, 0xE4408255, 0x535D4351, 41 | 0x9E3B1D25, 0x2926DC21, 0xF0009F2C, 0x471D5E28, 0x424D1936, 0xF550D832, 42 | 0x2C769B3F, 0x9B6B5A3B, 0x26D61503, 0x91CBD407, 0x48ED970A, 0xFFF0560E, 43 | 0xFAA01110, 0x4DBDD014, 0x949B9319, 0x2386521D, 0x0E562FF1, 0xB94BEEF5, 44 | 0x606DADF8, 0xD7706CFC, 0xD2202BE2, 0x653DEAE6, 0xBC1BA9EB, 0x0B0668EF, 45 | 0xB6BB27D7, 0x01A6E6D3, 0xD880A5DE, 0x6F9D64DA, 0x6ACD23C4, 0xDDD0E2C0, 46 | 0x04F6A1CD, 0xB3EB60C9, 0x7E8D3EBD, 0xC990FFB9, 0x10B6BCB4, 0xA7AB7DB0, 47 | 0xA2FB3AAE, 0x15E6FBAA, 0xCCC0B8A7, 0x7BDD79A3, 0xC660369B, 0x717DF79F, 48 | 0xA85BB492, 0x1F467596, 0x1A163288, 0xAD0BF38C, 0x742DB081, 0xC3307185, 49 | 0x99908A5D, 0x2E8D4B59, 0xF7AB0854, 0x40B6C950, 0x45E68E4E, 0xF2FB4F4A, 50 | 0x2BDD0C47, 0x9CC0CD43, 0x217D827B, 0x9660437F, 0x4F460072, 0xF85BC176, 51 | 0xFD0B8668, 0x4A16476C, 0x93300461, 0x242DC565, 0xE94B9B11, 0x5E565A15, 52 | 0x87701918, 0x306DD81C, 0x353D9F02, 0x82205E06, 0x5B061D0B, 0xEC1BDC0F, 53 | 0x51A69337, 0xE6BB5233, 0x3F9D113E, 0x8880D03A, 0x8DD09724, 0x3ACD5620, 54 | 0xE3EB152D, 0x54F6D429, 0x7926A9C5, 0xCE3B68C1, 0x171D2BCC, 0xA000EAC8, 55 | 0xA550ADD6, 0x124D6CD2, 0xCB6B2FDF, 0x7C76EEDB, 0xC1CBA1E3, 0x76D660E7, 56 | 0xAFF023EA, 0x18EDE2EE, 0x1DBDA5F0, 0xAAA064F4, 0x738627F9, 0xC49BE6FD, 57 | 0x09FDB889, 0xBEE0798D, 0x67C63A80, 0xD0DBFB84, 0xD58BBC9A, 0x62967D9E, 58 | 0xBBB03E93, 0x0CADFF97, 0xB110B0AF, 0x060D71AB, 0xDF2B32A6, 0x6836F3A2, 59 | 0x6D66B4BC, 0xDA7B75B8, 0x035D36B5, 0xB440F7B1, 60 | } 61 | 62 | func CalcCrc32(crc uint32, buffer []byte) uint32 { 63 | var i int = 0 64 | for i = 0; i < len(buffer); i++ { 65 | crc = crc32table[(crc^uint32(buffer[i]))&0xff] ^ (crc >> 8) 66 | } 67 | return crc 68 | } 69 | 70 | type StartCodeType int 71 | 72 | const ( 73 | StartCode3 StartCodeType = 3 74 | STartCode4 StartCodeType = 4 75 | ) 76 | 77 | func FindStartCode(nalu []byte, offset int) (int, StartCodeType) { 78 | idx := bytes.Index(nalu[offset:], []byte{0x00, 0x00, 0x01}) 79 | switch { 80 | case idx > 0: 81 | if nalu[offset+idx-1] == 0x00 { 82 | return offset + idx - 1, STartCode4 83 | } 84 | fallthrough 85 | case idx == 0: 86 | return offset + idx, StartCode3 87 | } 88 | return -1, StartCode3 89 | } 90 | 91 | func SplitFrame(frames []byte, onFrame func(nalu []byte) bool) { 92 | beg, sc := FindStartCode(frames, 0) 93 | for beg >= 0 { 94 | end, sc2 := FindStartCode(frames, beg+int(sc)) 95 | if end == -1 { 96 | if onFrame != nil { 97 | onFrame(frames[beg+int(sc):]) 98 | } 99 | break 100 | } 101 | if onFrame != nil && onFrame(frames[beg+int(sc):end]) == false { 102 | break 103 | } 104 | beg = end 105 | sc = sc2 106 | } 107 | } 108 | func H264NaluType(h264 []byte) uint8 { 109 | loc, sc := FindStartCode(h264, 0) 110 | return h264[loc+int(sc)] & 0x1F 111 | } 112 | func H265NaluType(h265 []byte) uint8 { 113 | loc, sc := FindStartCode(h265, 0) 114 | return (h265[loc+int(sc)] >> 1) & 0x3F 115 | } 116 | 117 | func mpegH264FindNALU(data []byte) (int, int, error) { 118 | var zeros, i int 119 | 120 | for i = 0; i+2 < len(data); i++ { 121 | if data[i] == 0x01 && zeros >= 2 { 122 | return i + 1, zeros + 1, nil // 返回 NALU 的长度和前导零的数量 123 | } 124 | 125 | if data[i] == 0 { 126 | zeros++ 127 | } else { 128 | zeros = 0 129 | } 130 | } 131 | 132 | return -1, 0, errors.New("no valid NALU found") 133 | } 134 | 135 | // 来自media-server 136 | func mpegH26xVerify(data []byte) (int, error) { 137 | h264Flags := uint32(0x01A0) // SPS/PPS/IDR 138 | h265Flags := uint64(0x700000000) // VPS/SPS/PPS 139 | h266Flags := uint32(0xC000) // VPS/SPS/PPS 140 | count := 0 141 | h26x := [5][10]int{} 142 | 143 | p := 0 144 | end := len(data) 145 | 146 | for p < end && count < len(h26x[0]) { 147 | n, _, err := mpegH264FindNALU(data[p:]) 148 | if err != nil { 149 | break 150 | } 151 | if p+n+1 > end { 152 | break 153 | } 154 | 155 | h26x[0][count] = int(data[p+n]) & 0x1F // H.264 NALU type 156 | h26x[1][count] = (int(data[p+n]) >> 1) & 0x3F // H.265 NALU type 157 | h26x[2][count] = (int(data[p+n+1]) >> 3) & 0x1F // H.266 NALU type 158 | h26x[3][count] = int(data[p+n]) // MPEG-4 VOP start code 159 | h26x[4][count] = int(data[p+n+1]) // MPEG-4 VOP coding type 160 | count++ 161 | 162 | p += n // 移动到下一个 NALU 163 | } 164 | 165 | for n := 0; n < count; n++ { 166 | h264Flags &= ^(1 << h26x[0][n]) 167 | h265Flags &= ^(1 << h26x[1][n]) 168 | h266Flags &= ^(1 << h26x[2][n]) 169 | } 170 | 171 | if h264Flags == 0 && h265Flags != 0 && h266Flags != 0 { 172 | // match SPS/PPS/IDR 173 | return CodecH264, nil 174 | } else if h265Flags == 0 && h264Flags != 0 && h266Flags != 0 { 175 | // match VPS/SPS/PPS 176 | return CodecH265, nil 177 | } else if h266Flags == 0 && h264Flags != 0 && h265Flags != 0 { 178 | // match SPS/PPS 179 | return CodecH266, nil 180 | } else if h26x[3][0] == 0xB0 && (h26x[4][0]&0x30) == 0 { 181 | // match VOP start code 182 | return CodecMpeg4, nil 183 | } 184 | 185 | return CodecUnknown, nil 186 | } 187 | func audioVerify(data []byte) PsStreamType { 188 | if data[0] == 0xFF && (data[1]&0xF0) == 0xF0 && len(data) > 7 { 189 | aacLen := ((int(data[3]) & 0x03) << 11) | 190 | (int(data[4]) << 3) | 191 | (int(data[5]) >> 5 & 0x07) 192 | if len(data) == aacLen { 193 | return PsStreamAac 194 | } 195 | } 196 | return PsStreamG711A 197 | } 198 | -------------------------------------------------------------------------------- /gb28181/ptz.go: -------------------------------------------------------------------------------- 1 | package gb28181 2 | 3 | import ( 4 | "encoding/hex" 5 | "encoding/xml" 6 | ) 7 | 8 | type MessagePtz struct { 9 | XMLName xml.Name `xml:"Control"` 10 | CmdType string `xml:"CmdType"` 11 | SN int `xml:"SN"` 12 | DeviceID string `xml:"DeviceID"` 13 | PTZCmd string `xml:"PTZCmd"` 14 | } 15 | 16 | const DeviceControl = "DeviceControl" 17 | const PTZFirstByte = 0xA5 18 | const ( 19 | PresetSet = 0x81 20 | PresetCall = 0x82 21 | PresetDel = 0x83 22 | ) 23 | 24 | const ( 25 | CruiseAdd = 0x84 26 | CruiseDel = 0x85 27 | CruiseSetSpeed = 0x86 28 | CruiseStopTime = 0x87 29 | CruiseStart = 0x88 30 | ) 31 | const ( 32 | ScanningStart = 0x89 33 | ScanningSpeed = 0x8A 34 | ) 35 | 36 | /* 37 | 表 A.3 指令格式 38 | 字节 字节1 字节2 字节3 字节4 字节5 字节6 字节7 字节8 39 | 含义 A5H 组合码1 地址 指令 数据1 数据2 组合码2 校验码 40 | 各字节定义如下: 41 | 字节1: 指令的首字节为 A5H。 42 | 字节2: 组合码1, 高4 位是版本信息, 低4 位是校验位。 本标准的版本号是1.0, 版本信息为0H。 43 | 校验位= (字节1 的高4 位+ 字节1 的低4 位+ 字节2 的高4 位) %16。 44 | 字节3: 地址的低8 位。 45 | 字节4: 指令码。 46 | 字节5、6: 数据1 和数据2。 47 | 字节7: 组合码2, 高4 位是数据3, 低4 位是地址的高4 位; 在后续叙述中, 没有特别指明的高4 位, 48 | 表示该4 位与所指定的功能无关。 49 | 字节8: 校验码, 为前面的第1~7 字节的算术和的低8 位, 即算术和对256 取模后的结果。 50 | 字节8= (字节1+ 字节2+ 字节3+ 字节4+ 字节5+ 字节6+ 字节7) %256。 51 | 地址范围000H~FFFH(即0~4095) , 其中000H 地址作为广播地址。 52 | 注: 前端设备控制中, 不使用字节3 和字节7 的低4 位地址码, 使用前端设备控制消息体中的 统一编码 53 | 标识控制的前端设备 54 | */ 55 | type PtzHead struct { 56 | FirstByte uint8 57 | AssembleByte uint8 58 | Addr uint8 //低地址码0-ff 59 | } 60 | 61 | // 获取组合码 62 | func getAssembleCode() uint8 { 63 | return (PTZFirstByte>>4 + PTZFirstByte&0xF + 0) % 16 64 | } 65 | func getVerificationCode(ptz []byte) { 66 | sum := uint8(0) 67 | for i := 0; i < len(ptz)-1; i++ { 68 | sum += ptz[i] 69 | } 70 | ptz[len(ptz)-1] = sum 71 | } 72 | 73 | /* 74 | 注1 : 字节4 中的 Bit5、Bit4 分别控制镜头变倍的缩小和放大, 字节4 中的 Bit3、Bit2、Bit1、Bit0 位分别控制云台 75 | 上、 下、 左、 右方向的转动, 相应 Bit 位置1 时, 启动云台向相应方向转动, 相应 Bit 位清0 时, 停止云台相应 76 | 方向的转动。 云台的转动方向以监视器显示图像的移动方向为准。 77 | 注2: 字节5 控制水平方向速度, 速度范围由慢到快为00H~FFH; 字节6 控制垂直方向速度, 速度范围由慢到快 78 | 为00H-FFH。 79 | 注3: 字节7 的高4 位为变焦速度, 速度范围由慢到快为0H~FH; 低4 位为地址的高4 位。 80 | */ 81 | type Ptz struct { 82 | ZoomOut bool 83 | ZoomIn bool 84 | Up bool 85 | Down bool 86 | Left bool 87 | Right bool 88 | Speed byte //0-8 89 | } 90 | 91 | func (p *Ptz) Pack() string { 92 | buf := make([]byte, 8) 93 | buf[0] = PTZFirstByte 94 | buf[1] = getAssembleCode() 95 | buf[2] = 1 96 | buf[4] = 0 97 | buf[5] = 0 98 | buf[6] = 0 99 | if p.ZoomOut { 100 | buf[3] |= 1 << 5 101 | buf[6] = p.Speed << 4 102 | } 103 | 104 | if p.ZoomIn { 105 | buf[3] |= 1 << 4 106 | buf[6] = p.Speed << 4 107 | } 108 | if p.Up { 109 | buf[3] |= 1 << 3 110 | buf[5] = p.Speed 111 | } 112 | if p.Down { 113 | buf[3] |= 1 << 2 114 | buf[5] = p.Speed 115 | } 116 | if p.Left { 117 | buf[3] |= 1 << 1 118 | buf[4] = p.Speed 119 | } 120 | if p.Right { 121 | buf[3] |= 1 122 | buf[4] = p.Speed 123 | } 124 | getVerificationCode(buf) 125 | return hex.EncodeToString(buf) 126 | } 127 | 128 | func (p *Ptz) Stop() string { 129 | buf := make([]byte, 8) 130 | buf[0] = PTZFirstByte 131 | buf[1] = getAssembleCode() 132 | buf[2] = 1 133 | buf[3] = 0 134 | buf[4] = 0 135 | buf[5] = 0 136 | buf[6] = 0 137 | getVerificationCode(buf) 138 | return hex.EncodeToString(buf) 139 | } 140 | 141 | /* 142 | 注1 : 字节4 中的 Bit3 为1 时, 光圈缩小;Bit2 为1 时, 光圈放大。 Bit1 为1 时, 聚焦近;Bit0 为1 时, 聚焦远。 Bit3~ 143 | Bit0 的相应位清0, 则相应控制操作停止动作。 144 | 注2: 字节5 表示聚焦速度, 速度范围由慢到快为00H~FFH。 145 | 注3: 字节6 表示光圈速度, 速度范围由慢到快为00H~FFH 146 | */ 147 | type Fi struct { 148 | IrisIn bool 149 | IrisOut bool 150 | FocusNear bool 151 | FocusFar bool 152 | Speed byte //0-8 153 | } 154 | 155 | func (f *Fi) Pack() string { 156 | buf := make([]byte, 8) 157 | buf[0] = PTZFirstByte 158 | buf[1] = getAssembleCode() 159 | buf[2] = 1 160 | buf[3] |= 1 << 6 161 | buf[4] = 0 162 | buf[5] = 0 163 | buf[6] = 0 164 | 165 | if f.IrisIn { 166 | buf[3] |= 1 << 3 167 | buf[5] = f.Speed 168 | } 169 | if f.IrisOut { 170 | buf[3] |= 1 << 2 171 | buf[5] = f.Speed 172 | } 173 | if f.FocusNear { 174 | buf[3] |= 1 << 1 175 | buf[4] = f.Speed 176 | } 177 | if f.FocusFar { 178 | buf[3] |= 1 179 | buf[4] = f.Speed 180 | } 181 | getVerificationCode(buf) 182 | return hex.EncodeToString(buf) 183 | } 184 | 185 | type Preset struct { 186 | CMD byte 187 | Point byte 188 | } 189 | 190 | func (p *Preset) Pack() string { 191 | buf := make([]byte, 8) 192 | buf[0] = PTZFirstByte 193 | buf[1] = getAssembleCode() 194 | buf[2] = 1 195 | 196 | buf[3] = p.CMD 197 | 198 | buf[4] = 0 199 | buf[5] = p.Point 200 | buf[6] = 0 201 | getVerificationCode(buf) 202 | return hex.EncodeToString(buf) 203 | } 204 | 205 | /* 206 | 注1 : 字节5 表示巡航组号, 字节6 表示预置位号。 207 | 注2: 序号2 中, 字节6 为00H 时, 删除对应的整条巡航; 序号3、4 中字节6 表示数据的低8 位, 字节7 的高4 位 208 | 表示数据的高4 位。 209 | 注3: 巡航停留时间的单位是秒(s) 。 210 | 注4: 停止巡航用 PTZ 指令中的字节4 的各 Bit 位均为0 的停止指令。 211 | */ 212 | type Cruise struct { 213 | CMD byte 214 | GroupNum byte 215 | Value uint16 216 | } 217 | 218 | func (c *Cruise) Pack() string { 219 | buf := make([]byte, 8) 220 | buf[0] = PTZFirstByte 221 | buf[1] = getAssembleCode() 222 | buf[2] = 1 223 | buf[3] = c.CMD 224 | 225 | buf[4] = c.GroupNum 226 | buf[5] = byte(c.Value & 0xFF) 227 | buf[6] = byte(c.Value>>8) & 0x0F 228 | getVerificationCode(buf) 229 | return hex.EncodeToString(buf) 230 | } 231 | 232 | /* 233 | 注1 : 字节5 表示扫描组号。 234 | 注2: 序号4 中, 字节6 表示数据的低8 位, 字节7 的高4 位表示数据的高4 位。 235 | 注3: 停止自动扫描用 PTZ 指令中的字节4 的各 Bit 位均为0 的停止指令。 236 | 注4: 自动扫描开始时, 整体画面从右向左移动。 237 | */ 238 | type Scanning struct { 239 | CMD byte 240 | No byte 241 | Value byte 242 | HighAddr byte // 0-f 后4位高地址码 0-f 243 | } 244 | -------------------------------------------------------------------------------- /gb28181/t_http_api.go: -------------------------------------------------------------------------------- 1 | package gb28181 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type DeviceInfos struct { 10 | DeviceItems []*DeviceItem `json:"device_items"` 11 | } 12 | type DeviceItem struct { 13 | DeviceId string `json:"device_id"` // 设备ID 14 | Channels []*ChannelItem `json:"channels"` 15 | } 16 | type ChannelItem struct { 17 | ChannelId string `json:"channel_id"` // channel id 18 | Name string `json:"name"` // 设备名称 19 | Manufacturer string `json:"manufacturer"` // 制造厂商 20 | Owner string `json:"owner"` // 设备归属 21 | CivilCode string `json:"civilCode"` // 行政区划编码 22 | Address string `json:"address"` // 地址 23 | Status ChannelStatus `json:"status"` // 状态 on 在线 off离线 24 | Longitude string `json:"longitude"` // 经度 25 | Latitude string `json:"latitude"` // 纬度 26 | StreamName string `json:"-"` 27 | } 28 | type PlayInfo struct { 29 | NetWork string `json:"network" form:"network" url:"network"` // 媒体传输类型,tcp/udp,默认udp 30 | DeviceId string `json:"device_id" form:"device_id" url:"device_id"` // 设备 Id 31 | ChannelId string `json:"channel_id" form:"channel_id" url:"channel_id"` // channel id 32 | StreamName string `json:"stream_name" form:"stream_name" url:"stream_name"` // 对应的流名 33 | SinglePort bool `json:"single_port" form:"single_port" url:"single_port"` // 是否单端口 34 | DumpFileName string `json:"dump_file_name" form:"dump_file_name" url:"dump_file_name"` // dump文件路径 35 | } 36 | type ReqPlay struct { 37 | PlayInfo 38 | } 39 | type RespPlay struct { 40 | StreamName string `json:"stream_name" form:"stream_name" url:"stream_name"` 41 | } 42 | type ReqStop struct { 43 | PlayInfo 44 | } 45 | 46 | type PtzDirection struct { 47 | DeviceId string `json:"device_id" form:"device_id" url:"device_id"` // 设备 Id 48 | ChannelId string `json:"channel_id" form:"channel_id" url:"channel_id"` // channel id 49 | Up bool `json:"up" form:"up" url:"up"` 50 | Down bool `json:"down" form:"down" url:"down"` 51 | Left bool `json:"left" form:"left" url:"left"` 52 | Right bool `json:"right" form:"right" url:"right"` 53 | Speed byte `json:"speed" form:"speed" url:"speed"` //0-8 54 | } 55 | type PtzZoom struct { 56 | DeviceId string `json:"device_id" form:"device_id" url:"device_id"` // 设备 Id 57 | ChannelId string `json:"channel_id" form:"channel_id" url:"channel_id"` // channel id 58 | ZoomOut bool `json:"zoom_out" form:"zoom_out" url:"zoom_out"` 59 | ZoomIn bool `json:"zoom_in" form:"zoom_in" url:"zoom_in"` 60 | Speed byte `json:"speed" form:"speed" url:"speed"` //0-8 61 | } 62 | type PtzFi struct { 63 | DeviceId string `json:"device_id" form:"device_id" url:"device_id"` // 设备 Id 64 | ChannelId string `json:"channel_id" form:"channel_id" url:"channel_id"` // channel id 65 | IrisIn bool `json:"iris_in" form:"iris_in" url:"iris_in"` 66 | IrisOut bool `json:"iris_out" form:"iris_out" url:"iris_out"` 67 | FocusNear bool `json:"focus_near" form:"focus_near" url:"focus_near"` 68 | FocusFar bool `json:"focus_far" form:"focus_far" url:"focus_far"` 69 | Speed byte `json:"speed" form:"speed" url:"speed"` //0-8 70 | } 71 | type PresetCmd byte 72 | 73 | const ( 74 | PresetEditPoint PresetCmd = iota 75 | PresetDelPoint 76 | PresetCallPoint 77 | ) 78 | 79 | type PtzPreset struct { 80 | DeviceId string `json:"device_id" form:"device_id" url:"device_id"` // 设备 Id 81 | ChannelId string `json:"channel_id" form:"channel_id" url:"channel_id"` // channel id 82 | Cmd PresetCmd `json:"cmd" form:"cmd" url:"cmd"` 83 | Point byte `json:"point" form:"point" url:"point"` 84 | } 85 | type PtzStop struct { 86 | DeviceId string `json:"device_id" form:"device_id" url:"device_id"` // 设备 Id 87 | ChannelId string `json:"channel_id" form:"channel_id" url:"channel_id"` // channel id 88 | } 89 | type ReqUpdateNotify struct { 90 | DeviceId string `json:"device_id" form:"device_id" url:"device_id"` //设备 Id 91 | } 92 | 93 | func ResponseErrorWithMsg(c *gin.Context, code ResCode, msg interface{}) { 94 | c.JSON(http.StatusOK, &ResponseData{ 95 | Code: code, 96 | Msg: msg, 97 | Data: nil, 98 | }) 99 | } 100 | 101 | func ResponseSuccess(c *gin.Context, data interface{}) { 102 | c.JSON(http.StatusOK, &ResponseData{ 103 | Code: CodeSuccess, 104 | Msg: CodeSuccess.Msg(), 105 | Data: data, 106 | }) 107 | } 108 | 109 | type ResCode int64 110 | 111 | const ( 112 | CodeSuccess ResCode = 1000 + iota 113 | CodeInvalidParam 114 | CodeServerBusy 115 | CodeDeviceNotRegister 116 | CodeDeviceStopError 117 | ) 118 | 119 | var codeMsgMap = map[ResCode]string{ 120 | CodeSuccess: "success", 121 | CodeInvalidParam: "请求参数错误", 122 | CodeServerBusy: "服务繁忙", 123 | CodeDeviceNotRegister: "设备暂时未注册", 124 | CodeDeviceStopError: "设备停止播放错误", 125 | } 126 | 127 | const ( 128 | SpeedParamError = "speed 范围(0,8]" 129 | PointParamError = "point 范围(0,50]" 130 | ) 131 | 132 | func (c ResCode) Msg() string { 133 | msg, ok := codeMsgMap[c] 134 | if !ok { 135 | msg = codeMsgMap[CodeServerBusy] 136 | } 137 | return msg 138 | } 139 | 140 | type ResponseData struct { 141 | Code ResCode `json:"code"` 142 | Msg interface{} `json:"msg"` 143 | Data interface{} `json:"data,omitempty"` 144 | } 145 | -------------------------------------------------------------------------------- /gb28181/util.go: -------------------------------------------------------------------------------- 1 | package gb28181 2 | 3 | import ( 4 | "bytes" 5 | "encoding/xml" 6 | "golang.org/x/net/html/charset" 7 | "golang.org/x/text/encoding/simplifiedchinese" 8 | "golang.org/x/text/transform" 9 | "io/ioutil" 10 | "math/rand" 11 | "time" 12 | ) 13 | 14 | func RandNumString(n int) string { 15 | numbers := "0123456789" 16 | return randStringBySoure(numbers, n) 17 | } 18 | 19 | func RandString(n int) string { 20 | letterBytes := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 21 | return randStringBySoure(letterBytes, n) 22 | } 23 | 24 | // https://github.com/kpbird/golang_random_string 25 | func randStringBySoure(src string, n int) string { 26 | randomness := make([]byte, n) 27 | 28 | rand.Seed(time.Now().UnixNano()) 29 | _, err := rand.Read(randomness) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | l := len(src) 35 | 36 | // fill output 37 | output := make([]byte, n) 38 | for pos := range output { 39 | random := randomness[pos] 40 | randomPos := random % uint8(l) 41 | output[pos] = src[randomPos] 42 | } 43 | 44 | return string(output) 45 | } 46 | 47 | func DecodeGbk(v interface{}, body []byte) error { 48 | bodyBytes, err := GbkToUtf8(body) 49 | if err != nil { 50 | return err 51 | } 52 | decoder := xml.NewDecoder(bytes.NewReader(bodyBytes)) 53 | decoder.CharsetReader = charset.NewReaderLabel 54 | err = decoder.Decode(v) 55 | return err 56 | } 57 | 58 | func GbkToUtf8(s []byte) ([]byte, error) { 59 | reader := transform.NewReader(bytes.NewReader(s), simplifiedchinese.GBK.NewDecoder()) 60 | d, e := ioutil.ReadAll(reader) 61 | if e != nil { 62 | return s, e 63 | } 64 | return d, nil 65 | } 66 | -------------------------------------------------------------------------------- /gb28181/xml.go: -------------------------------------------------------------------------------- 1 | package gb28181 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | ) 7 | 8 | var ( 9 | // CatalogXML 获取设备列表xml样式 10 | CatalogXML = ` 11 | Catalog 12 | %d 13 | %s 14 | 15 | ` 16 | // RecordInfoXML 获取录像文件列表xml样式 17 | RecordInfoXML = ` 18 | 19 | RecordInfo 20 | %d 21 | %s 22 | %s 23 | %s 24 | 0 25 | all 26 | 27 | ` 28 | // DeviceInfoXML 查询设备详情xml样式 29 | DeviceInfoXML = ` 30 | 31 | DeviceInfo 32 | %d 33 | %s 34 | 35 | ` 36 | // DevicePositionXML 订阅设备位置 37 | DevicePositionXML = ` 38 | 39 | MobilePosition 40 | %d 41 | %s 42 | %d 43 | ` 44 | ) 45 | 46 | func BuildCatalogXML(sn int, id string) string { 47 | return fmt.Sprintf(CatalogXML, sn, id) 48 | } 49 | 50 | // AlarmResponseXML alarm response xml样式 51 | var ( 52 | AlarmResponseXML = ` 53 | 54 | Alarm 55 | 17430 56 | %s 57 | 58 | ` 59 | ) 60 | 61 | // BuildRecordInfoXML 获取录像文件列表指令 62 | func BuildAlarmResponseXML(id string) string { 63 | return fmt.Sprintf(AlarmResponseXML, id) 64 | } 65 | 66 | func BuildDeviceInfoXML(sn int, id string) string { 67 | return fmt.Sprintf(DeviceInfoXML, sn, id) 68 | } 69 | 70 | func XmlEncode(v interface{}) (string, error) { 71 | xmlData, err := xml.MarshalIndent(v, "", " ") 72 | if err != nil { 73 | return "", err 74 | } 75 | xml := string(xmlData) 76 | xml = `` + "\n" + xml + "\n" 77 | return xml, err 78 | } 79 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/q191201771/lalmax 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/bluenviron/gohlslib v1.3.0 7 | github.com/bluenviron/gortsplib/v4 v4.8.0 8 | github.com/bluenviron/mediacommon v1.9.2 9 | github.com/datarhei/gosrt v0.5.4 10 | github.com/ghettovoice/gosip v0.0.0-20230802091127-d58873a3fe44 11 | github.com/gin-gonic/gin v1.9.1 12 | github.com/gofrs/uuid v4.4.0+incompatible 13 | github.com/livekit/livekit-server v1.7.0 14 | github.com/pion/ice/v2 v2.3.24 15 | github.com/pion/interceptor v0.1.29 16 | github.com/pion/rtp v1.8.6 17 | github.com/pion/transport/v3 v3.0.2 18 | github.com/pion/webrtc/v3 v3.2.40 19 | github.com/q191201771/lal v0.37.4 20 | github.com/q191201771/naza v0.30.48 21 | github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b 22 | github.com/smallnest/chanx v1.2.0 23 | github.com/use-go/onvif v0.0.9 24 | github.com/yapingcat/gomedia v0.0.0-20240316172424-76660eca7389 25 | golang.org/x/net v0.26.0 26 | golang.org/x/text v0.16.0 27 | ) 28 | 29 | require ( 30 | github.com/Eyevinn/mp4ff v0.47.0 // indirect 31 | github.com/abema/go-mp4 v1.2.0 // indirect 32 | github.com/asticode/go-astikit v0.30.0 // indirect 33 | github.com/asticode/go-astits v1.13.0 // indirect 34 | github.com/avast/retry-go/v4 v4.6.0 // indirect 35 | github.com/beevik/etree v1.1.0 // indirect 36 | github.com/benbjohnson/clock v1.3.5 // indirect 37 | github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c // indirect 38 | github.com/beorn7/perks v1.0.1 // indirect 39 | github.com/bep/debounce v1.2.1 // indirect 40 | github.com/bytedance/sonic v1.9.1 // indirect 41 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 42 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 43 | github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect 44 | github.com/d5/tengo/v2 v2.17.0 // indirect 45 | github.com/davecgh/go-spew v1.1.1 // indirect 46 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 47 | github.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca // indirect 48 | github.com/eapache/channels v1.1.0 // indirect 49 | github.com/eapache/queue v1.1.0 // indirect 50 | github.com/elgs/gostrgen v0.0.0-20161222160715-9d61ae07eeae // indirect 51 | github.com/elliotchance/orderedmap/v2 v2.2.0 // indirect 52 | github.com/florianl/go-tc v0.4.3 // indirect 53 | github.com/frostbyte73/core v0.0.10 // indirect 54 | github.com/fsnotify/fsnotify v1.7.0 // indirect 55 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 56 | github.com/gammazero/deque v0.2.1 // indirect 57 | github.com/gammazero/workerpool v1.1.3 // indirect 58 | github.com/gin-contrib/sse v0.1.0 // indirect 59 | github.com/go-jose/go-jose/v3 v3.0.3 // indirect 60 | github.com/go-logr/logr v1.4.2 // indirect 61 | github.com/go-playground/locales v0.14.1 // indirect 62 | github.com/go-playground/universal-translator v0.18.1 // indirect 63 | github.com/go-playground/validator/v10 v10.14.0 // indirect 64 | github.com/gobwas/httphead v0.1.0 // indirect 65 | github.com/gobwas/pool v0.2.1 // indirect 66 | github.com/gobwas/ws v1.1.0-rc.1 // indirect 67 | github.com/goccy/go-json v0.10.2 // indirect 68 | github.com/google/go-cmp v0.6.0 // indirect 69 | github.com/google/uuid v1.6.0 // indirect 70 | github.com/google/wire v0.6.0 // indirect 71 | github.com/gorilla/websocket v1.5.3 // indirect 72 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 73 | github.com/hashicorp/go-retryablehttp v0.7.5 // indirect 74 | github.com/hashicorp/go-version v1.7.0 // indirect 75 | github.com/hashicorp/golang-lru v0.5.4 // indirect 76 | github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 77 | github.com/jellydator/ttlcache/v3 v3.2.0 // indirect 78 | github.com/josharian/native v1.1.0 // indirect 79 | github.com/json-iterator/go v1.1.12 // indirect 80 | github.com/juju/errors v0.0.0-20220331221717-b38fca44723b // indirect 81 | github.com/jxskiss/base62 v1.1.0 // indirect 82 | github.com/klauspost/compress v1.17.8 // indirect 83 | github.com/klauspost/cpuid/v2 v2.2.6 // indirect 84 | github.com/leodido/go-urn v1.2.4 // indirect 85 | github.com/lithammer/shortuuid/v4 v4.0.0 // indirect 86 | github.com/livekit/mageutil v0.0.0-20230125210925-54e8a70427c1 // indirect 87 | github.com/livekit/mediatransportutil v0.0.0-20240622055623-ce8d272f389e // indirect 88 | github.com/livekit/protocol v1.19.0 // indirect 89 | github.com/livekit/psrpc v0.5.3-0.20240526192918-fbdaf10e6aa5 // indirect 90 | github.com/mackerelio/go-osstat v0.2.5 // indirect 91 | github.com/mattn/go-colorable v0.1.4 // indirect 92 | github.com/mattn/go-isatty v0.0.19 // indirect 93 | github.com/mdlayher/netlink v1.7.1 // indirect 94 | github.com/mdlayher/socket v0.4.0 // indirect 95 | github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect 96 | github.com/mitchellh/go-homedir v1.1.0 // indirect 97 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 98 | github.com/modern-go/reflect2 v1.0.2 // indirect 99 | github.com/nats-io/nats.go v1.35.0 // indirect 100 | github.com/nats-io/nkeys v0.4.7 // indirect 101 | github.com/nats-io/nuid v1.0.1 // indirect 102 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 103 | github.com/pion/datachannel v1.5.5 // indirect 104 | github.com/pion/dtls/v2 v2.2.11 // indirect 105 | github.com/pion/logging v0.2.2 // indirect 106 | github.com/pion/mdns v0.0.12 // indirect 107 | github.com/pion/randutil v0.1.0 // indirect 108 | github.com/pion/rtcp v1.2.14 // indirect 109 | github.com/pion/sctp v1.8.16 // indirect 110 | github.com/pion/sdp/v3 v3.0.9 // indirect 111 | github.com/pion/srtp/v2 v2.0.18 // indirect 112 | github.com/pion/stun v0.6.1 // indirect 113 | github.com/pion/transport/v2 v2.2.5 // indirect 114 | github.com/pion/turn/v2 v2.1.6 // indirect 115 | github.com/pkg/errors v0.9.1 // indirect 116 | github.com/pmezard/go-difflib v1.0.0 // indirect 117 | github.com/prometheus/client_golang v1.19.1 // indirect 118 | github.com/prometheus/client_model v0.5.0 // indirect 119 | github.com/prometheus/common v0.48.0 // indirect 120 | github.com/prometheus/procfs v0.12.0 // indirect 121 | github.com/puzpuzpuz/xsync/v3 v3.1.0 // indirect 122 | github.com/redis/go-redis/v9 v9.5.3 // indirect 123 | github.com/rs/cors v1.11.0 // indirect 124 | github.com/rs/zerolog v1.26.1 // indirect 125 | github.com/russross/blackfriday/v2 v2.1.0 // indirect 126 | github.com/sirupsen/logrus v1.9.3 // indirect 127 | github.com/stretchr/testify v1.9.0 // indirect 128 | github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5 // indirect 129 | github.com/thoas/go-funk v0.9.3 // indirect 130 | github.com/twitchtv/twirp v8.1.3+incompatible // indirect 131 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 132 | github.com/ua-parser/uap-go v0.0.0-20240611065828-3a4781585db6 // indirect 133 | github.com/ugorji/go/codec v1.2.11 // indirect 134 | github.com/urfave/cli/v2 v2.27.2 // indirect 135 | github.com/urfave/negroni/v3 v3.1.1 // indirect 136 | github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect 137 | github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect 138 | github.com/zeebo/xxh3 v1.0.2 // indirect 139 | go.uber.org/atomic v1.11.0 // indirect 140 | go.uber.org/multierr v1.11.0 // indirect 141 | go.uber.org/zap v1.27.0 // indirect 142 | go.uber.org/zap/exp v0.2.0 // indirect 143 | golang.org/x/arch v0.3.0 // indirect 144 | golang.org/x/crypto v0.24.0 // indirect 145 | golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect 146 | golang.org/x/sync v0.7.0 // indirect 147 | golang.org/x/sys v0.21.0 // indirect 148 | golang.org/x/term v0.21.0 // indirect 149 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240521202816-d264139d666e // indirect 150 | google.golang.org/grpc v1.64.0 // indirect 151 | google.golang.org/protobuf v1.34.2 // indirect 152 | gopkg.in/yaml.v2 v2.4.0 // indirect 153 | gopkg.in/yaml.v3 v3.0.1 // indirect 154 | ) 155 | -------------------------------------------------------------------------------- /hook/gop_cache.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import "github.com/q191201771/lal/pkg/base" 4 | 5 | // GopCache gop cache 6 | type GopCache struct { 7 | videoheader *base.RtmpMsg 8 | audioheader *base.RtmpMsg 9 | 10 | gopSize int 11 | singleGopMaxFrameNum int 12 | 13 | data []Gop 14 | first int 15 | last int 16 | } 17 | 18 | // NewGopCache 创建 gop 缓存 19 | func NewGopCache(gopSize, singleGopMaxFrameNum int) *GopCache { 20 | if gopSize < 0 { 21 | gopSize = 0 22 | } 23 | num := gopSize + 1 24 | return &GopCache{ 25 | data: make([]Gop, num), 26 | gopSize: num, 27 | singleGopMaxFrameNum: singleGopMaxFrameNum, 28 | } 29 | } 30 | 31 | // Feed 写入缓存 32 | func (c *GopCache) Feed(msg base.RtmpMsg) { 33 | switch msg.Header.MsgTypeId { 34 | case base.RtmpTypeIdMetadata: 35 | return 36 | case base.RtmpTypeIdAudio: 37 | if msg.IsAacSeqHeader() { 38 | c.audioheader = &msg 39 | return 40 | } 41 | if msg.AudioCodecId() == base.RtmpSoundFormatG711A || msg.AudioCodecId() == base.RtmpSoundFormatG711U || msg.AudioCodecId() == base.RtmpSoundFormatOpus { 42 | c.audioheader = &msg 43 | } 44 | case base.RtmpTypeIdVideo: 45 | if msg.IsVideoKeySeqHeader() { 46 | c.videoheader = &msg 47 | return 48 | } 49 | } 50 | 51 | if c.gopSize > 1 { 52 | if msg.IsVideoKeyNalu() { 53 | c.feedNewGop(msg) 54 | } else { 55 | c.feedLastGop(msg) 56 | } 57 | } 58 | } 59 | 60 | func (c *GopCache) feedNewGop(msg base.RtmpMsg) { 61 | if c.isGopRingFull() { 62 | c.first = (c.first + 1) % c.gopSize 63 | } 64 | c.data[c.last].clear() 65 | c.data[c.last].feed(msg) 66 | c.last = (c.last + 1) % c.gopSize 67 | } 68 | 69 | func (c *GopCache) feedLastGop(msg base.RtmpMsg) { 70 | if c.isGopRingEmpty() { 71 | return 72 | } 73 | 74 | idx := (c.last - 1 + c.gopSize) % c.gopSize 75 | if c.singleGopMaxFrameNum == 0 || c.data[idx].size() <= c.singleGopMaxFrameNum { 76 | c.data[idx].feed(msg) 77 | } 78 | } 79 | 80 | func (c *GopCache) isGopRingFull() bool { 81 | return (c.last+1)%c.gopSize == c.first 82 | } 83 | func (c *GopCache) isGopRingEmpty() bool { 84 | return c.first == c.last 85 | } 86 | 87 | func (c *GopCache) Clear() { 88 | // c.audioheader = nil 89 | // c.videoheader = nil 90 | c.last = 0 91 | c.first = 0 92 | } 93 | 94 | func (c *GopCache) GetGopCount() int { 95 | return (c.last + c.gopSize - c.first) % c.gopSize 96 | } 97 | 98 | func (c *GopCache) GetGopDataAt(pos int) []base.RtmpMsg { 99 | if pos >= c.GetGopCount() || pos < 0 { 100 | return nil 101 | } 102 | return c.data[(c.first+pos)%c.gopSize].data 103 | } 104 | 105 | type Gop struct { 106 | data []base.RtmpMsg 107 | } 108 | 109 | func (g *Gop) feed(msg base.RtmpMsg) { 110 | g.data = append(g.data, msg) 111 | } 112 | 113 | func (g *Gop) clear() { 114 | if len(g.data) == 0 { 115 | return 116 | } 117 | g.data = g.data[:0] 118 | } 119 | func (g *Gop) size() int { 120 | return len(g.data) 121 | } 122 | -------------------------------------------------------------------------------- /hook/hookmanager.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/q191201771/naza/pkg/nazalog" 7 | ) 8 | 9 | type HookSessionMangaer struct { 10 | sessionMap sync.Map 11 | } 12 | 13 | var ( 14 | manager *HookSessionMangaer 15 | once sync.Once 16 | ) 17 | 18 | func GetHookSessionManagerInstance() *HookSessionMangaer { 19 | once.Do(func() { 20 | manager = &HookSessionMangaer{} 21 | }) 22 | 23 | return manager 24 | } 25 | 26 | func (m *HookSessionMangaer) SetHookSession(streamName string, session *HookSession) { 27 | nazalog.Info("SetHookSession, streamName:", streamName) 28 | m.sessionMap.Store(streamName, session) 29 | } 30 | 31 | func (m *HookSessionMangaer) RemoveHookSession(streamName string) { 32 | nazalog.Info("RemoveHookSession, streamName:", streamName) 33 | // s, ok := m.sessionMap.Load(streamName) 34 | // if ok { 35 | m.sessionMap.Delete(streamName) 36 | // } 37 | } 38 | 39 | func (m *HookSessionMangaer) GetHookSession(streamName string) (bool, *HookSession) { 40 | s, ok := m.sessionMap.Load(streamName) 41 | if ok { 42 | return true, s.(*HookSession) 43 | } 44 | 45 | return false, nil 46 | } 47 | -------------------------------------------------------------------------------- /hook/hooksession.go: -------------------------------------------------------------------------------- 1 | package hook 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | 7 | "github.com/q191201771/lalmax/fmp4/hls" 8 | 9 | "github.com/q191201771/lal/pkg/base" 10 | "github.com/q191201771/naza/pkg/nazalog" 11 | ) 12 | 13 | var _ base.ISession = (*consumerInfo)(nil) 14 | 15 | type IHookSessionSubscriber interface { 16 | OnMsg(msg base.RtmpMsg) 17 | OnStop() 18 | } 19 | 20 | type HookSession struct { 21 | uniqueKey string 22 | streamName string 23 | consumers sync.Map 24 | hlssvr *hls.HlsServer 25 | gopCache *GopCache 26 | hasVideo bool 27 | } 28 | 29 | type consumerInfo struct { 30 | subscriber IHookSessionSubscriber 31 | hasSendVideo bool 32 | 33 | base.StatSession 34 | } 35 | 36 | // AppName implements base.ISession. 37 | func (c *consumerInfo) AppName() string { 38 | return c.SessionId 39 | } 40 | 41 | // GetStat implements base.ISession. 42 | func (c *consumerInfo) GetStat() base.StatSession { 43 | return c.StatSession 44 | } 45 | 46 | // IsAlive implements base.ISession. 47 | func (c *consumerInfo) IsAlive() (readAlive bool, writeAlive bool) { 48 | return true, true 49 | } 50 | 51 | // RawQuery implements base.ISession. 52 | func (c *consumerInfo) RawQuery() string { 53 | return "" 54 | } 55 | 56 | // StreamName implements base.ISession. 57 | func (c *consumerInfo) StreamName() string { 58 | return c.SessionId 59 | } 60 | 61 | // UniqueKey implements base.ISession. 62 | func (c *consumerInfo) UniqueKey() string { 63 | return c.SessionId 64 | } 65 | 66 | // UpdateStat implements base.ISession. 67 | func (c *consumerInfo) UpdateStat(intervalSec uint32) { 68 | } 69 | 70 | // Url implements base.ISession. 71 | func (*consumerInfo) Url() string { 72 | return "" 73 | } 74 | 75 | func NewHookSession(uniqueKey, streamName string, hlssvr *hls.HlsServer, gopNum, singleGopMaxFrameNum int) *HookSession { 76 | s := &HookSession{ 77 | uniqueKey: uniqueKey, 78 | streamName: streamName, 79 | hlssvr: hlssvr, 80 | gopCache: NewGopCache(gopNum, singleGopMaxFrameNum), 81 | } 82 | 83 | if s.hlssvr != nil { 84 | s.hlssvr.NewHlsSession(streamName) 85 | } 86 | 87 | nazalog.Infof("create hook session, uniqueKey:%s, streamName:%s", uniqueKey, streamName) 88 | 89 | GetHookSessionManagerInstance().SetHookSession(streamName, s) 90 | return s 91 | } 92 | 93 | func (session *HookSession) OnMsg(msg base.RtmpMsg) { 94 | if session.hlssvr != nil { 95 | session.hlssvr.OnMsg(session.streamName, msg) 96 | } 97 | 98 | session.consumers.Range(func(key, value interface{}) bool { 99 | c := value.(*consumerInfo) 100 | 101 | gopCount := session.gopCache.GetGopCount() 102 | if !c.hasSendVideo && gopCount > 0 { 103 | if v := session.GetVideoSeqHeaderMsg(); v != nil { 104 | c.subscriber.OnMsg(*v) 105 | } 106 | if v := session.GetAudioSeqHeaderMsg(); v != nil { 107 | c.subscriber.OnMsg(*v) 108 | } 109 | for i := 0; i < gopCount; i++ { 110 | for _, item := range session.gopCache.GetGopDataAt(i) { 111 | c.subscriber.OnMsg(item) 112 | } 113 | } 114 | c.hasSendVideo = true 115 | } 116 | 117 | if msg.Header.MsgTypeId == base.RtmpTypeIdVideo { 118 | if !c.hasSendVideo { 119 | if !msg.IsVideoKeyNalu() { 120 | return true 121 | } 122 | if v := session.GetVideoSeqHeaderMsg(); v != nil { 123 | c.subscriber.OnMsg(*v) 124 | } 125 | if v := session.GetAudioSeqHeaderMsg(); v != nil { 126 | c.subscriber.OnMsg(*v) 127 | } 128 | c.hasSendVideo = true 129 | } 130 | 131 | c.subscriber.OnMsg(msg) 132 | } else if msg.Header.MsgTypeId == base.RtmpTypeIdAudio { 133 | if !session.hasVideo || c.hasSendVideo { 134 | c.subscriber.OnMsg(msg) 135 | } 136 | } 137 | return true 138 | }) 139 | 140 | if !session.hasVideo && msg.IsVideoKeyNalu() { 141 | session.hasVideo = true 142 | } 143 | 144 | session.gopCache.Feed(msg) 145 | } 146 | 147 | func (session *HookSession) OnStop() { 148 | if session.hlssvr != nil { 149 | session.hlssvr.OnStop(session.streamName) 150 | } 151 | 152 | nazalog.Debugf("OnStop, uniqueKey:%s, streamName:%s", session.uniqueKey, session.streamName) 153 | session.consumers.Range(func(key, value interface{}) bool { 154 | c := value.(*consumerInfo) 155 | c.subscriber.OnStop() 156 | return true 157 | }) 158 | 159 | GetHookSessionManagerInstance().RemoveHookSession(session.streamName) 160 | } 161 | 162 | func (session *HookSession) AddConsumer(consumerId string, subscriber IHookSessionSubscriber) { 163 | 164 | info := &consumerInfo{ 165 | subscriber: subscriber, 166 | StatSession: base.StatSession{ 167 | SessionId: consumerId, 168 | StartTime: time.Now().Format(time.DateTime), 169 | // Protocol: , TODO: (xugo)需要传递更多的参数来填充数据 170 | }, 171 | } 172 | 173 | nazalog.Info("AddConsumer, consumerId:", consumerId) 174 | session.consumers.Store(consumerId, info) 175 | } 176 | 177 | func (session *HookSession) GetAllConsumer() []base.StatSub { 178 | out := make([]base.StatSub, 0, 10) 179 | session.consumers.Range(func(key, value any) bool { 180 | v, ok := value.(*consumerInfo) 181 | if ok { 182 | // TODO: (xugo)先简单实现,此处需要优化数据准确性 183 | out = append(out, base.Session2StatSub(v)) 184 | } 185 | return true 186 | }) 187 | return out 188 | } 189 | 190 | func (session *HookSession) RemoveConsumer(consumerId string) { 191 | _, ok := session.consumers.Load(consumerId) 192 | if ok { 193 | nazalog.Info("RemoveConsumer, consumerId:", consumerId) 194 | session.consumers.Delete(consumerId) 195 | } 196 | } 197 | 198 | func (session *HookSession) GetVideoSeqHeaderMsg() *base.RtmpMsg { 199 | return session.gopCache.videoheader 200 | } 201 | 202 | func (session *HookSession) GetAudioSeqHeaderMsg() *base.RtmpMsg { 203 | return session.gopCache.audioheader 204 | } 205 | -------------------------------------------------------------------------------- /image/gb-hk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/q191201771/lalmax/735eb5bd13388e731a73dad500ac366c87f29de6/image/gb-hk.png -------------------------------------------------------------------------------- /image/init.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/q191201771/lalmax/735eb5bd13388e731a73dad500ac366c87f29de6/image/init.png -------------------------------------------------------------------------------- /image/rtc_01.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/q191201771/lalmax/735eb5bd13388e731a73dad500ac366c87f29de6/image/rtc_01.jpeg -------------------------------------------------------------------------------- /image/rtc_02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/q191201771/lalmax/735eb5bd13388e731a73dad500ac366c87f29de6/image/rtc_02.png -------------------------------------------------------------------------------- /image/srt_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/q191201771/lalmax/735eb5bd13388e731a73dad500ac366c87f29de6/image/srt_0.png -------------------------------------------------------------------------------- /image/srt_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/q191201771/lalmax/735eb5bd13388e731a73dad500ac366c87f29de6/image/srt_1.png -------------------------------------------------------------------------------- /image/srt_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/q191201771/lalmax/735eb5bd13388e731a73dad500ac366c87f29de6/image/srt_2.png -------------------------------------------------------------------------------- /image/srt_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/q191201771/lalmax/735eb5bd13388e731a73dad500ac366c87f29de6/image/srt_3.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/q191201771/lalmax/server" 10 | 11 | "github.com/q191201771/naza/pkg/nazalog" 12 | 13 | "github.com/q191201771/lal/pkg/base" 14 | 15 | config "github.com/q191201771/lalmax/conf" 16 | 17 | "github.com/q191201771/naza/pkg/bininfo" 18 | ) 19 | 20 | func main() { 21 | defer nazalog.Sync() 22 | 23 | confFilename := parseFlag() 24 | err := config.Open(confFilename) 25 | if err != nil { 26 | nazalog.Errorf("open config failed, configname:%+v", confFilename) 27 | return 28 | } 29 | 30 | maxConf := config.GetConfig() 31 | 32 | svr, err := server.NewLalMaxServer(maxConf) 33 | if err != nil { 34 | nazalog.Fatalf("create lalmax server failed. err=%+v", err) 35 | } 36 | 37 | if err = svr.Run(); err != nil { 38 | nazalog.Infof("server manager done. err=%+v", err) 39 | } 40 | } 41 | 42 | func parseFlag() string { 43 | binInfoFlag := flag.Bool("v", false, "show bin info") 44 | cf := flag.String("c", "", "specify conf file") 45 | p := flag.String("p", "", "specify current work directory") 46 | flag.Parse() 47 | 48 | if *binInfoFlag { 49 | _, _ = fmt.Fprint(os.Stderr, bininfo.StringifyMultiLine()) 50 | _, _ = fmt.Fprintln(os.Stderr, base.LalFullInfo) 51 | os.Exit(0) 52 | } 53 | if *p != "" { 54 | os.Chdir(*p) 55 | } 56 | if *cf != "" { 57 | return *cf 58 | } 59 | nazalog.Warnf("config file did not specify in the command line, try to load it in the usual path.") 60 | defaultConfigFileList := []string{ 61 | filepath.FromSlash("lalmax.conf.json"), 62 | filepath.FromSlash("./conf/lalmax.conf.json"), 63 | filepath.FromSlash("../conf/lalmax.conf.json"), 64 | } 65 | for _, dcf := range defaultConfigFileList { 66 | fi, err := os.Stat(dcf) 67 | if err == nil && fi.Size() > 0 && !fi.IsDir() { 68 | nazalog.Warnf("%s exist. using it as config file.", dcf) 69 | return dcf 70 | } else { 71 | nazalog.Warnf("%s not exist.", dcf) 72 | } 73 | } 74 | 75 | // 默认位置都没有,退出程序 76 | flag.Usage() 77 | _, _ = fmt.Fprintf(os.Stderr, ` 78 | Example: 79 | %s -c %s 80 | `, os.Args[0], filepath.FromSlash("./conf/lalmax.conf.json")) 81 | base.OsExitAndWaitPressIfWindows(1) 82 | return *cf 83 | } 84 | -------------------------------------------------------------------------------- /onvif/server.go: -------------------------------------------------------------------------------- 1 | package onvif 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "strings" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/q191201771/lal/pkg/base" 14 | "github.com/q191201771/naza/pkg/nazalog" 15 | goonvif "github.com/use-go/onvif" 16 | "github.com/use-go/onvif/device" 17 | media "github.com/use-go/onvif/media" 18 | sdk "github.com/use-go/onvif/sdk/device" 19 | sdkmedia "github.com/use-go/onvif/sdk/media" 20 | onvifcmd "github.com/use-go/onvif/xsd/onvif" 21 | ) 22 | 23 | type OnvifPullRequest struct { 24 | Addr string `json:"addr"` // 摄像机IP:PORT 25 | Username string `json:"username"` // 用户名 26 | Password string `json:"password"` // 密码 27 | RtspMode int `json:"rtspmode"` // rtsp拉流模式,0-tcp, 1-udp 28 | PullAllProfiles bool `json:"pullallprofiles"` // 是否请求所有profiles 29 | } 30 | 31 | type OnvifServer struct { 32 | } 33 | 34 | func NewOnvifServer() *OnvifServer { 35 | return &OnvifServer{} 36 | } 37 | 38 | func (s *OnvifServer) HandlePull(c *gin.Context) { 39 | pullreq := OnvifPullRequest{} 40 | err := c.ShouldBind(&pullreq) 41 | if err != nil { 42 | c.Status(http.StatusBadRequest) 43 | return 44 | } 45 | 46 | dev, err := goonvif.NewDevice(goonvif.DeviceParams{ 47 | Xaddr: pullreq.Addr, 48 | Username: pullreq.Username, 49 | Password: pullreq.Password, 50 | }) 51 | 52 | if err != nil { 53 | nazalog.Error(err) 54 | return 55 | } 56 | 57 | deviceInfoReq := device.GetDeviceInformation{} 58 | deviceInfoRes, err := sdk.Call_GetDeviceInformation(context.Background(), dev, deviceInfoReq) 59 | if err != nil { 60 | nazalog.Error(err) 61 | return 62 | } 63 | 64 | getCapabilities := device.GetCapabilities{Category: "All"} 65 | _, err = sdk.Call_GetCapabilities(context.Background(), dev, getCapabilities) 66 | if err != nil { 67 | nazalog.Error("Call_GetCapabilities failed, err:", err) 68 | c.Status(http.StatusInternalServerError) 69 | return 70 | } 71 | 72 | profilesReq := media.GetProfiles{} 73 | profilesRes, err := sdkmedia.Call_GetProfiles(context.Background(), dev, profilesReq) 74 | if err != nil { 75 | nazalog.Error("Call_GetProfiles failed, err:", err) 76 | c.Status(http.StatusInternalServerError) 77 | return 78 | } 79 | 80 | if len(profilesRes.Profiles) == 0 { 81 | nazalog.Error("profilesRes.Profiles invalid") 82 | c.Status(http.StatusInternalServerError) 83 | return 84 | } 85 | 86 | var protocol onvifcmd.TransportProtocol 87 | if pullreq.RtspMode == 1 { 88 | protocol = "UDP" 89 | } else { 90 | protocol = "TCP" 91 | } 92 | 93 | if pullreq.PullAllProfiles { 94 | for _, profile := range profilesRes.Profiles { 95 | streamUrlReq := media.GetStreamUri{ 96 | ProfileToken: profile.Token, 97 | StreamSetup: onvifcmd.StreamSetup{ 98 | Stream: "RTP-Unicast", 99 | Transport: onvifcmd.Transport{ 100 | Protocol: protocol, 101 | }, 102 | }, 103 | } 104 | streamUrlRes, err := sdkmedia.Call_GetStreamUri(context.Background(), dev, streamUrlReq) 105 | if err != nil { 106 | nazalog.Error(err) 107 | return 108 | } 109 | 110 | playUrl := buildPlayUrl(string(streamUrlRes.MediaUri.Uri), pullreq.Username, pullreq.Password) 111 | DoPull(playUrl, fmt.Sprintf("%s-%s", deviceInfoRes.Model, profile.Name), pullreq.RtspMode) 112 | } 113 | } else { 114 | streamUrlReq := media.GetStreamUri{ 115 | ProfileToken: profilesRes.Profiles[0].Token, 116 | StreamSetup: onvifcmd.StreamSetup{ 117 | Stream: "RTP-Unicast", 118 | Transport: onvifcmd.Transport{ 119 | Protocol: protocol, 120 | }, 121 | }, 122 | } 123 | streamUrlRes, err := sdkmedia.Call_GetStreamUri(context.Background(), dev, streamUrlReq) 124 | if err != nil { 125 | nazalog.Error(err) 126 | return 127 | } 128 | 129 | playUrl := buildPlayUrl(string(streamUrlRes.MediaUri.Uri), pullreq.Username, pullreq.Password) 130 | DoPull(playUrl, fmt.Sprintf("%s-%s", deviceInfoRes.Model, profilesRes.Profiles[0].Name), pullreq.RtspMode) 131 | } 132 | } 133 | 134 | func buildPlayUrl(rawurl, username, password string) string { 135 | if username != "" && password != "" { 136 | playUrl := fmt.Sprintf("rtsp://%s:%s@%s", username, password, strings.TrimLeft(rawurl, "rtsp://")) 137 | return playUrl 138 | } 139 | 140 | return rawurl 141 | } 142 | 143 | func DoPull(url, streamname string, rtspmod int) { 144 | request := base.ApiCtrlStartRelayPullReq{ 145 | Url: url, 146 | StreamName: streamname, 147 | RtspMode: rtspmod, 148 | AutoStopPullAfterNoOutMs: -1, 149 | } 150 | 151 | data, _ := json.Marshal(request) 152 | 153 | req, err := http.NewRequest("POST", "http://127.0.0.1:8083/api/ctrl/start_relay_pull", bytes.NewReader(data)) 154 | if err != nil { 155 | return 156 | } 157 | 158 | req.Header.Set("Content-Type", "application/json") 159 | 160 | cli := &http.Client{ 161 | Transport: http.DefaultTransport, 162 | Timeout: time.Duration(5) * time.Second, 163 | } 164 | 165 | resp, err := cli.Do(req) 166 | if err != nil { 167 | return 168 | } 169 | 170 | if resp.StatusCode != 200 { 171 | return 172 | } 173 | 174 | resp.Body.Close() 175 | 176 | return 177 | } 178 | -------------------------------------------------------------------------------- /room/server.go: -------------------------------------------------------------------------------- 1 | package room 2 | 3 | import ( 4 | lkc "github.com/livekit/livekit-server/pkg/config" 5 | lkrouting "github.com/livekit/livekit-server/pkg/routing" 6 | lkservice "github.com/livekit/livekit-server/pkg/service" 7 | "github.com/livekit/livekit-server/pkg/telemetry/prometheus" 8 | "github.com/q191201771/naza/pkg/nazalog" 9 | ) 10 | 11 | type RoomServer struct { 12 | APIKey string 13 | APISecret string 14 | lkcconf *lkc.Config 15 | lkserver *lkservice.LivekitServer 16 | } 17 | 18 | func NewRoomServer(apiKey, apiSecret string) *RoomServer { 19 | s := &RoomServer{ 20 | APIKey: apiKey, 21 | APISecret: apiSecret, 22 | } 23 | 24 | var err error 25 | s.lkcconf, err = s.getLivekitConfig() 26 | if err != nil { 27 | return nil 28 | } 29 | 30 | return s 31 | } 32 | 33 | func (s *RoomServer) Start() error { 34 | var err error 35 | currentNode, err := lkrouting.NewLocalNode(s.lkcconf) 36 | if err != nil { 37 | nazalog.Error("failed to create local node:", err) 38 | return err 39 | } 40 | 41 | if err := prometheus.Init(currentNode.Id, currentNode.Type); err != nil { 42 | return err 43 | } 44 | 45 | s.lkserver, err = lkservice.InitializeServer(s.lkcconf, currentNode) 46 | if err != nil { 47 | nazalog.Error("failed to initialize server:", err) 48 | return err 49 | } 50 | 51 | nazalog.Info("starting livekit server") 52 | 53 | return s.lkserver.Start() 54 | } 55 | 56 | func (s *RoomServer) Stop() error { 57 | if s.lkserver != nil { 58 | nazalog.Error("stopping livekit server") 59 | s.lkserver.Stop(true) 60 | } 61 | return nil 62 | } 63 | 64 | func (s *RoomServer) getLivekitConfig() (*lkc.Config, error) { 65 | strictMode := true 66 | conf, err := lkc.NewConfig("", strictMode, nil, nil) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | conf.Keys = map[string]string{ 72 | s.APIKey: s.APISecret, 73 | } 74 | 75 | if conf.BindAddresses == nil { 76 | conf.BindAddresses = []string{ 77 | "127.0.0.1", 78 | "::1", 79 | } 80 | } 81 | 82 | lkc.InitLoggerFromConfig(&conf.Logging) 83 | 84 | return conf, nil 85 | } 86 | -------------------------------------------------------------------------------- /rtc/jessibucasession.go: -------------------------------------------------------------------------------- 1 | package rtc 2 | 3 | import ( 4 | "context" 5 | "math" 6 | "sync" 7 | 8 | "github.com/gofrs/uuid" 9 | "github.com/pion/webrtc/v3" 10 | "github.com/q191201771/lal/pkg/base" 11 | "github.com/q191201771/lal/pkg/httpflv" 12 | "github.com/q191201771/lal/pkg/logic" 13 | "github.com/q191201771/lal/pkg/remux" 14 | "github.com/q191201771/lalmax/hook" 15 | "github.com/q191201771/naza/pkg/nazalog" 16 | "github.com/smallnest/chanx" 17 | ) 18 | 19 | type jessibucaSession struct { 20 | hooks *hook.HookSession 21 | pc *peerConnection 22 | subscriberId string 23 | lalServer logic.ILalServer 24 | videoTrack *webrtc.TrackLocalStaticRTP 25 | audioTrack *webrtc.TrackLocalStaticRTP 26 | videopacker *Packer 27 | audiopacker *Packer 28 | msgChan *chanx.UnboundedChan[base.RtmpMsg] 29 | closeChan chan bool 30 | remoteSafari bool 31 | DC *webrtc.DataChannel 32 | streamId string 33 | cancel context.CancelFunc 34 | stopOne sync.Once 35 | } 36 | 37 | func NewJessibucaSession(streamid string, writeChanSize int, pc *peerConnection, lalServer logic.ILalServer) *jessibucaSession { 38 | ok, session := hook.GetHookSessionManagerInstance().GetHookSession(streamid) 39 | if !ok { 40 | nazalog.Error("not found streamid:", streamid) 41 | return nil 42 | } 43 | 44 | u, _ := uuid.NewV4() 45 | ctx, cancel := context.WithCancel(context.Background()) 46 | return &jessibucaSession{ 47 | hooks: session, 48 | pc: pc, 49 | lalServer: lalServer, 50 | subscriberId: u.String(), 51 | streamId: streamid, 52 | cancel: cancel, 53 | msgChan: chanx.NewUnboundedChan[base.RtmpMsg](ctx, writeChanSize), 54 | closeChan: make(chan bool, 1), 55 | } 56 | } 57 | func (conn *jessibucaSession) createDataChannel() { 58 | if conn.DC != nil { 59 | return 60 | } 61 | conn.DC, _ = conn.pc.CreateDataChannel(conn.streamId, nil) 62 | } 63 | func (conn *jessibucaSession) GetAnswerSDP(offer string) (sdp string) { 64 | var err error 65 | conn.createDataChannel() 66 | 67 | gatherComplete := webrtc.GatheringCompletePromise(conn.pc.PeerConnection) 68 | 69 | conn.pc.SetRemoteDescription(webrtc.SessionDescription{ 70 | Type: webrtc.SDPTypeOffer, 71 | SDP: string(offer), 72 | }) 73 | 74 | answer, err := conn.pc.CreateAnswer(nil) 75 | if err != nil { 76 | nazalog.Error(err) 77 | return 78 | } 79 | 80 | err = conn.pc.SetLocalDescription(answer) 81 | if err != nil { 82 | nazalog.Error(err) 83 | return 84 | } 85 | 86 | <-gatherComplete 87 | 88 | sdp = conn.pc.LocalDescription().SDP 89 | return 90 | } 91 | 92 | func (conn *jessibucaSession) Run() { 93 | ok, _ := hook.GetHookSessionManagerInstance().GetHookSession(conn.streamId) 94 | if ok { 95 | conn.hooks.AddConsumer(conn.subscriberId, conn) 96 | 97 | conn.pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { 98 | nazalog.Info("peer connection state: ", state.String()) 99 | 100 | switch state { 101 | case webrtc.PeerConnectionStateConnected: 102 | case webrtc.PeerConnectionStateDisconnected: 103 | fallthrough 104 | case webrtc.PeerConnectionStateFailed: 105 | fallthrough 106 | case webrtc.PeerConnectionStateClosed: 107 | conn.closeChan <- true 108 | } 109 | }) 110 | if conn.DC != nil { 111 | conn.DC.OnOpen(func() { 112 | if err := conn.DC.Send(httpflv.FlvHeader); err != nil { 113 | nazalog.Warnf(" stream write videoHeader err:%s", err.Error()) 114 | return 115 | } 116 | 117 | defer func() { 118 | nazalog.Info("RemoveConsumer, connid:", conn.subscriberId) 119 | conn.hooks.RemoveConsumer(conn.subscriberId) 120 | conn.DC.Close() 121 | conn.pc.Close() 122 | conn.DC = nil 123 | conn.cancel() 124 | }() 125 | for { 126 | select { 127 | case msg := <-conn.msgChan.Out: 128 | lazyRtmpMsg2FlvTag := remux.LazyRtmpMsg2FlvTag{} 129 | lazyRtmpMsg2FlvTag.Init(msg) 130 | buf := lazyRtmpMsg2FlvTag.GetEnsureWithoutSdf() 131 | sendBuf := chunkSlice(buf, math.MaxUint16) 132 | for _, v := range sendBuf { 133 | if err := conn.DC.Send(v); err != nil { 134 | nazalog.Warnf(" stream write msg err:%s", err.Error()) 135 | return 136 | } 137 | } 138 | 139 | case <-conn.closeChan: 140 | return 141 | } 142 | } 143 | 144 | }) 145 | } 146 | } 147 | 148 | } 149 | 150 | func chunkSlice(slice []byte, size int) [][]byte { 151 | var chunks [][]byte 152 | 153 | for i := 0; i < len(slice); i += size { 154 | end := i + size 155 | 156 | if end > len(slice) { 157 | end = len(slice) 158 | } 159 | 160 | chunks = append(chunks, slice[i:end]) 161 | } 162 | 163 | return chunks 164 | } 165 | 166 | func (conn *jessibucaSession) OnMsg(msg base.RtmpMsg) { 167 | switch msg.Header.MsgTypeId { 168 | case base.RtmpTypeIdMetadata: 169 | return 170 | case base.RtmpTypeIdAudio: 171 | if conn.DC != nil { 172 | conn.msgChan.In <- msg 173 | } 174 | case base.RtmpTypeIdVideo: 175 | if conn.DC != nil { 176 | conn.msgChan.In <- msg 177 | } 178 | } 179 | } 180 | 181 | func (conn *jessibucaSession) OnStop() { 182 | conn.stopOne.Do(func() { 183 | conn.closeChan <- true 184 | }) 185 | } 186 | -------------------------------------------------------------------------------- /rtc/packer.go: -------------------------------------------------------------------------------- 1 | package rtc 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | 7 | "github.com/pion/rtp" 8 | "github.com/q191201771/lal/pkg/avc" 9 | "github.com/q191201771/lal/pkg/base" 10 | "github.com/q191201771/lal/pkg/hevc" 11 | "github.com/q191201771/lal/pkg/rtprtcp" 12 | "github.com/q191201771/naza/pkg/nazalog" 13 | ) 14 | 15 | const ( 16 | PacketH264 = "H264" 17 | PacketHEVC = "HEVC" 18 | PacketSafariHevc = "SafariHevc" 19 | PacketPCMA = "PCMA" 20 | PacketPCMU = "PCMU" 21 | PacketOPUS = "OPUS" 22 | ) 23 | 24 | type Packer struct { 25 | enc IRtpEncoder 26 | } 27 | 28 | func NewPacker(mimeType string, codec []byte) *Packer { 29 | p := &Packer{} 30 | 31 | switch mimeType { 32 | case PacketH264: 33 | p.enc = NewH264RtpEncoder(codec) 34 | case PacketPCMA: 35 | p.enc = NewG711RtpEncoder(8) 36 | case PacketPCMU: 37 | p.enc = NewG711RtpEncoder(0) 38 | case PacketSafariHevc: 39 | p.enc = NewSafariHEVCRtpEncoder(codec) 40 | case PacketOPUS: 41 | p.enc = NewOpusRtpEncoder(111) 42 | } 43 | return p 44 | } 45 | 46 | func (p *Packer) Encode(msg base.RtmpMsg) ([]*rtp.Packet, error) { 47 | return p.enc.Encode(msg) 48 | } 49 | 50 | type IRtpEncoder interface { 51 | Encode(msg base.RtmpMsg) ([]*rtp.Packet, error) 52 | } 53 | 54 | type H264RtpEncoder struct { 55 | IRtpEncoder 56 | sps []byte 57 | pps []byte 58 | rtpPacker *rtprtcp.RtpPacker 59 | } 60 | 61 | func NewH264RtpEncoder(codec []byte) *H264RtpEncoder { 62 | 63 | sps, pps, err := avc.ParseSpsPpsFromSeqHeader(codec) 64 | if err != nil { 65 | nazalog.Error(err) 66 | return nil 67 | } 68 | 69 | pp := rtprtcp.NewRtpPackerPayloadAvc(func(option *rtprtcp.RtpPackerPayloadAvcHevcOption) { 70 | option.Typ = rtprtcp.RtpPackerPayloadAvcHevcTypeAnnexb 71 | }) 72 | 73 | return &H264RtpEncoder{ 74 | sps: sps, 75 | pps: pps, 76 | rtpPacker: rtprtcp.NewRtpPacker(pp, 90000, 0), 77 | } 78 | } 79 | 80 | func (enc *H264RtpEncoder) Encode(msg base.RtmpMsg) ([]*rtp.Packet, error) { 81 | var out []byte 82 | err := avc.IterateNaluAvcc(msg.Payload[5:], func(nal []byte) { 83 | t := avc.ParseNaluType(nal[0]) 84 | if t == avc.NaluTypeSei { 85 | return 86 | } 87 | 88 | if t == avc.NaluTypeIdrSlice { 89 | out = append(out, avc.NaluStartCode3...) 90 | out = append(out, enc.sps...) 91 | out = append(out, avc.NaluStartCode3...) 92 | out = append(out, enc.pps...) 93 | } 94 | 95 | out = append(out, avc.NaluStartCode3...) 96 | out = append(out, nal...) 97 | }) 98 | 99 | if err != nil { 100 | return nil, fmt.Errorf("Packetize failed") 101 | } 102 | 103 | if len(out) == 0 { 104 | return nil, fmt.Errorf("Packetize failed") 105 | } 106 | 107 | avpacket := base.AvPacket{ 108 | Timestamp: int64(msg.Dts()), 109 | Payload: out, 110 | } 111 | 112 | var pkts []*rtp.Packet 113 | rtpPkts := enc.rtpPacker.Pack(avpacket) 114 | for _, pkt := range rtpPkts { 115 | var newRtpPkt rtp.Packet 116 | err := newRtpPkt.Unmarshal(pkt.Raw) 117 | if err != nil { 118 | nazalog.Error(err) 119 | continue 120 | } 121 | 122 | pkts = append(pkts, &newRtpPkt) 123 | } 124 | 125 | if len(pkts) == 0 { 126 | return nil, fmt.Errorf("Packetize failed") 127 | } 128 | 129 | return pkts, nil 130 | } 131 | 132 | type G711RtpEncoder struct { 133 | IRtpEncoder 134 | rtpPacker *rtprtcp.RtpPacker 135 | } 136 | 137 | func NewG711RtpEncoder(pt uint8) *G711RtpEncoder { 138 | // TODO 暂时采样率设置为8000 139 | pp := rtprtcp.NewRtpPackerPayloadPcm() 140 | 141 | return &G711RtpEncoder{ 142 | rtpPacker: rtprtcp.NewRtpPacker(pp, 8000, 0), 143 | } 144 | } 145 | 146 | func (enc *G711RtpEncoder) Encode(msg base.RtmpMsg) ([]*rtp.Packet, error) { 147 | avpacket := base.AvPacket{ 148 | Timestamp: int64(msg.Dts()), 149 | Payload: msg.Payload[1:], 150 | } 151 | 152 | var pkts []*rtp.Packet 153 | rtpPkts := enc.rtpPacker.Pack(avpacket) 154 | for _, pkt := range rtpPkts { 155 | var newRtpPkt rtp.Packet 156 | err := newRtpPkt.Unmarshal(pkt.Raw) 157 | if err != nil { 158 | nazalog.Error(err) 159 | continue 160 | } 161 | 162 | pkts = append(pkts, &newRtpPkt) 163 | } 164 | 165 | if len(pkts) == 0 { 166 | return nil, fmt.Errorf("Packetize failed") 167 | } 168 | 169 | return pkts, nil 170 | } 171 | 172 | type SafariHEVCRtpEncoder struct { 173 | IRtpEncoder 174 | vps []byte 175 | sps []byte 176 | pps []byte 177 | payloadType int 178 | ssrc int 179 | seqId uint16 180 | tsBase int64 181 | } 182 | 183 | func NewSafariHEVCRtpEncoder(codec []byte) *SafariHEVCRtpEncoder { 184 | vps, sps, pps, err := hevc.ParseVpsSpsPpsFromSeqHeader(codec) 185 | if err != nil { 186 | nazalog.Error(err) 187 | return nil 188 | } 189 | 190 | return &SafariHEVCRtpEncoder{ 191 | vps: vps, 192 | sps: sps, 193 | pps: pps, 194 | payloadType: 98, 195 | ssrc: 0, 196 | seqId: uint16(rand.Int() % 65536), 197 | tsBase: -1, 198 | } 199 | } 200 | 201 | func (enc *SafariHEVCRtpEncoder) Encode(msg base.RtmpMsg) ([]*rtp.Packet, error) { 202 | var pkts []*rtp.Packet 203 | var out []byte 204 | var keyFrame bool 205 | 206 | if enc.tsBase == -1 { 207 | enc.tsBase = int64(msg.Dts()) 208 | } 209 | 210 | err := avc.IterateNaluAvcc(msg.Payload[5:], func(nal []byte) { 211 | t := hevc.ParseNaluType(nal[0]) 212 | if t == hevc.NaluTypeSei { 213 | return 214 | } 215 | 216 | if hevc.IsIrapNalu(t) { 217 | keyFrame = true 218 | out = append(out, avc.NaluStartCode4...) 219 | out = append(out, enc.vps...) 220 | out = append(out, avc.NaluStartCode4...) 221 | out = append(out, enc.sps...) 222 | out = append(out, avc.NaluStartCode4...) 223 | out = append(out, enc.pps...) 224 | } 225 | 226 | out = append(out, avc.NaluStartCode4...) 227 | out = append(out, nal...) 228 | }) 229 | 230 | if err != nil { 231 | return nil, fmt.Errorf("Packetize failed") 232 | } 233 | 234 | if len(out) == 0 { 235 | return nil, fmt.Errorf("Packetize failed") 236 | } 237 | 238 | payloads := enc.doPacketNaluForSafariHevc(out, keyFrame) 239 | for i, payload := range payloads { 240 | var pkt rtp.Packet 241 | pkt.Version = 2 242 | pkt.Timestamp = uint32((int64(msg.Dts()) - enc.tsBase) * 90) 243 | pkt.PayloadType = uint8(enc.payloadType) 244 | pkt.SSRC = uint32(enc.ssrc) 245 | 246 | if i == len(payloads)-1 { 247 | pkt.Marker = true 248 | } 249 | 250 | pkt.SequenceNumber = enc.seqId 251 | enc.seqId += 1 252 | pkt.Payload = payload 253 | 254 | pkts = append(pkts, &pkt) 255 | } 256 | 257 | if len(pkts) == 0 { 258 | return nil, fmt.Errorf("Packetize failed") 259 | } 260 | 261 | return pkts, nil 262 | } 263 | 264 | func (enc *SafariHEVCRtpEncoder) doPacketNaluForSafariHevc(nalu []byte, keyFrame bool) [][]byte { 265 | var rtpPayloads [][]byte 266 | 267 | naluLen := len(nalu) 268 | maxPayloadSize := 1200 269 | splitNum := naluLen/maxPayloadSize + 1 270 | remainder := naluLen % splitNum 271 | referenceLen := naluLen / splitNum 272 | dataPos := 0 273 | 274 | for i := splitNum; i > 0; i-- { 275 | tmpLen := referenceLen 276 | if i < remainder { 277 | tmpLen++ 278 | } 279 | buf := make([]byte, tmpLen+1) 280 | if keyFrame { 281 | if i == splitNum { 282 | buf[0] = 3 283 | } else { 284 | buf[0] = 1 285 | } 286 | } else { 287 | if i == splitNum { 288 | buf[0] = 2 289 | } else { 290 | buf[0] = 0 291 | } 292 | } 293 | copy(buf[1:], nalu[dataPos:dataPos+tmpLen]) 294 | dataPos += tmpLen 295 | 296 | rtpPayloads = append(rtpPayloads, buf) 297 | } 298 | 299 | return rtpPayloads 300 | } 301 | 302 | type OpusRtpEncoder struct { 303 | IRtpEncoder 304 | rtpPacker *rtprtcp.RtpPacker 305 | } 306 | 307 | func NewOpusRtpEncoder(pt uint8) *OpusRtpEncoder { 308 | pp := rtprtcp.NewRtpPackerPayloadOpus() 309 | 310 | return &OpusRtpEncoder{ 311 | rtpPacker: rtprtcp.NewRtpPacker(pp, 48000, 0), 312 | } 313 | } 314 | 315 | func (enc *OpusRtpEncoder) Encode(msg base.RtmpMsg) ([]*rtp.Packet, error) { 316 | avpacket := base.AvPacket{ 317 | Timestamp: int64(msg.Dts()), 318 | Payload: msg.Payload[1:], 319 | } 320 | 321 | var pkts []*rtp.Packet 322 | rtpPkts := enc.rtpPacker.Pack(avpacket) 323 | for _, pkt := range rtpPkts { 324 | var newRtpPkt rtp.Packet 325 | err := newRtpPkt.Unmarshal(pkt.Raw) 326 | if err != nil { 327 | nazalog.Error(err) 328 | continue 329 | } 330 | 331 | pkts = append(pkts, &newRtpPkt) 332 | } 333 | 334 | if len(pkts) == 0 { 335 | return nil, fmt.Errorf("Packetize failed") 336 | } 337 | 338 | return pkts, nil 339 | } 340 | -------------------------------------------------------------------------------- /rtc/peerConnection.go: -------------------------------------------------------------------------------- 1 | package rtc 2 | 3 | import ( 4 | "github.com/pion/ice/v2" 5 | "github.com/pion/interceptor" 6 | "github.com/pion/webrtc/v3" 7 | "github.com/q191201771/naza/pkg/nazalog" 8 | ) 9 | 10 | type peerConnection struct { 11 | *webrtc.PeerConnection 12 | } 13 | 14 | func newPeerConnection(ips []string, iceUDPMux ice.UDPMux, iceTCPMux ice.TCPMux) (conn *peerConnection, err error) { 15 | configuration := webrtc.Configuration{} 16 | settingsEngine := webrtc.SettingEngine{} 17 | 18 | if len(ips) != 0 { 19 | settingsEngine.SetNAT1To1IPs(ips, webrtc.ICECandidateTypeHost) 20 | } else { 21 | configuration.ICEServers = []webrtc.ICEServer{ 22 | { 23 | URLs: []string{"stun:stun.l.google.com:19302"}, 24 | }, 25 | } 26 | } 27 | 28 | if iceUDPMux != nil { 29 | settingsEngine.SetICEUDPMux(iceUDPMux) 30 | } 31 | 32 | if iceTCPMux != nil { 33 | settingsEngine.SetICETCPMux(iceTCPMux) 34 | settingsEngine.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeTCP4}) 35 | } 36 | 37 | mediaEngine := &webrtc.MediaEngine{} 38 | err = mediaEngine.RegisterCodec( 39 | webrtc.RTPCodecParameters{ 40 | RTPCodecCapability: webrtc.RTPCodecCapability{ 41 | MimeType: webrtc.MimeTypeH264, 42 | ClockRate: 90000, 43 | }, 44 | PayloadType: 96, 45 | }, 46 | webrtc.RTPCodecTypeVideo) 47 | 48 | if err != nil { 49 | nazalog.Error(err) 50 | return 51 | } 52 | 53 | err = mediaEngine.RegisterCodec( 54 | webrtc.RTPCodecParameters{ 55 | RTPCodecCapability: webrtc.RTPCodecCapability{ 56 | MimeType: webrtc.MimeTypeH265, 57 | ClockRate: 90000, 58 | }, 59 | PayloadType: 102, 60 | }, 61 | webrtc.RTPCodecTypeVideo) 62 | 63 | if err != nil { 64 | nazalog.Error(err) 65 | return 66 | } 67 | 68 | // opus 69 | err = mediaEngine.RegisterCodec( 70 | webrtc.RTPCodecParameters{ 71 | RTPCodecCapability: webrtc.RTPCodecCapability{ 72 | MimeType: webrtc.MimeTypeOpus, 73 | ClockRate: 48000, 74 | }, 75 | PayloadType: 111, 76 | }, 77 | webrtc.RTPCodecTypeAudio) 78 | 79 | if err != nil { 80 | nazalog.Error(err) 81 | return 82 | } 83 | 84 | // PCMU 85 | err = mediaEngine.RegisterCodec( 86 | webrtc.RTPCodecParameters{ 87 | RTPCodecCapability: webrtc.RTPCodecCapability{ 88 | MimeType: webrtc.MimeTypePCMU, 89 | ClockRate: 8000, 90 | }, 91 | PayloadType: 0, 92 | }, 93 | webrtc.RTPCodecTypeAudio) 94 | 95 | if err != nil { 96 | nazalog.Error(err) 97 | return 98 | } 99 | 100 | // PCMA 101 | err = mediaEngine.RegisterCodec( 102 | webrtc.RTPCodecParameters{ 103 | RTPCodecCapability: webrtc.RTPCodecCapability{ 104 | MimeType: webrtc.MimeTypePCMA, 105 | ClockRate: 8000, 106 | }, 107 | PayloadType: 8, 108 | }, 109 | webrtc.RTPCodecTypeAudio) 110 | 111 | if err != nil { 112 | nazalog.Error(err) 113 | return 114 | } 115 | 116 | interceptorRegistry := &interceptor.Registry{} 117 | if err := webrtc.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry); err != nil { 118 | return nil, err 119 | } 120 | 121 | api := webrtc.NewAPI( 122 | webrtc.WithSettingEngine(settingsEngine), 123 | webrtc.WithMediaEngine(mediaEngine), 124 | webrtc.WithInterceptorRegistry(interceptorRegistry)) 125 | 126 | pc, err := api.NewPeerConnection(configuration) 127 | if err != nil { 128 | nazalog.Error(err) 129 | return nil, err 130 | } 131 | 132 | conn = &peerConnection{ 133 | PeerConnection: pc, 134 | } 135 | 136 | return 137 | } 138 | -------------------------------------------------------------------------------- /rtc/server.go: -------------------------------------------------------------------------------- 1 | package rtc 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "net/http" 7 | "strings" 8 | 9 | config "github.com/q191201771/lalmax/conf" 10 | 11 | "github.com/gin-gonic/gin" 12 | "github.com/pion/ice/v2" 13 | "github.com/pion/webrtc/v3" 14 | "github.com/q191201771/lal/pkg/logic" 15 | "github.com/q191201771/naza/pkg/nazalog" 16 | ) 17 | 18 | type RtcServer struct { 19 | config config.RtcConfig 20 | lalServer logic.ILalServer 21 | udpMux ice.UDPMux 22 | tcpMux ice.TCPMux 23 | } 24 | 25 | func NewRtcServer(config config.RtcConfig, lal logic.ILalServer) (*RtcServer, error) { 26 | var udpMux ice.UDPMux 27 | var tcpMux ice.TCPMux 28 | 29 | if config.ICEUDPMuxPort != 0 { 30 | var udplistener *net.UDPConn 31 | 32 | udplistener, err := net.ListenUDP("udp", &net.UDPAddr{ 33 | IP: net.IP{0, 0, 0, 0}, 34 | Port: config.ICEUDPMuxPort, 35 | }) 36 | 37 | if err != nil { 38 | nazalog.Error(err) 39 | return nil, err 40 | } 41 | nazalog.Infof("webrtc ice udp listen. port=%d", config.ICEUDPMuxPort) 42 | udpMux = webrtc.NewICEUDPMux(nil, udplistener) 43 | } 44 | if config.WriteChanSize == 0 { 45 | config.WriteChanSize = 1024 46 | } 47 | if config.ICETCPMuxPort != 0 { 48 | var tcplistener *net.TCPListener 49 | 50 | tcplistener, err := net.ListenTCP("tcp", &net.TCPAddr{ 51 | IP: net.IP{0, 0, 0, 0}, 52 | Port: config.ICETCPMuxPort, 53 | }) 54 | 55 | if err != nil { 56 | nazalog.Error(err) 57 | return nil, err 58 | } 59 | nazalog.Infof("webrtc ice tcp listen. port=%d", config.ICETCPMuxPort) 60 | tcpMux = webrtc.NewICETCPMux(nil, tcplistener, 20) 61 | } 62 | 63 | svr := &RtcServer{ 64 | config: config, 65 | lalServer: lal, 66 | udpMux: udpMux, 67 | tcpMux: tcpMux, 68 | } 69 | 70 | return svr, nil 71 | } 72 | 73 | func (s *RtcServer) HandleWHIP(c *gin.Context) { 74 | streamid := c.Request.URL.Query().Get("streamid") 75 | if streamid == "" { 76 | c.Status(http.StatusMethodNotAllowed) 77 | return 78 | } 79 | 80 | body, err := c.GetRawData() 81 | if err != nil { 82 | nazalog.Error(err) 83 | c.Status(http.StatusBadRequest) 84 | return 85 | } 86 | 87 | if len(body) == 0 { 88 | nazalog.Error("invalid body") 89 | c.Status(http.StatusNoContent) 90 | return 91 | } 92 | 93 | pc, err := newPeerConnection(s.config.ICEHostNATToIPs, s.udpMux, s.tcpMux) 94 | if err != nil { 95 | c.Status(http.StatusInternalServerError) 96 | return 97 | } 98 | 99 | whipsession := NewWhipSession(streamid, pc, s.lalServer) 100 | if whipsession == nil { 101 | c.Status(http.StatusInternalServerError) 102 | return 103 | } 104 | 105 | c.Header("Location", fmt.Sprintf("whip/%s", whipsession.subscriberId)) 106 | 107 | sdp := whipsession.GetAnswerSDP(string(body)) 108 | if sdp == "" { 109 | c.Status(http.StatusInternalServerError) 110 | return 111 | } 112 | 113 | go whipsession.Run() 114 | 115 | c.Data(http.StatusCreated, "application/sdp", []byte(sdp)) 116 | } 117 | func (s *RtcServer) HandleJessibuca(c *gin.Context) { 118 | streamid := c.Param("streamid") 119 | if streamid == "" { 120 | c.Status(http.StatusMethodNotAllowed) 121 | return 122 | } 123 | 124 | body, err := c.GetRawData() 125 | if err != nil { 126 | nazalog.Error(err) 127 | c.Status(http.StatusBadRequest) 128 | return 129 | } 130 | 131 | if len(body) == 0 { 132 | nazalog.Error("invalid body") 133 | c.Status(http.StatusNoContent) 134 | return 135 | } 136 | 137 | pc, err := newPeerConnection(s.config.ICEHostNATToIPs, s.udpMux, s.tcpMux) 138 | if err != nil { 139 | c.Status(http.StatusInternalServerError) 140 | return 141 | } 142 | 143 | jessibucaSession := NewJessibucaSession(streamid, s.config.WriteChanSize, pc, s.lalServer) 144 | if jessibucaSession == nil { 145 | c.Status(http.StatusInternalServerError) 146 | return 147 | } 148 | 149 | c.Header("Location", fmt.Sprintf("jessibucaflv/%s", jessibucaSession.subscriberId)) 150 | 151 | sdp := jessibucaSession.GetAnswerSDP(string(body)) 152 | if sdp == "" { 153 | c.Status(http.StatusInternalServerError) 154 | return 155 | } 156 | 157 | go jessibucaSession.Run() 158 | 159 | c.Data(http.StatusCreated, "application/sdp", []byte(sdp)) 160 | } 161 | func (s *RtcServer) HandleWHEP(c *gin.Context) { 162 | streamid := c.Request.URL.Query().Get("streamid") 163 | if streamid == "" { 164 | c.Status(http.StatusMethodNotAllowed) 165 | return 166 | } 167 | 168 | body, err := c.GetRawData() 169 | if err != nil { 170 | nazalog.Error(err) 171 | c.Status(http.StatusBadRequest) 172 | return 173 | } 174 | 175 | if len(body) == 0 { 176 | nazalog.Error("invalid body") 177 | c.Status(http.StatusNoContent) 178 | return 179 | } 180 | 181 | pc, err := newPeerConnection(s.config.ICEHostNATToIPs, s.udpMux, s.tcpMux) 182 | if err != nil { 183 | c.Status(http.StatusInternalServerError) 184 | return 185 | } 186 | 187 | whepsession := NewWhepSession(streamid, s.config.WriteChanSize, pc, s.lalServer) 188 | if whepsession == nil { 189 | c.Status(http.StatusInternalServerError) 190 | return 191 | } 192 | 193 | c.Header("Location", fmt.Sprintf("whep/%s", whepsession.subscriberId)) 194 | 195 | userAgent := c.Request.UserAgent() 196 | if strings.Contains(userAgent, "Safari") { 197 | whepsession.SetRemoteSafari(true) 198 | } 199 | 200 | sdp := whepsession.GetAnswerSDP(string(body)) 201 | if sdp == "" { 202 | c.Status(http.StatusInternalServerError) 203 | return 204 | } 205 | 206 | go whepsession.Run() 207 | 208 | c.Data(http.StatusCreated, "application/sdp", []byte(sdp)) 209 | } 210 | 211 | func (s *RtcServer) handleWHEP(w http.ResponseWriter, r *http.Request, streamid, body string) { 212 | pc, err := newPeerConnection(s.config.ICEHostNATToIPs, s.udpMux, s.tcpMux) 213 | if err != nil { 214 | w.WriteHeader(http.StatusInternalServerError) 215 | return 216 | } 217 | 218 | whepsession := NewWhepSession(streamid, s.config.WriteChanSize, pc, s.lalServer) 219 | if whepsession == nil { 220 | w.WriteHeader(http.StatusInternalServerError) 221 | return 222 | } 223 | 224 | sdp := whepsession.GetAnswerSDP(string(body)) 225 | if sdp == "" { 226 | w.WriteHeader(http.StatusInternalServerError) 227 | return 228 | } 229 | 230 | go whepsession.Run() 231 | 232 | w.Header().Set("Content-Type", "application/sdp") 233 | w.WriteHeader(http.StatusCreated) 234 | w.Write([]byte(sdp)) 235 | } 236 | -------------------------------------------------------------------------------- /rtc/unpacker.go: -------------------------------------------------------------------------------- 1 | package rtc 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/bluenviron/gortsplib/v4/pkg/format" 9 | "github.com/bluenviron/gortsplib/v4/pkg/format/rtph264" 10 | "github.com/bluenviron/gortsplib/v4/pkg/format/rtph265" 11 | "github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm" 12 | "github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio" 13 | "github.com/bluenviron/gortsplib/v4/pkg/rtpreorderer" 14 | "github.com/bluenviron/gortsplib/v4/pkg/rtptime" 15 | "github.com/pion/rtp" 16 | "github.com/pion/webrtc/v3" 17 | "github.com/q191201771/lal/pkg/avc" 18 | "github.com/q191201771/lal/pkg/base" 19 | "github.com/q191201771/naza/pkg/nazalog" 20 | ) 21 | 22 | var ErrNeedMoreFrames = errors.New("need more frames") 23 | 24 | type UnPacker struct { 25 | reorderer *rtpreorderer.Reorderer 26 | payloadType base.AvPacketPt 27 | clockRate uint32 28 | pktChan chan<- base.AvPacket 29 | timeDecoder *rtptime.GlobalDecoder 30 | format format.Format 31 | dec IRtpDecoder 32 | } 33 | 34 | func NewUnPacker(mimeType string, clockRate uint32, pktChan chan<- base.AvPacket) *UnPacker { 35 | un := &UnPacker{ 36 | reorderer: rtpreorderer.New(), 37 | clockRate: clockRate, 38 | pktChan: pktChan, 39 | timeDecoder: rtptime.NewGlobalDecoder(), 40 | } 41 | 42 | switch mimeType { 43 | case webrtc.MimeTypeH264: 44 | un.payloadType = base.AvPacketPtAvc 45 | un.format = &format.H264{} 46 | un.dec = NewH264RtpDecoder(un.format) 47 | case webrtc.MimeTypePCMA: 48 | un.payloadType = base.AvPacketPtG711A 49 | un.format = &format.G711{} 50 | case webrtc.MimeTypePCMU: 51 | un.payloadType = base.AvPacketPtG711U 52 | un.format = &format.G711{} 53 | case webrtc.MimeTypeOpus: 54 | un.payloadType = base.AvPacketPtOpus 55 | un.format = &format.Opus{} 56 | un.dec = NewOpusRtpDecoder(un.format) 57 | case webrtc.MimeTypeH265: 58 | un.payloadType = base.AvPacketPtHevc 59 | un.format = &format.H265{} 60 | un.dec = NewH265RtpDecoder(un.format) 61 | default: 62 | nazalog.Error("unsupport mineType:", mimeType) 63 | } 64 | 65 | nazalog.Info("create rtp unpacker, mimeType:", mimeType) 66 | 67 | return un 68 | } 69 | 70 | func (un *UnPacker) UnPack(pkt *rtp.Packet) (err error) { 71 | packets, lost := un.reorderer.Process(pkt) 72 | if lost != 0 { 73 | nazalog.Error("rtp lost") 74 | return 75 | } 76 | 77 | for _, rtppkt := range packets { 78 | pts, ok := un.timeDecoder.Decode(un.format, rtppkt) 79 | if !ok { 80 | continue 81 | } 82 | 83 | frame, err := un.dec.Decode(rtppkt) 84 | if err != nil { 85 | if err != ErrNeedMoreFrames { 86 | nazalog.Error("rtp dec Decode failed:", err) 87 | return err 88 | } 89 | 90 | continue 91 | } 92 | 93 | var pkt base.AvPacket 94 | pkt.PayloadType = un.payloadType 95 | pkt.Timestamp = int64(pts / time.Millisecond) 96 | pkt.Pts = pkt.Timestamp 97 | pkt.Payload = append(pkt.Payload, frame...) 98 | 99 | un.pktChan <- pkt 100 | } 101 | 102 | return 103 | } 104 | 105 | type IRtpDecoder interface { 106 | Decode(pkt *rtp.Packet) ([]byte, error) 107 | } 108 | 109 | type H264RtpDecoder struct { 110 | IRtpDecoder 111 | dec *rtph264.Decoder 112 | } 113 | 114 | func NewH264RtpDecoder(f format.Format) *H264RtpDecoder { 115 | dec, _ := f.(*format.H264).CreateDecoder() 116 | return &H264RtpDecoder{ 117 | dec: dec, 118 | } 119 | } 120 | 121 | func (r *H264RtpDecoder) Decode(pkt *rtp.Packet) ([]byte, error) { 122 | nalus, err := r.dec.Decode(pkt) 123 | if err != nil { 124 | return nil, ErrNeedMoreFrames 125 | } 126 | 127 | if len(nalus) == 0 { 128 | err = fmt.Errorf("invalid frame") 129 | return nil, err 130 | } 131 | 132 | var frame []byte 133 | for _, nalu := range nalus { 134 | frame = append(frame, avc.NaluStartCode4...) 135 | frame = append(frame, nalu...) 136 | } 137 | 138 | return frame, nil 139 | } 140 | 141 | type G711RtpDecoder struct { 142 | IRtpDecoder 143 | dec *rtplpcm.Decoder 144 | } 145 | 146 | func NewG711RtpDecoder(f format.Format) *G711RtpDecoder { 147 | dec, _ := f.(*format.G711).CreateDecoder() 148 | return &G711RtpDecoder{ 149 | dec: dec, 150 | } 151 | } 152 | 153 | func (r *G711RtpDecoder) Decode(pkt *rtp.Packet) ([]byte, error) { 154 | frame, err := r.dec.Decode(pkt) 155 | if err != nil { 156 | nazalog.Error(err) 157 | return nil, err 158 | } 159 | 160 | return frame, nil 161 | } 162 | 163 | type OpusRtpDecoder struct { 164 | IRtpDecoder 165 | dec *rtpsimpleaudio.Decoder 166 | } 167 | 168 | func NewOpusRtpDecoder(f format.Format) *OpusRtpDecoder { 169 | dec, _ := f.(*format.Opus).CreateDecoder() 170 | return &OpusRtpDecoder{ 171 | dec: dec, 172 | } 173 | } 174 | 175 | func (r *OpusRtpDecoder) Decode(pkt *rtp.Packet) ([]byte, error) { 176 | frame, err := r.dec.Decode(pkt) 177 | if err != nil { 178 | nazalog.Error(err) 179 | return nil, err 180 | } 181 | 182 | return frame, nil 183 | } 184 | 185 | type H265RtpDecoder struct { 186 | IRtpDecoder 187 | dec *rtph265.Decoder 188 | } 189 | 190 | func NewH265RtpDecoder(f format.Format) *H265RtpDecoder { 191 | dec, _ := f.(*format.H265).CreateDecoder() 192 | return &H265RtpDecoder{ 193 | dec: dec, 194 | } 195 | } 196 | 197 | func (r *H265RtpDecoder) Decode(pkt *rtp.Packet) ([]byte, error) { 198 | nalus, err := r.dec.Decode(pkt) 199 | if err != nil { 200 | return nil, ErrNeedMoreFrames 201 | } 202 | 203 | if len(nalus) == 0 { 204 | err = fmt.Errorf("invalid frame") 205 | return nil, err 206 | } 207 | 208 | var frame []byte 209 | for _, nalu := range nalus { 210 | frame = append(frame, avc.NaluStartCode4...) 211 | frame = append(frame, nalu...) 212 | } 213 | 214 | return frame, nil 215 | } 216 | -------------------------------------------------------------------------------- /rtc/whepsession.go: -------------------------------------------------------------------------------- 1 | package rtc 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/q191201771/lalmax/hook" 7 | "github.com/smallnest/chanx" 8 | 9 | "github.com/gofrs/uuid" 10 | "github.com/pion/webrtc/v3" 11 | "github.com/q191201771/lal/pkg/base" 12 | "github.com/q191201771/lal/pkg/logic" 13 | "github.com/q191201771/naza/pkg/nazalog" 14 | ) 15 | 16 | type whepSession struct { 17 | hooks *hook.HookSession 18 | pc *peerConnection 19 | subscriberId string 20 | lalServer logic.ILalServer 21 | videoTrack *webrtc.TrackLocalStaticRTP 22 | audioTrack *webrtc.TrackLocalStaticRTP 23 | videopacker *Packer 24 | audiopacker *Packer 25 | msgChan *chanx.UnboundedChan[base.RtmpMsg] 26 | closeChan chan bool 27 | remoteSafari bool 28 | } 29 | 30 | func NewWhepSession(streamid string, writeChanSize int, pc *peerConnection, lalServer logic.ILalServer) *whepSession { 31 | ok, session := hook.GetHookSessionManagerInstance().GetHookSession(streamid) 32 | if !ok { 33 | nazalog.Error("not found streamid:", streamid) 34 | return nil 35 | } 36 | 37 | u, _ := uuid.NewV4() 38 | return &whepSession{ 39 | hooks: session, 40 | pc: pc, 41 | lalServer: lalServer, 42 | subscriberId: u.String(), 43 | msgChan: chanx.NewUnboundedChan[base.RtmpMsg](context.Background(), writeChanSize), 44 | closeChan: make(chan bool, 2), 45 | } 46 | } 47 | 48 | func (conn *whepSession) SetRemoteSafari(val bool) { 49 | conn.remoteSafari = val 50 | } 51 | 52 | func (conn *whepSession) GetAnswerSDP(offer string) (sdp string) { 53 | var err error 54 | 55 | videoHeader := conn.hooks.GetVideoSeqHeaderMsg() 56 | if videoHeader != nil { 57 | if videoHeader.IsAvcKeySeqHeader() { 58 | conn.videoTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264}, "video", "lalmax") 59 | if err != nil { 60 | nazalog.Error(err) 61 | return 62 | } 63 | 64 | _, err = conn.pc.AddTrack(conn.videoTrack) 65 | if err != nil { 66 | nazalog.Error(err) 67 | return 68 | } 69 | 70 | conn.videopacker = NewPacker(PacketH264, videoHeader.Payload) 71 | } else if videoHeader.IsHevcKeySeqHeader() { 72 | if conn.remoteSafari { 73 | // hevc暂时只支持对接Safari hevc 74 | conn.videoTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH265}, "video", "lalmax") 75 | if err != nil { 76 | nazalog.Error(err) 77 | return 78 | } 79 | 80 | _, err = conn.pc.AddTrack(conn.videoTrack) 81 | if err != nil { 82 | nazalog.Error(err) 83 | return 84 | } 85 | 86 | conn.videopacker = NewPacker(PacketSafariHevc, videoHeader.Payload) 87 | } 88 | } 89 | } 90 | 91 | audioHeader := conn.hooks.GetAudioSeqHeaderMsg() 92 | if audioHeader != nil { 93 | var mimeType string 94 | audioId := audioHeader.AudioCodecId() 95 | switch audioId { 96 | case base.RtmpSoundFormatG711A: 97 | conn.audioTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypePCMA}, "audio", "lalmax") 98 | if err != nil { 99 | nazalog.Error(err) 100 | return 101 | } 102 | 103 | mimeType = PacketPCMA 104 | case base.RtmpSoundFormatG711U: 105 | conn.audioTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypePCMU}, "audio", "lalmax") 106 | if err != nil { 107 | nazalog.Error(err) 108 | return 109 | } 110 | 111 | mimeType = PacketPCMU 112 | case base.RtmpSoundFormatOpus: 113 | conn.audioTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "lalmax") 114 | if err != nil { 115 | nazalog.Error(err) 116 | return 117 | } 118 | 119 | mimeType = PacketOPUS 120 | default: 121 | nazalog.Error("unsupport audio codeid:", audioId) 122 | } 123 | 124 | if conn.audioTrack != nil { 125 | _, err = conn.pc.AddTrack(conn.audioTrack) 126 | if err != nil { 127 | nazalog.Error(err) 128 | return 129 | } 130 | 131 | conn.audiopacker = NewPacker(mimeType, nil) 132 | } 133 | } 134 | 135 | gatherComplete := webrtc.GatheringCompletePromise(conn.pc.PeerConnection) 136 | 137 | conn.pc.SetRemoteDescription(webrtc.SessionDescription{ 138 | Type: webrtc.SDPTypeOffer, 139 | SDP: string(offer), 140 | }) 141 | 142 | answer, err := conn.pc.CreateAnswer(nil) 143 | if err != nil { 144 | nazalog.Error(err) 145 | return 146 | } 147 | 148 | err = conn.pc.SetLocalDescription(answer) 149 | if err != nil { 150 | nazalog.Error(err) 151 | return 152 | } 153 | 154 | <-gatherComplete 155 | 156 | sdp = conn.pc.LocalDescription().SDP 157 | return 158 | } 159 | 160 | func (conn *whepSession) Run() { 161 | conn.hooks.AddConsumer(conn.subscriberId, conn) 162 | 163 | conn.pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { 164 | nazalog.Info("peer connection state: ", state.String()) 165 | 166 | switch state { 167 | case webrtc.PeerConnectionStateConnected: 168 | case webrtc.PeerConnectionStateDisconnected: 169 | fallthrough 170 | case webrtc.PeerConnectionStateFailed: 171 | fallthrough 172 | case webrtc.PeerConnectionStateClosed: 173 | conn.closeChan <- true 174 | } 175 | }) 176 | 177 | for { 178 | select { 179 | case msg := <-conn.msgChan.Out: 180 | if msg.Header.MsgTypeId == base.RtmpTypeIdAudio && conn.audioTrack != nil { 181 | conn.sendAudio(msg) 182 | } else if msg.Header.MsgTypeId == base.RtmpTypeIdVideo && conn.videoTrack != nil { 183 | conn.sendVideo(msg) 184 | } 185 | case <-conn.closeChan: 186 | nazalog.Info("RemoveConsumer, connid:", conn.subscriberId) 187 | conn.hooks.RemoveConsumer(conn.subscriberId) 188 | return 189 | } 190 | } 191 | } 192 | 193 | func (conn *whepSession) OnMsg(msg base.RtmpMsg) { 194 | switch msg.Header.MsgTypeId { 195 | case base.RtmpTypeIdMetadata: 196 | return 197 | case base.RtmpTypeIdAudio: 198 | if conn.audioTrack != nil { 199 | conn.msgChan.In <- msg 200 | } 201 | case base.RtmpTypeIdVideo: 202 | if msg.IsVideoKeySeqHeader() { 203 | return 204 | } 205 | if conn.videoTrack != nil { 206 | conn.msgChan.In <- msg 207 | } 208 | } 209 | } 210 | 211 | func (conn *whepSession) OnStop() { 212 | conn.closeChan <- true 213 | } 214 | 215 | func (conn *whepSession) sendAudio(msg base.RtmpMsg) { 216 | if conn.audiopacker != nil { 217 | pkts, err := conn.audiopacker.Encode(msg) 218 | if err != nil { 219 | nazalog.Error(err) 220 | return 221 | } 222 | 223 | for _, pkt := range pkts { 224 | if err := conn.audioTrack.WriteRTP(pkt); err != nil { 225 | continue 226 | } 227 | } 228 | } 229 | } 230 | 231 | func (conn *whepSession) sendVideo(msg base.RtmpMsg) { 232 | if conn.videopacker != nil { 233 | 234 | pkts, err := conn.videopacker.Encode(msg) 235 | if err != nil { 236 | nazalog.Error(err) 237 | return 238 | } 239 | 240 | for _, pkt := range pkts { 241 | if err := conn.videoTrack.WriteRTP(pkt); err != nil { 242 | continue 243 | } 244 | } 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /rtc/whipsession.go: -------------------------------------------------------------------------------- 1 | package rtc 2 | 3 | import ( 4 | "github.com/gofrs/uuid" 5 | "github.com/pion/webrtc/v3" 6 | "github.com/q191201771/lal/pkg/base" 7 | "github.com/q191201771/lal/pkg/logic" 8 | "github.com/q191201771/naza/pkg/nazalog" 9 | ) 10 | 11 | type whipSession struct { 12 | streamid string 13 | pc *peerConnection 14 | lalServer logic.ILalServer 15 | lalSession logic.ICustomizePubSessionContext 16 | videoUnpacker *UnPacker 17 | audioUnpacker *UnPacker 18 | pktChan chan base.AvPacket 19 | closeChan chan bool 20 | subscriberId string 21 | } 22 | 23 | func NewWhipSession(streamid string, pc *peerConnection, lalServer logic.ILalServer) *whipSession { 24 | session, err := lalServer.AddCustomizePubSession(streamid) 25 | if err != nil { 26 | nazalog.Error(err) 27 | return nil 28 | } 29 | 30 | session.WithOption(func(option *base.AvPacketStreamOption) { 31 | option.VideoFormat = base.AvPacketStreamVideoFormatAnnexb 32 | }) 33 | 34 | u, _ := uuid.NewV4() 35 | 36 | return &whipSession{ 37 | streamid: streamid, 38 | pc: pc, 39 | lalServer: lalServer, 40 | lalSession: session, 41 | pktChan: make(chan base.AvPacket, 100), 42 | closeChan: make(chan bool, 2), 43 | subscriberId: u.String(), 44 | } 45 | } 46 | 47 | func (conn *whipSession) GetAnswerSDP(offer string) (sdp string) { 48 | gatherComplete := webrtc.GatheringCompletePromise(conn.pc.PeerConnection) 49 | 50 | conn.pc.SetRemoteDescription(webrtc.SessionDescription{ 51 | Type: webrtc.SDPTypeOffer, 52 | SDP: string(offer), 53 | }) 54 | 55 | answer, err := conn.pc.CreateAnswer(nil) 56 | if err != nil { 57 | nazalog.Error(err) 58 | return 59 | } 60 | 61 | err = conn.pc.SetLocalDescription(answer) 62 | if err != nil { 63 | nazalog.Error(err) 64 | return 65 | } 66 | 67 | <-gatherComplete 68 | 69 | sdp = conn.pc.LocalDescription().SDP 70 | return 71 | } 72 | 73 | func (conn *whipSession) Run() { 74 | 75 | conn.pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) { 76 | nazalog.Info("peer connection state: ", state.String()) 77 | 78 | switch state { 79 | case webrtc.PeerConnectionStateConnected: 80 | case webrtc.PeerConnectionStateDisconnected: 81 | fallthrough 82 | case webrtc.PeerConnectionStateFailed: 83 | fallthrough 84 | case webrtc.PeerConnectionStateClosed: 85 | conn.closeChan <- true 86 | } 87 | }) 88 | 89 | var videoPt webrtc.PayloadType 90 | conn.pc.OnTrack(func(tr *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { 91 | switch tr.Kind() { 92 | case webrtc.RTPCodecTypeVideo: 93 | conn.videoUnpacker = NewUnPacker(tr.Codec().MimeType, tr.Codec().ClockRate, conn.pktChan) 94 | videoPt = tr.PayloadType() 95 | case webrtc.RTPCodecTypeAudio: 96 | mimeType := tr.Codec().MimeType 97 | if tr.Codec().MimeType == "" { 98 | // pt为0或者8按照G711U和G711A处理,提高兼容性 99 | if tr.PayloadType() == 0 { 100 | mimeType = webrtc.MimeTypePCMU 101 | } else if tr.PayloadType() == 8 { 102 | mimeType = webrtc.MimeTypePCMA 103 | } 104 | } 105 | conn.audioUnpacker = NewUnPacker(mimeType, tr.Codec().ClockRate, conn.pktChan) 106 | } 107 | 108 | for { 109 | pkt, _, err := tr.ReadRTP() 110 | if err != nil { 111 | nazalog.Error(err) 112 | return 113 | } 114 | 115 | if conn.videoUnpacker != nil && pkt.Header.PayloadType == uint8(videoPt) { 116 | conn.videoUnpacker.UnPack(pkt) 117 | } else if conn.audioUnpacker != nil { 118 | conn.audioUnpacker.UnPack(pkt) 119 | } 120 | } 121 | }) 122 | 123 | for { 124 | select { 125 | case <-conn.closeChan: 126 | nazalog.Info("whip connect close, streamid:", conn.streamid) 127 | conn.lalServer.DelCustomizePubSession(conn.lalSession) 128 | return 129 | case pkt := <-conn.pktChan: 130 | conn.lalSession.FeedAvPacket(pkt) 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | ./lalmax -c conf/lalmax.conf.json -------------------------------------------------------------------------------- /server/http_notify.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020, Chef. All rights reserved. 2 | // https://github.com/q191201771/lal 3 | // 4 | // Use of this source code is governed by a MIT-style license 5 | // that can be found in the License file. 6 | // 7 | // Author: Chef (191201771@qq.com) 8 | 9 | package server 10 | 11 | import ( 12 | "net/http" 13 | "time" 14 | 15 | "github.com/q191201771/lalmax/hook" 16 | 17 | config "github.com/q191201771/lalmax/conf" 18 | 19 | "github.com/q191201771/lal/pkg/base" 20 | "github.com/q191201771/naza/pkg/nazahttp" 21 | "github.com/q191201771/naza/pkg/nazalog" 22 | ) 23 | 24 | // TODO(chef): refactor 配置参数供外部传入 25 | // TODO(chef): refactor maxTaskLen修改为能表示是阻塞任务的意思 26 | var ( 27 | maxTaskLen = 1024 28 | notifyTimeoutSec = 3 29 | ) 30 | 31 | var Log = nazalog.GetGlobalLogger() 32 | 33 | type PostTask struct { 34 | url string 35 | info interface{} 36 | } 37 | 38 | type HttpNotify struct { 39 | cfg config.HttpNotifyConfig 40 | 41 | serverId string 42 | 43 | taskQueue chan PostTask 44 | notifyUpdateQueue chan PostTask 45 | client *http.Client 46 | } 47 | 48 | func NewHttpNotify(cfg config.HttpNotifyConfig, serverId string) *HttpNotify { 49 | httpNotify := &HttpNotify{ 50 | cfg: cfg, 51 | serverId: serverId, 52 | taskQueue: make(chan PostTask, maxTaskLen), 53 | notifyUpdateQueue: make(chan PostTask, maxTaskLen), 54 | client: &http.Client{ 55 | Timeout: time.Duration(notifyTimeoutSec) * time.Second, 56 | }, 57 | } 58 | go httpNotify.RunLoop() 59 | go httpNotify.NotifyUpdateRunLoop() 60 | 61 | return httpNotify 62 | } 63 | 64 | // TODO(chef): Dispose 65 | 66 | // --------------------------------------------------------------------------------------------------------------------- 67 | 68 | func (h *HttpNotify) NotifyServerStart(info base.LalInfo) { 69 | info.ServerId = h.serverId 70 | h.asyncPost(h.cfg.OnServerStart, info) 71 | } 72 | 73 | func (h *HttpNotify) NotifyUpdate(info base.UpdateInfo) { 74 | info.ServerId = h.serverId 75 | for i, v := range info.Groups { 76 | exist, session := hook.GetHookSessionManagerInstance().GetHookSession(v.StreamName) 77 | if exist { 78 | info.Groups[i].StatSubs = append(info.Groups[i].StatSubs, session.GetAllConsumer()...) 79 | } 80 | } 81 | h.notifyUpdateAsyncPost(h.cfg.OnUpdate, info) 82 | } 83 | 84 | func (h *HttpNotify) NotifyPubStart(info base.PubStartInfo) { 85 | info.ServerId = h.serverId 86 | h.asyncPost(h.cfg.OnPubStart, info) 87 | } 88 | 89 | func (h *HttpNotify) NotifyPubStop(info base.PubStopInfo) { 90 | info.ServerId = h.serverId 91 | h.asyncPost(h.cfg.OnPubStop, info) 92 | } 93 | 94 | func (h *HttpNotify) NotifySubStart(info base.SubStartInfo) { 95 | info.ServerId = h.serverId 96 | h.asyncPost(h.cfg.OnSubStart, info) 97 | } 98 | 99 | func (h *HttpNotify) NotifySubStop(info base.SubStopInfo) { 100 | info.ServerId = h.serverId 101 | h.asyncPost(h.cfg.OnSubStop, info) 102 | } 103 | 104 | func (h *HttpNotify) NotifyPullStart(info base.PullStartInfo) { 105 | info.ServerId = h.serverId 106 | h.asyncPost(h.cfg.OnRelayPullStart, info) 107 | } 108 | 109 | func (h *HttpNotify) NotifyPullStop(info base.PullStopInfo) { 110 | info.ServerId = h.serverId 111 | h.asyncPost(h.cfg.OnRelayPullStop, info) 112 | } 113 | 114 | func (h *HttpNotify) NotifyRtmpConnect(info base.RtmpConnectInfo) { 115 | info.ServerId = h.serverId 116 | h.asyncPost(h.cfg.OnRtmpConnect, info) 117 | } 118 | 119 | func (h *HttpNotify) NotifyOnHlsMakeTs(info base.HlsMakeTsInfo) { 120 | info.ServerId = h.serverId 121 | h.asyncPost(h.cfg.OnHlsMakeTs, info) 122 | } 123 | 124 | // ----- implement INotifyHandler interface ---------------------------------------------------------------------------- 125 | 126 | func (h *HttpNotify) OnServerStart(info base.LalInfo) { 127 | h.NotifyServerStart(info) 128 | } 129 | 130 | func (h *HttpNotify) OnUpdate(info base.UpdateInfo) { 131 | h.NotifyUpdate(info) 132 | } 133 | 134 | func (h *HttpNotify) OnPubStart(info base.PubStartInfo) { 135 | h.NotifyPubStart(info) 136 | } 137 | 138 | func (h *HttpNotify) OnPubStop(info base.PubStopInfo) { 139 | h.NotifyPubStop(info) 140 | } 141 | 142 | func (h *HttpNotify) OnSubStart(info base.SubStartInfo) { 143 | h.NotifySubStart(info) 144 | } 145 | 146 | func (h *HttpNotify) OnSubStop(info base.SubStopInfo) { 147 | h.NotifySubStop(info) 148 | } 149 | 150 | func (h *HttpNotify) OnRelayPullStart(info base.PullStartInfo) { 151 | h.NotifyPullStart(info) 152 | } 153 | 154 | func (h *HttpNotify) OnRelayPullStop(info base.PullStopInfo) { 155 | h.NotifyPullStop(info) 156 | } 157 | 158 | func (h *HttpNotify) OnRtmpConnect(info base.RtmpConnectInfo) { 159 | h.NotifyRtmpConnect(info) 160 | } 161 | 162 | func (h *HttpNotify) OnHlsMakeTs(info base.HlsMakeTsInfo) { 163 | h.NotifyOnHlsMakeTs(info) 164 | } 165 | 166 | // --------------------------------------------------------------------------------------------------------------------- 167 | 168 | func (h *HttpNotify) RunLoop() { 169 | for { 170 | select { 171 | case t := <-h.taskQueue: 172 | h.post(t.url, t.info) 173 | } 174 | } 175 | } 176 | 177 | func (h *HttpNotify) NotifyUpdateRunLoop() { 178 | for { 179 | select { 180 | case t := <-h.notifyUpdateQueue: 181 | h.post(t.url, t.info) 182 | } 183 | } 184 | } 185 | 186 | // --------------------------------------------------------------------------------------------------------------------- 187 | 188 | func (h *HttpNotify) notifyUpdateAsyncPost(url string, info interface{}) { 189 | if !h.cfg.Enable || url == "" { 190 | return 191 | } 192 | 193 | select { 194 | case h.notifyUpdateQueue <- PostTask{url: url, info: info}: 195 | // noop 196 | default: 197 | Log.Error("http notify queue full.") 198 | } 199 | } 200 | 201 | func (h *HttpNotify) asyncPost(url string, info interface{}) { 202 | if !h.cfg.Enable || url == "" { 203 | return 204 | } 205 | 206 | select { 207 | case h.taskQueue <- PostTask{url: url, info: info}: 208 | // noop 209 | default: 210 | Log.Error("http notify queue full.") 211 | } 212 | } 213 | 214 | func (h *HttpNotify) post(url string, info interface{}) { 215 | if _, err := nazahttp.PostJson(url, info, h.client); err != nil { 216 | Log.Errorf("http notify post error. err=%+v, url=%s, info=%+v", err, url, info) 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /server/middle.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/q191201771/lal/pkg/base" 8 | ) 9 | 10 | func (s *LalMaxServer) Cors() gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | method := c.Request.Method 13 | origin := c.GetHeader("Origin") 14 | if len(origin) == 0 { 15 | c.Header("Access-Control-Allow-Origin", "*") 16 | } else { 17 | c.Header("Access-Control-Allow-Origin", origin) 18 | } 19 | //服务器支持的所有跨域请求的方法 20 | c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE") 21 | //允许跨域设置可以返回其他子段,可以自定义字段 22 | c.Header("Access-Control-Allow-Headers", "*") 23 | c.Header("Access-Control-Allow-Headers", "Content-Type,Access-Token") 24 | c.Header("Access-Control-Allow-Credentials", "true") 25 | c.Header("Cross-Origin-Resource-Policy", "cross-origin") 26 | 27 | //允许类型校验 28 | if method == "OPTIONS" { 29 | c.AbortWithStatus(http.StatusNoContent) 30 | } 31 | c.Next() 32 | } 33 | } 34 | 35 | // Authentication 接口鉴权 36 | func Authentication(secrets, ips []string) gin.HandlerFunc { 37 | out := base.ApiRespBasic{ 38 | ErrorCode: http.StatusUnauthorized, 39 | Desp: http.StatusText(http.StatusUnauthorized), 40 | } 41 | return func(c *gin.Context) { 42 | if !authentication(c.Query("token"), c.ClientIP(), secrets, ips) { 43 | c.JSON(200, out) 44 | return 45 | } 46 | c.Next() 47 | } 48 | } 49 | 50 | // authentication 判断是否符合要求,返回 false 表示鉴权失败 51 | func authentication(reqToken, clientIP string, secrets, ips []string) bool { 52 | // 秘钥过滤 53 | if len(secrets) > 0 && !containFn(secrets, reqToken) { 54 | return false 55 | } 56 | // ip 白名单过滤 57 | if len(ips) > 0 && !containFn(ips, clientIP) { 58 | return false 59 | } 60 | return true 61 | } 62 | 63 | func containFn[T comparable](ts []T, t T) bool { 64 | for _, v := range ts { 65 | if v == t { 66 | return true 67 | } 68 | } 69 | return false 70 | } 71 | -------------------------------------------------------------------------------- /server/router.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/q191201771/lalmax/hook" 9 | 10 | "github.com/q191201771/lalmax/gb28181" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/q191201771/lal/pkg/base" 14 | "github.com/q191201771/lal/pkg/logic" 15 | "github.com/q191201771/naza/pkg/nazahttp" 16 | "github.com/q191201771/naza/pkg/nazajson" 17 | "github.com/q191201771/naza/pkg/nazalog" 18 | ) 19 | 20 | func (s *LalMaxServer) InitRouter(router *gin.Engine) { 21 | if router == nil { 22 | return 23 | } 24 | router.Use(s.Cors()) 25 | 26 | rtc := router.Group("/webrtc") 27 | // whip 28 | rtc.POST("/whip", s.HandleWHIP) 29 | rtc.OPTIONS("/whip", s.HandleWHIP) 30 | rtc.DELETE("/whip", s.HandleWHIP) 31 | // whep 32 | rtc.POST("/whep", s.HandleWHEP) 33 | rtc.OPTIONS("/whep", s.HandleWHEP) 34 | rtc.DELETE("/whep", s.HandleWHEP) 35 | // Jessibuca flv封装play 36 | rtc.POST("/play/live/:streamid", s.HandleJessibuca) 37 | rtc.DELETE("/play/live/:streamid", s.HandleJessibuca) 38 | 39 | // http-fmp4 40 | router.GET("/live/m4s/:streamid", s.HandleHttpFmp4) 41 | 42 | // hls-fmp4/llhls 43 | router.GET("/live/hls/:streamid/:type", s.HandleHls) 44 | 45 | // onvif 46 | router.POST("/api/ctrl/onvif/pull", s.HandleOnvifPull) 47 | 48 | // gb 49 | gbLogic := gb28181.NewGbLogic(s.gbsbr) 50 | gb := router.Group("/api/gb") 51 | gb.GET("/device_infos", gbLogic.GetDeviceInfos) 52 | gb.POST("/start_play", gbLogic.StartPlay) 53 | gb.POST("/stop_play", gbLogic.StopPlay) 54 | gb.POST("/update_all_notify", gbLogic.UpdateAllNotify) 55 | gb.POST("/update_notify", gbLogic.UpdateNotify) 56 | gb.POST("/ptz_direction", gbLogic.PtzDirection) 57 | gb.POST("/ptz_zoom", gbLogic.PtzZoom) 58 | gb.POST("/ptz_fi", gbLogic.PtzFi) 59 | gb.POST("/ptz_preset", gbLogic.PtzPreset) 60 | gb.POST("/ptz_stop", gbLogic.PtzStop) 61 | 62 | auth := Authentication(s.conf.HttpConfig.CtrlAuthWhitelist.Secrets, s.conf.HttpConfig.CtrlAuthWhitelist.IPs) 63 | // stat 64 | stat := router.Group("/api/stat", auth) 65 | stat.GET("/group", s.statGroupHandler) 66 | stat.GET("/all_group", s.statAllGroupHandler) 67 | stat.GET("/lal_info", s.statLalInfoHandler) 68 | 69 | // ctrl 70 | ctrl := router.Group("/api/ctrl", auth) 71 | ctrl.POST("/start_relay_pull", s.ctrlStartRelayPullHandler) 72 | ctrl.POST("/stop_relay_pull", s.ctrlStopRelayPullHandler) 73 | ctrl.POST("/kick_session", s.ctrlKickSessionHandler) 74 | ctrl.POST("/start_rtp_pub", s.ctrlStartRtpPubHandler) 75 | } 76 | 77 | func (s *LalMaxServer) HandleWHIP(c *gin.Context) { 78 | switch c.Request.Method { 79 | case "POST": 80 | if s.rtcsvr != nil { 81 | s.rtcsvr.HandleWHIP(c) 82 | } 83 | case "DELETE": 84 | // TODO 实现DELETE 85 | c.Status(http.StatusOK) 86 | } 87 | } 88 | 89 | func (s *LalMaxServer) HandleWHEP(c *gin.Context) { 90 | switch c.Request.Method { 91 | case "POST": 92 | if s.rtcsvr != nil { 93 | s.rtcsvr.HandleWHEP(c) 94 | } 95 | case "DELETE": 96 | // TODO 实现DELETE 97 | c.Status(http.StatusOK) 98 | } 99 | } 100 | 101 | func (s *LalMaxServer) HandleJessibuca(c *gin.Context) { 102 | switch c.Request.Method { 103 | case "POST": 104 | if s.rtcsvr != nil { 105 | s.rtcsvr.HandleJessibuca(c) 106 | } 107 | case "DELETE": 108 | // TODO 实现DELETE 109 | c.Status(http.StatusOK) 110 | } 111 | } 112 | 113 | func (s *LalMaxServer) HandleHls(c *gin.Context) { 114 | if s.hlssvr != nil { 115 | s.hlssvr.HandleRequest(c) 116 | } else { 117 | nazalog.Error("hls is disable") 118 | c.Status(http.StatusNotFound) 119 | } 120 | } 121 | 122 | func (s *LalMaxServer) HandleHttpFmp4(c *gin.Context) { 123 | if s.httpfmp4svr != nil { 124 | s.httpfmp4svr.HandleRequest(c) 125 | } else { 126 | nazalog.Error("http-fmp4 is disable") 127 | c.Status(http.StatusNotFound) 128 | } 129 | } 130 | 131 | func (s *LalMaxServer) HandleOnvifPull(c *gin.Context) { 132 | if s.onvifsvr != nil { 133 | s.onvifsvr.HandlePull(c) 134 | } 135 | } 136 | 137 | func (s *LalMaxServer) statGroupHandler(c *gin.Context) { 138 | var v base.ApiStatGroupResp 139 | streamName := c.Query("stream_name") 140 | if streamName == "" { 141 | v.ErrorCode = base.ErrorCodeParamMissing 142 | v.Desp = base.DespParamMissing 143 | c.JSON(http.StatusOK, v) 144 | return 145 | } 146 | v.Data = s.lalsvr.StatGroup(streamName) 147 | if v.Data == nil { 148 | v.ErrorCode = base.ErrorCodeGroupNotFound 149 | v.Desp = base.DespGroupNotFound 150 | c.JSON(http.StatusOK, v) 151 | return 152 | } 153 | exist, session := hook.GetHookSessionManagerInstance().GetHookSession(streamName) 154 | if exist { 155 | v.Data.StatSubs = append(v.Data.StatSubs, session.GetAllConsumer()...) 156 | } 157 | v.ErrorCode = base.ErrorCodeSucc 158 | v.Desp = base.DespSucc 159 | c.JSON(http.StatusOK, v) 160 | } 161 | 162 | func (s *LalMaxServer) statAllGroupHandler(c *gin.Context) { 163 | var out base.ApiStatAllGroupResp 164 | out.ErrorCode = base.ErrorCodeSucc 165 | out.Desp = base.DespSucc 166 | groups := s.lalsvr.StatAllGroup() 167 | for i, group := range groups { 168 | exist, session := hook.GetHookSessionManagerInstance().GetHookSession(group.StreamName) 169 | if exist { 170 | groups[i].StatSubs = append(groups[i].StatSubs, session.GetAllConsumer()...) 171 | } 172 | } 173 | out.Data.Groups = groups 174 | c.JSON(http.StatusOK, out) 175 | } 176 | 177 | func (s *LalMaxServer) statLalInfoHandler(c *gin.Context) { 178 | var v base.ApiStatLalInfoResp 179 | v.ErrorCode = base.ErrorCodeSucc 180 | v.Desp = base.DespSucc 181 | v.Data = s.lalsvr.StatLalInfo() 182 | c.JSON(http.StatusOK, v) 183 | } 184 | 185 | func (s *LalMaxServer) ctrlStartRelayPullHandler(c *gin.Context) { 186 | var info base.ApiCtrlStartRelayPullReq 187 | var v base.ApiCtrlStartRelayPullResp 188 | j, err := unmarshalRequestJSONBody(c.Request, &info, "url") 189 | if err != nil { 190 | Log.Warnf("http api start pull error. err=%+v", err) 191 | v.ErrorCode = base.ErrorCodeParamMissing 192 | v.Desp = base.DespParamMissing 193 | c.JSON(http.StatusOK, v) 194 | return 195 | } 196 | 197 | if !j.Exist("pull_timeout_ms") { 198 | info.PullTimeoutMs = logic.DefaultApiCtrlStartRelayPullReqPullTimeoutMs 199 | } 200 | if !j.Exist("pull_retry_num") { 201 | info.PullRetryNum = base.PullRetryNumNever 202 | } 203 | if !j.Exist("auto_stop_pull_after_no_out_ms") { 204 | info.AutoStopPullAfterNoOutMs = base.AutoStopPullAfterNoOutMsNever 205 | } 206 | if !j.Exist("rtsp_mode") { 207 | info.RtspMode = base.RtspModeTcp 208 | } 209 | 210 | Log.Infof("http api start pull. req info=%+v", info) 211 | 212 | resp := s.lalsvr.CtrlStartRelayPull(info) 213 | c.JSON(http.StatusOK, resp) 214 | } 215 | 216 | func (s *LalMaxServer) ctrlStopRelayPullHandler(c *gin.Context) { 217 | var v base.ApiCtrlStopRelayPullResp 218 | streamName := c.Query("stream_name") 219 | if streamName == "" { 220 | v.ErrorCode = base.ErrorCodeParamMissing 221 | v.Desp = base.DespParamMissing 222 | c.JSON(http.StatusOK, v) 223 | return 224 | } 225 | 226 | Log.Infof("http api stop pull. stream_name=%s", streamName) 227 | 228 | resp := s.lalsvr.CtrlStopRelayPull(streamName) 229 | c.JSON(http.StatusOK, resp) 230 | } 231 | 232 | func (s *LalMaxServer) ctrlKickSessionHandler(c *gin.Context) { 233 | var v base.ApiCtrlKickSessionResp 234 | var info base.ApiCtrlKickSessionReq 235 | 236 | _, err := unmarshalRequestJSONBody(c.Request, &info, "stream_name", "session_id") 237 | if err != nil { 238 | Log.Warnf("http api kick session error. err=%+v", err) 239 | v.ErrorCode = base.ErrorCodeParamMissing 240 | v.Desp = base.DespParamMissing 241 | c.JSON(http.StatusOK, v) 242 | return 243 | } 244 | 245 | Log.Infof("http api kick session. req info=%+v", info) 246 | 247 | resp := s.lalsvr.CtrlKickSession(info) 248 | c.JSON(http.StatusOK, resp) 249 | } 250 | 251 | func (s *LalMaxServer) ctrlStartRtpPubHandler(c *gin.Context) { 252 | var v base.ApiCtrlStartRtpPubResp 253 | var info base.ApiCtrlStartRtpPubReq 254 | 255 | j, err := unmarshalRequestJSONBody(c.Request, &info, "stream_name") 256 | if err != nil { 257 | Log.Warnf("http api start rtp pub error. err=%+v", err) 258 | v.ErrorCode = base.ErrorCodeParamMissing 259 | v.Desp = base.DespParamMissing 260 | c.JSON(http.StatusOK, v) 261 | return 262 | } 263 | 264 | if !j.Exist("timeout_ms") { 265 | info.TimeoutMs = logic.DefaultApiCtrlStartRtpPubReqTimeoutMs 266 | } 267 | 268 | Log.Infof("http api start rtp pub. req info=%+v", info) 269 | 270 | lal := s.lalsvr.(*logic.ServerManager) 271 | resp := lal.CtrlStartRtpPub(info) 272 | c.JSON(http.StatusOK, resp) 273 | } 274 | 275 | func unmarshalRequestJSONBody(r *http.Request, info interface{}, keyFieldList ...string) (nazajson.Json, error) { 276 | body, err := io.ReadAll(r.Body) 277 | if err != nil { 278 | return nazajson.Json{}, err 279 | } 280 | 281 | j, err := nazajson.New(body) 282 | if err != nil { 283 | return j, err 284 | } 285 | for _, kf := range keyFieldList { 286 | if !j.Exist(kf) { 287 | return j, nazahttp.ErrParamMissing 288 | } 289 | } 290 | 291 | return j, json.Unmarshal(body, info) 292 | } 293 | -------------------------------------------------------------------------------- /server/router_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/q191201771/lalmax/hook" 13 | 14 | config "github.com/q191201771/lalmax/conf" 15 | 16 | "github.com/q191201771/lal/pkg/base" 17 | ) 18 | 19 | var max *LalMaxServer 20 | 21 | const httpNotifyAddr = ":55559" 22 | 23 | func TestMain(m *testing.M) { 24 | var err error 25 | max, err = NewLalMaxServer(&config.Config{ 26 | HttpFmp4Config: config.HttpFmp4Config{Enable: true}, 27 | LalSvrConfigPath: "../conf/lalserver.conf.json", 28 | HttpConfig: config.HttpConfig{ 29 | ListenAddr: ":52349", 30 | }, 31 | HttpNotifyConfig: config.HttpNotifyConfig{ 32 | Enable: true, 33 | UpdateIntervalSec: 2, 34 | OnUpdate: fmt.Sprintf("http://127.0.0.1%s/on_update", httpNotifyAddr), 35 | }, 36 | }) 37 | if err != nil { 38 | panic(err) 39 | } 40 | go max.Run() 41 | os.Exit(m.Run()) 42 | } 43 | 44 | func TestAllGroup(t *testing.T) { 45 | _, err := max.lalsvr.AddCustomizePubSession("test") 46 | if err != nil { 47 | t.Fatal(err) 48 | } 49 | t.Run("no consumer", func(t *testing.T) { 50 | r := httptest.NewRecorder() 51 | req := httptest.NewRequest("GET", "/api/stat/all_group", nil) 52 | max.router.ServeHTTP(r, req) 53 | resp := r.Result() 54 | if resp.StatusCode != 200 { 55 | t.Fatal(resp.Status) 56 | } 57 | var out base.ApiStatAllGroupResp 58 | if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { 59 | t.Fatal(err) 60 | } 61 | if len(out.Data.Groups) <= 0 { 62 | t.Fatal("no group") 63 | } 64 | if len(out.Data.Groups[0].StatSubs) != 0 { 65 | t.Fatal("subs err") 66 | } 67 | }) 68 | 69 | t.Run("has consumer", func(t *testing.T) { 70 | ss := hook.NewHookSession("test", "test", max.hlssvr, 1, 0) 71 | ss.AddConsumer("consumer1", nil) 72 | hook.GetHookSessionManagerInstance().SetHookSession("test", ss) 73 | 74 | r := httptest.NewRecorder() 75 | req := httptest.NewRequest("GET", "/api/stat/all_group", nil) 76 | max.router.ServeHTTP(r, req) 77 | resp := r.Result() 78 | if resp.StatusCode != 200 { 79 | t.Fatal(resp.Status) 80 | } 81 | var out base.ApiStatAllGroupResp 82 | if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { 83 | t.Fatal(err) 84 | } 85 | if len(out.Data.Groups) <= 0 { 86 | t.Fatal("no group") 87 | } 88 | if len(out.Data.Groups[0].StatSubs) <= 0 { 89 | t.Fatal("subs err") 90 | } 91 | group := out.Data.Groups[0] 92 | if group.StatSubs[0].SessionId != "consumer1" { 93 | t.Fatal("SessionId err") 94 | } 95 | }) 96 | } 97 | 98 | func TestNotifyUpdate(t *testing.T) { 99 | _, err := max.lalsvr.AddCustomizePubSession("test") 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | ss := hook.NewHookSession("test", "test", max.hlssvr, 1, 0) 104 | ss.AddConsumer("consumer1", nil) 105 | hook.GetHookSessionManagerInstance().SetHookSession("test", ss) 106 | 107 | http.HandleFunc("/on_update", func(w http.ResponseWriter, r *http.Request) { 108 | var out base.ApiStatAllGroupResp 109 | if err := json.NewDecoder(r.Body).Decode(&out); err != nil { 110 | t.Fatal(err) 111 | } 112 | if len(out.Data.Groups) <= 0 { 113 | t.Fatal("no group") 114 | } 115 | if len(out.Data.Groups[0].StatSubs) <= 0 { 116 | t.Fatal("subs err") 117 | } 118 | group := out.Data.Groups[0] 119 | if group.StatSubs[0].SessionId != "consumer1" { 120 | t.Fatal("SessionId err") 121 | } 122 | }) 123 | go http.ListenAndServe(httpNotifyAddr, nil) 124 | time.Sleep(time.Second * 3) 125 | } 126 | 127 | func TestAuthentication(t *testing.T) { 128 | t.Run("无须鉴权", func(t *testing.T) { 129 | if !authentication("12", "192.168.0.2", nil, nil) { 130 | t.Fatal("期望通过, 但实际未通过") 131 | } 132 | }) 133 | t.Run("Token 鉴权失败", func(t *testing.T) { 134 | if authentication("1", "192.168.0.2", []string{"12"}, nil) { 135 | t.Fatal("期望不通过, 但实际通过") 136 | } 137 | }) 138 | t.Run("token 鉴权成功", func(t *testing.T) { 139 | if !authentication("12", "192.168.0.2", []string{"12"}, nil) { 140 | t.Fatal("期望通过, 但实际不通过") 141 | } 142 | }) 143 | t.Run("ip 白名单鉴权失败", func(t *testing.T) { 144 | if authentication("12", "192.168.0.2", nil, []string{"192.168.1.2"}) { 145 | t.Fatal("期望不通过, 但实际通过") 146 | } 147 | }) 148 | t.Run("ip 白名单鉴权成功", func(t *testing.T) { 149 | if !authentication("12", "192.168.0.2", []string{"12"}, []string{"192.168.0.2"}) { 150 | t.Fatal("期望通过, 但实际不通过") 151 | } 152 | }) 153 | t.Run("两种模式结合鉴权通过", func(t *testing.T) { 154 | if !authentication("12", "192.168.0.2", []string{"12"}, []string{"192.168.0.2"}) { 155 | t.Fatal("期望通过, 但实际不通过") 156 | } 157 | }) 158 | } 159 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "net/http" 7 | 8 | "github.com/q191201771/lalmax/srt" 9 | 10 | "github.com/q191201771/lalmax/rtc" 11 | 12 | "github.com/q191201771/lalmax/onvif" 13 | 14 | "github.com/q191201771/lalmax/hook" 15 | 16 | "github.com/q191201771/lalmax/gb28181" 17 | 18 | "github.com/q191201771/lalmax/room" 19 | 20 | httpfmp4 "github.com/q191201771/lalmax/fmp4/http-fmp4" 21 | 22 | "github.com/q191201771/lalmax/fmp4/hls" 23 | 24 | config "github.com/q191201771/lalmax/conf" 25 | 26 | "github.com/gin-gonic/gin" 27 | "github.com/q191201771/lal/pkg/logic" 28 | "github.com/q191201771/naza/pkg/nazalog" 29 | ) 30 | 31 | type LalMaxServer struct { 32 | lalsvr logic.ILalServer 33 | conf *config.Config 34 | srtsvr *srt.SrtServer 35 | rtcsvr *rtc.RtcServer 36 | router *gin.Engine 37 | routerTls *gin.Engine 38 | httpfmp4svr *httpfmp4.HttpFmp4Server 39 | hlssvr *hls.HlsServer 40 | gbsbr *gb28181.GB28181Server 41 | onvifsvr *onvif.OnvifServer 42 | roomsvr *room.RoomServer 43 | } 44 | 45 | func NewLalMaxServer(conf *config.Config) (*LalMaxServer, error) { 46 | lalsvr := logic.NewLalServer(func(option *logic.Option) { 47 | option.ConfFilename = conf.LalSvrConfigPath 48 | option.NotifyHandler = NewHttpNotify(conf.HttpNotifyConfig, conf.ServerId) 49 | }) 50 | 51 | maxsvr := &LalMaxServer{ 52 | lalsvr: lalsvr, 53 | conf: conf, 54 | } 55 | 56 | if conf.SrtConfig.Enable { 57 | maxsvr.srtsvr = srt.NewSrtServer(conf.SrtConfig.Addr, lalsvr, func(option *srt.SrtOption) { 58 | option.Latency = 300 59 | option.PeerLatency = 300 60 | }) 61 | } 62 | 63 | if conf.RtcConfig.Enable { 64 | var err error 65 | maxsvr.rtcsvr, err = rtc.NewRtcServer(conf.RtcConfig, lalsvr) 66 | if err != nil { 67 | nazalog.Error("create rtc svr failed, err:", err) 68 | return nil, err 69 | } 70 | } 71 | 72 | if conf.HttpFmp4Config.Enable { 73 | maxsvr.httpfmp4svr = httpfmp4.NewHttpFmp4Server() 74 | } 75 | 76 | if conf.HlsConfig.Enable { 77 | maxsvr.hlssvr = hls.NewHlsServer(conf.HlsConfig) 78 | } 79 | 80 | if conf.GB28181Config.Enable { 81 | maxsvr.gbsbr = gb28181.NewGB28181Server(conf.GB28181Config, lalsvr) 82 | } 83 | 84 | if conf.OnvifConfig.Enable { 85 | maxsvr.onvifsvr = onvif.NewOnvifServer() 86 | } 87 | 88 | if conf.RoomConfig.Enable { 89 | maxsvr.roomsvr = room.NewRoomServer(conf.RoomConfig.APIKey, conf.RoomConfig.APISecret) 90 | } 91 | 92 | maxsvr.router = gin.Default() 93 | maxsvr.InitRouter(maxsvr.router) 94 | if conf.HttpConfig.EnableHttps { 95 | maxsvr.routerTls = gin.Default() 96 | maxsvr.InitRouter(maxsvr.routerTls) 97 | } 98 | 99 | return maxsvr, nil 100 | } 101 | 102 | func (s *LalMaxServer) Run() (err error) { 103 | s.lalsvr.WithOnHookSession(func(uniqueKey string, streamName string) logic.ICustomizeHookSessionContext { 104 | // 有新的流了,创建业务层的对象,用于hook这个流 105 | return hook.NewHookSession(uniqueKey, streamName, s.hlssvr, s.conf.HookConfig.GopCacheNum, s.conf.HookConfig.SingleGopMaxFrameNum) 106 | }) 107 | 108 | ctx, cancel := context.WithCancel(context.Background()) 109 | defer cancel() 110 | 111 | if s.srtsvr != nil { 112 | go s.srtsvr.Run(ctx) 113 | } 114 | 115 | go func() { 116 | nazalog.Infof("lalmax http listen. addr=%s", s.conf.HttpConfig.ListenAddr) 117 | if err = s.router.Run(s.conf.HttpConfig.ListenAddr); err != nil { 118 | nazalog.Infof("lalmax http stop. addr=%s", s.conf.HttpConfig.ListenAddr) 119 | } 120 | }() 121 | 122 | if s.conf.HttpConfig.EnableHttps { 123 | server := &http.Server{Addr: s.conf.HttpConfig.HttpsListenAddr, Handler: s.routerTls, TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){}} 124 | go func() { 125 | nazalog.Infof("lalmax https listen. addr=%s", s.conf.HttpConfig.HttpsListenAddr) 126 | if err = server.ListenAndServeTLS(s.conf.HttpConfig.HttpsCertFile, s.conf.HttpConfig.HttpsKeyFile); err != nil { 127 | nazalog.Infof("lalmax https stop. addr=%s", s.conf.HttpConfig.ListenAddr) 128 | } 129 | }() 130 | } 131 | 132 | if s.gbsbr != nil { 133 | go s.gbsbr.Start() 134 | } 135 | 136 | if s.roomsvr != nil { 137 | go s.roomsvr.Start() 138 | } 139 | 140 | return s.lalsvr.RunLoop() 141 | } 142 | -------------------------------------------------------------------------------- /srt/pub.go: -------------------------------------------------------------------------------- 1 | package srt 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | srt "github.com/datarhei/gosrt" 7 | "github.com/q191201771/lal/pkg/aac" 8 | "github.com/q191201771/lal/pkg/base" 9 | "github.com/q191201771/lal/pkg/logic" 10 | "github.com/q191201771/naza/pkg/nazalog" 11 | codec "github.com/yapingcat/gomedia/go-codec" 12 | ts "github.com/yapingcat/gomedia/go-mpeg2" 13 | ) 14 | 15 | type Publisher struct { 16 | ctx context.Context 17 | srv *SrtServer 18 | ss logic.ICustomizePubSessionContext 19 | streamName string 20 | demuxer *ts.TSDemuxer 21 | conn srt.Conn 22 | subscribers []*Subscriber 23 | } 24 | 25 | func NewPublisher(ctx context.Context, conn srt.Conn, streamName string, srv *SrtServer) *Publisher { 26 | pub := &Publisher{ 27 | ctx: ctx, 28 | srv: srv, 29 | streamName: streamName, 30 | conn: conn, 31 | demuxer: ts.NewTSDemuxer(), 32 | } 33 | 34 | nazalog.Infof("create srt publisher, streamName:%s", streamName) 35 | return pub 36 | } 37 | 38 | func (p *Publisher) SetSession(session logic.ICustomizePubSessionContext) { 39 | p.ss = session 40 | } 41 | 42 | func (p *Publisher) Run() { 43 | defer func() { 44 | p.conn.Close() 45 | p.srv.Remove(p.streamName, p.ss) 46 | }() 47 | audioSampleRate := uint32(0) 48 | var foundAudio bool 49 | p.demuxer.OnFrame = func(cid ts.TS_STREAM_TYPE, frame []byte, pts uint64, dts uint64) { 50 | var pkt base.AvPacket 51 | if cid == ts.TS_STREAM_AAC { 52 | if !foundAudio { 53 | if asc, err := codec.ConvertADTSToASC(frame); err != nil { 54 | return 55 | } else { 56 | p.ss.FeedAudioSpecificConfig(asc.Encode()) 57 | audioSampleRate = uint32(codec.AACSampleIdxToSample(int(asc.Sample_freq_index))) 58 | } 59 | 60 | foundAudio = true 61 | } 62 | 63 | var preAudioDts uint64 64 | ctx := aac.AdtsHeaderContext{} 65 | for len(frame) > aac.AdtsHeaderLength { 66 | ctx.Unpack(frame[:]) 67 | if preAudioDts == 0 { 68 | preAudioDts = dts 69 | } else { 70 | preAudioDts += uint64(1024 * 1000 / audioSampleRate) 71 | } 72 | 73 | aacPacket := base.AvPacket{ 74 | Timestamp: int64(preAudioDts), 75 | PayloadType: base.AvPacketPtAac, 76 | Pts: int64(preAudioDts), 77 | } 78 | if len(frame) >= int(ctx.AdtsLength) { 79 | Payload := frame[aac.AdtsHeaderLength:ctx.AdtsLength] 80 | if len(frame) > int(ctx.AdtsLength) { 81 | frame = frame[ctx.AdtsLength:] 82 | } else { 83 | frame = frame[0:0] 84 | } 85 | aacPacket.Payload = Payload 86 | p.ss.FeedAvPacket(aacPacket) 87 | } 88 | 89 | } 90 | } else if cid == ts.TS_STREAM_H264 { 91 | pkt.Payload = frame 92 | pkt.PayloadType = base.AvPacketPtAvc 93 | pkt.Pts = int64(pts) 94 | pkt.Timestamp = int64(dts) 95 | p.ss.FeedAvPacket(pkt) 96 | } else if cid == ts.TS_STREAM_H265 { 97 | pkt.Payload = frame 98 | pkt.PayloadType = base.AvPacketPtHevc 99 | pkt.Pts = int64(pts) 100 | pkt.Timestamp = int64(dts) 101 | p.ss.FeedAvPacket(pkt) 102 | } 103 | } 104 | err := p.demuxer.Input(bufio.NewReader(p.conn)) 105 | if err != nil { 106 | nazalog.Infof("stream [%s] disconnected", p.streamName) 107 | } 108 | return 109 | } 110 | -------------------------------------------------------------------------------- /srt/server.go: -------------------------------------------------------------------------------- 1 | package srt 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | "time" 7 | 8 | srt "github.com/datarhei/gosrt" 9 | "github.com/q191201771/lal/pkg/base" 10 | "github.com/q191201771/lal/pkg/logic" 11 | "github.com/q191201771/naza/pkg/nazalog" 12 | ) 13 | 14 | type SrtServer struct { 15 | addr string 16 | lalServer logic.ILalServer 17 | srtOpt SrtOption 18 | } 19 | type SrtOption struct { 20 | Latency int 21 | RecvLatency int 22 | PeerLatency int 23 | TlpktDrop bool 24 | TsbpdMode bool 25 | RecvBuf int 26 | SendBuf int 27 | MaxSendPacketSize int 28 | } 29 | 30 | var defaultSrtOption = SrtOption{ 31 | Latency: 300, 32 | RecvLatency: 300, 33 | PeerLatency: 300, 34 | TlpktDrop: true, 35 | TsbpdMode: true, 36 | RecvBuf: 2 * 1024 * 1024, 37 | SendBuf: 2 * 1024 * 1024, 38 | MaxSendPacketSize: 4, 39 | } 40 | 41 | type ModSrtOption func(option *SrtOption) 42 | 43 | func NewSrtServer(addr string, lal logic.ILalServer, modOptions ...ModSrtOption) *SrtServer { 44 | opt := defaultSrtOption 45 | for _, fn := range modOptions { 46 | fn(&opt) 47 | } 48 | svr := &SrtServer{ 49 | addr: addr, 50 | lalServer: lal, 51 | srtOpt: opt, 52 | } 53 | 54 | nazalog.Info("create srt server") 55 | return svr 56 | } 57 | 58 | func (s *SrtServer) Run(ctx context.Context) { 59 | conf := srt.DefaultConfig() 60 | conf.Latency = time.Millisecond * time.Duration(s.srtOpt.Latency) 61 | conf.ReceiverLatency = time.Millisecond * time.Duration(s.srtOpt.RecvLatency) 62 | conf.PeerLatency = time.Millisecond * time.Duration(s.srtOpt.PeerLatency) 63 | conf.TooLatePacketDrop = s.srtOpt.TlpktDrop 64 | conf.TSBPDMode = s.srtOpt.TsbpdMode 65 | conf.SendBufferSize = uint32(s.srtOpt.SendBuf) 66 | conf.ReceiverBufferSize = uint32(s.srtOpt.RecvBuf) 67 | 68 | srtlistener, err := srt.Listen("srt", s.addr, conf) 69 | if err != nil { 70 | panic(err) 71 | } 72 | 73 | defer srtlistener.Close() 74 | 75 | nazalog.Info("srt server listen addr:", s.addr) 76 | 77 | for { 78 | select { 79 | case <-ctx.Done(): 80 | return 81 | default: 82 | 83 | } 84 | 85 | var info StreamInfo 86 | conn, mode, err := srtlistener.Accept(func(req srt.ConnRequest) srt.ConnType { 87 | info = getStreamInfo(req.StreamId()) 88 | return info.Mode 89 | }) 90 | 91 | if err != nil { 92 | // rejected connection, ignore 93 | continue 94 | } 95 | 96 | if mode == srt.REJECT { 97 | // rejected connection, ignore 98 | continue 99 | } 100 | 101 | if info.Mode == srt.PUBLISH { 102 | go s.handlePublish(ctx, conn, info.StreamName) 103 | } else { 104 | go s.handleSubcribe(ctx, conn, info.StreamName) 105 | } 106 | } 107 | } 108 | 109 | func (s *SrtServer) handlePublish(ctx context.Context, conn srt.Conn, streamid string) { 110 | publisher := NewPublisher(ctx, conn, streamid, s) 111 | session, err := s.lalServer.AddCustomizePubSession(streamid) 112 | if err != nil { 113 | nazalog.Error(err) 114 | } 115 | 116 | if session != nil { 117 | session.WithOption(func(option *base.AvPacketStreamOption) { 118 | option.VideoFormat = base.AvPacketStreamVideoFormatAnnexb 119 | }) 120 | } 121 | 122 | publisher.SetSession(session) 123 | publisher.Run() 124 | } 125 | 126 | func (s *SrtServer) handleSubcribe(ctx context.Context, conn srt.Conn, streamid string) { 127 | subscriber := NewSubscriber(ctx, conn, streamid, s.srtOpt.MaxSendPacketSize) 128 | subscriber.Run() 129 | } 130 | 131 | func (s *SrtServer) Remove(host string, ss logic.ICustomizePubSessionContext) { 132 | s.lalServer.DelCustomizePubSession(ss) 133 | } 134 | 135 | type StreamInfo struct { 136 | StreamName string 137 | Mode srt.ConnType 138 | } 139 | 140 | func getStreamInfo(streamid string) StreamInfo { 141 | info := StreamInfo{ 142 | Mode: srt.REJECT, 143 | } 144 | 145 | s := strings.TrimLeft(streamid, "#!::") 146 | values := strings.Split(s, ",") 147 | for _, v := range values { 148 | ss := strings.Split(v, "=") 149 | name := ss[0] 150 | switch name { 151 | case "h": 152 | info.StreamName = ss[1] 153 | case "m": 154 | switch ss[1] { 155 | case "publish": 156 | info.Mode = srt.PUBLISH 157 | case "request": 158 | info.Mode = srt.SUBSCRIBE 159 | } 160 | } 161 | } 162 | 163 | return info 164 | } 165 | -------------------------------------------------------------------------------- /srt/stream_id.go: -------------------------------------------------------------------------------- 1 | package srt 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | ) 7 | 8 | type StreamID struct { 9 | User string 10 | Host string 11 | Resource string 12 | SessionID string 13 | Type string 14 | Mode string 15 | } 16 | 17 | func parseStreamID(streamID string) (*StreamID, error) { 18 | if !strings.Contains(streamID, "#!::") { 19 | return nil, errors.New("invalid streamid") 20 | } 21 | split := strings.Split(strings.TrimPrefix(streamID, "#!::"), ",") 22 | id := &StreamID{} 23 | 24 | for _, s := range split { 25 | if strings.Contains(s, "=") { 26 | kv := strings.Split(s, "=") 27 | if len(kv) != 2 { 28 | return nil, errors.New("invalid streamid") 29 | } 30 | 31 | if kv[0] == "u" { 32 | id.User = kv[1] 33 | } 34 | if kv[0] == "h" { 35 | id.Host = kv[1] 36 | } 37 | if kv[0] == "r" { 38 | id.Resource = kv[1] 39 | } 40 | if kv[0] == "s" { 41 | id.SessionID = kv[1] 42 | } 43 | if kv[0] == "t" { 44 | id.Type = kv[1] 45 | } 46 | if kv[0] == "m" { 47 | id.Mode = kv[1] 48 | } 49 | } 50 | } 51 | return id, nil 52 | } 53 | -------------------------------------------------------------------------------- /srt/sub.go: -------------------------------------------------------------------------------- 1 | package srt 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/q191201771/lalmax/hook" 7 | 8 | srt "github.com/datarhei/gosrt" 9 | "github.com/gofrs/uuid" 10 | "github.com/q191201771/lal/pkg/base" 11 | "github.com/q191201771/naza/pkg/nazalog" 12 | codec "github.com/yapingcat/gomedia/go-codec" 13 | flv "github.com/yapingcat/gomedia/go-flv" 14 | ts "github.com/yapingcat/gomedia/go-mpeg2" 15 | ) 16 | 17 | type Subscriber struct { 18 | ctx context.Context 19 | conn srt.Conn 20 | streamName string 21 | muxer *ts.TSMuxer 22 | hasInit bool 23 | videoPid uint16 24 | audioPid uint16 25 | flvVideoDemuxer flv.VideoTagDemuxer 26 | flvAudioDemuxer flv.AudioTagDemuxer 27 | videodts uint32 28 | audiodts uint32 29 | subscriberId string 30 | maxSendPacketSize int 31 | } 32 | 33 | func NewSubscriber(ctx context.Context, conn srt.Conn, streamName string, maxSendPacketSize int) *Subscriber { 34 | u, _ := uuid.NewV4() 35 | sub := &Subscriber{ 36 | ctx: ctx, 37 | conn: conn, 38 | streamName: streamName, 39 | muxer: ts.NewTSMuxer(), 40 | subscriberId: u.String(), 41 | maxSendPacketSize: maxSendPacketSize, 42 | } 43 | 44 | nazalog.Infof("create srt subscriber, streamName:%s, subscriberId:%s", streamName, sub.subscriberId) 45 | 46 | return sub 47 | } 48 | 49 | func (s *Subscriber) Run() { 50 | ok, session := hook.GetHookSessionManagerInstance().GetHookSession(s.streamName) 51 | if ok { 52 | var err error 53 | sendBuf := make([]byte, 0, s.maxSendPacketSize*ts.TS_PAKCET_SIZE) 54 | session.AddConsumer(s.subscriberId, s) 55 | s.muxer.OnPacket = func(tsPacket []byte) { 56 | defer func() { 57 | if err != nil { 58 | nazalog.Info("close srt socket") 59 | s.conn.Close() 60 | } 61 | 62 | }() 63 | 64 | select { 65 | case <-s.ctx.Done(): 66 | return 67 | default: 68 | } 69 | if len(sendBuf) > (s.maxSendPacketSize-1)*ts.TS_PAKCET_SIZE { 70 | if _, err = s.conn.Write(sendBuf); err != nil { 71 | session.RemoveConsumer(s.subscriberId) 72 | return 73 | } 74 | sendBuf = sendBuf[0:0] 75 | } 76 | sendBuf = append(sendBuf, tsPacket...) 77 | 78 | } 79 | } else { 80 | nazalog.Warnf("not found hook session, streamName:%s", s.streamName) 81 | s.conn.Close() 82 | } 83 | } 84 | 85 | func (s *Subscriber) OnMsg(msg base.RtmpMsg) { 86 | var err error 87 | if !s.hasInit { 88 | ok, session := hook.GetHookSessionManagerInstance().GetHookSession(s.streamName) 89 | if ok { 90 | videoheader := session.GetVideoSeqHeaderMsg() 91 | if videoheader != nil { 92 | if videoheader.IsAvcKeySeqHeader() { 93 | s.videoPid = s.muxer.AddStream(ts.TS_STREAM_H264) 94 | s.flvVideoDemuxer = flv.CreateFlvVideoTagHandle(flv.FLV_AVC) 95 | } else { 96 | s.videoPid = s.muxer.AddStream(ts.TS_STREAM_H265) 97 | s.flvVideoDemuxer = flv.CreateFlvVideoTagHandle(flv.FLV_HEVC) 98 | } 99 | 100 | s.flvVideoDemuxer.OnFrame(func(codecid codec.CodecID, b []byte, cts int) { 101 | s.muxer.Write(s.videoPid, b, uint64(s.videodts)+uint64(cts), uint64(s.videodts)) 102 | }) 103 | 104 | if err = s.flvVideoDemuxer.Decode(videoheader.Payload); err != nil { 105 | nazalog.Error(err) 106 | return 107 | } 108 | } 109 | 110 | audioheader := session.GetAudioSeqHeaderMsg() 111 | if audioheader != nil { 112 | if audioheader.IsAacSeqHeader() { 113 | s.audioPid = s.muxer.AddStream(ts.TS_STREAM_AAC) 114 | } else { 115 | return 116 | } 117 | 118 | s.flvAudioDemuxer = flv.CreateAudioTagDemuxer(flv.FLV_AAC) 119 | s.flvAudioDemuxer.OnFrame(func(codecid codec.CodecID, b []byte) { 120 | s.muxer.Write(s.audioPid, b, uint64(s.audiodts), uint64(s.audiodts)) 121 | }) 122 | 123 | if err = s.flvAudioDemuxer.Decode(audioheader.Payload); err != nil { 124 | nazalog.Error(err) 125 | return 126 | } 127 | } 128 | } 129 | 130 | s.hasInit = true 131 | } 132 | 133 | if msg.Header.MsgTypeId == base.RtmpTypeIdVideo { 134 | s.videodts = msg.Dts() 135 | if s.flvVideoDemuxer != nil { 136 | if err = s.flvVideoDemuxer.Decode(msg.Payload); err != nil { 137 | nazalog.Error(err) 138 | return 139 | } 140 | } 141 | } else { 142 | s.audiodts = msg.Dts() 143 | if s.flvAudioDemuxer != nil { 144 | if err = s.flvAudioDemuxer.Decode(msg.Payload); err != nil { 145 | nazalog.Error(err) 146 | return 147 | } 148 | } 149 | } 150 | } 151 | 152 | func (s *Subscriber) OnStop() { 153 | nazalog.Info("srt subscriber onStop") 154 | s.conn.Close() 155 | } 156 | -------------------------------------------------------------------------------- /thirdparty/srt-1.5.1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/q191201771/lalmax/735eb5bd13388e731a73dad500ac366c87f29de6/thirdparty/srt-1.5.1.tar.gz -------------------------------------------------------------------------------- /version/README.md: -------------------------------------------------------------------------------- 1 | 这个目录用于存放lalmax版本信息说明 2 | 3 | 版本格式 4 | 5 | v0.x1.x2 6 | 7 | 说明如下 8 | 9 | x1为大版本,例如一个大的功能发布或者常规迭代 10 | 11 | x2为小版本,例如小问题修复 12 | -------------------------------------------------------------------------------- /version/v0.1.0.md: -------------------------------------------------------------------------------- 1 | lalmax v0.1.0版本说明 2 | 3 | # 功能点 4 | 5 | (1) 支持SRT推拉流(暂不支持加密) 6 | 7 | [SRT相关说明](../document/srt.md) 8 | 9 | srt支持以后可以使用srt推流到lalmax,然后使用rtsp/hls/rtmp/http-flv/srt等协议进行拉流,也可以使用rtmp/rtsp推流到lalmax中,使用srt进行拉流 10 | 11 | ## SRT url格式 12 | 13 | 推流url 14 | srt://127.0.0.1:6001?streamid=#!::r=test110,m=publish 15 | 16 | 拉流url 17 | srt://127.0.0.1:6001?streamid=#!::r=test110,m=request 18 | 19 | -------------------------------------------------------------------------------- /version/v0.2.0.md: -------------------------------------------------------------------------------- 1 | lalmax v0.2.0版本说明 2 | 3 | [RTC相关说明](../document/rtc.md) 4 | 5 | # 功能点 6 | (1)支持WHIP推流和WHEP拉流,可以对接[OBS](https://github.com/obsproject/obs-studio/actions/runs/5227109208?pr=7926)、[vue-wish](https://github.com/zllovesuki/vue-wish) 7 | 8 | 视频:h264 9 | 10 | 音频:G711A/G711U 11 | 12 | # RTC url格式 13 | 推流url 14 | http(s)://127.0.0.1:1290/whip?streamid=test110 15 | 16 | 拉流url 17 | http(s)://127.0.0.1:1290/whep?streamid=test110 18 | --------------------------------------------------------------------------------