Showing preview only (638K chars total). Download the full file or copy to clipboard to get everything.
Repository: q191201771/lalmax
Branch: master
Commit: ffdfe24e1a84
Files: 115
Total size: 576.3 KB
Directory structure:
gitextract_sbsxbvrx/
├── .github/
│ └── workflows/
│ ├── go.yml
│ └── release.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── build.sh
├── conf/
│ ├── cert.pem
│ ├── key.pem
│ └── lalmax.conf.json
├── config/
│ ├── config.go
│ └── config_test.go
├── document/
│ ├── api.md
│ ├── api_gateway.md
│ ├── config.md
│ ├── gb28181.md
│ ├── hook_api.md
│ ├── hook_plugin_architecture.md
│ ├── lal_api.md
│ ├── lal_config.md
│ ├── rtc.md
│ ├── srt.md
│ └── stream_url.md
├── fmp4/
│ ├── hls/
│ │ ├── server.go
│ │ └── session.go
│ ├── http-fmp4/
│ │ ├── server.go
│ │ └── session.go
│ └── muxer/
│ ├── codec.go
│ ├── file_writer.go
│ ├── flac_box.go
│ ├── init.go
│ ├── init_track.go
│ ├── mp4_writer.go
│ ├── muxer.go
│ ├── muxer_part.go
│ ├── part.go
│ ├── part_sample.go
│ ├── part_track.go
│ ├── rtmp2fmp4.go
│ ├── seekablebuffer.go
│ ├── track.go
│ └── var.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
│ ├── rtppub/
│ │ ├── manager.go
│ │ └── manager_test.go
│ ├── rtppush/
│ │ ├── lower_push_session.go
│ │ └── lower_push_session_test.go
│ ├── server.go
│ ├── t_http_api.go
│ ├── util.go
│ └── xml.go
├── go.mod
├── go.sum
├── logic/
│ ├── gop_cache.go
│ ├── group.go
│ ├── group_manager.go
│ ├── group_test.go
│ ├── stat_aggregator.go
│ ├── stream_key.go
│ └── subscriber_stat.go
├── main.go
├── rtc/
│ ├── jessibucasession.go
│ ├── packer.go
│ ├── peerConnection.go
│ ├── server.go
│ ├── subscriber_stat.go
│ ├── unpacker.go
│ ├── whepsession.go
│ └── whipsession.go
├── run.sh
├── server/
│ ├── hook_builtin_http_plugin.go
│ ├── hook_filter.go
│ ├── hook_plugin.go
│ ├── http_notify.go
│ ├── middle.go
│ ├── router.go
│ ├── router_ctrl.go
│ ├── router_flv_proxy.go
│ ├── router_fmp4.go
│ ├── router_helper.go
│ ├── router_hook.go
│ ├── router_rtc.go
│ ├── router_stat.go
│ ├── router_test.go
│ ├── router_zlm_compat.go
│ ├── server.go
│ ├── stat_view.go
│ ├── zlm_compat_config.go
│ ├── zlm_compat_ffmpeg.go
│ ├── zlm_compat_test.go
│ └── zlm_compat_types.go
├── srt/
│ ├── pub.go
│ ├── server.go
│ ├── stream_id.go
│ └── sub.go
├── utils/
│ └── adjustdts.go
└── version/
├── README.md
├── v0.1.0.md
└── v0.2.0.md
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/workflows/go.yml
================================================
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go
name: Go
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'
- name: Build for Linux, macOS, and Windows
run: |
GOOS=linux go build -o lalmax-linux main.go
GOOS=darwin go build -o lalmax-macos main.go
GOOS=windows go build -o lalmax-windows.exe main.go
================================================
FILE: .github/workflows/release.yml
================================================
# https://github.com/wangyoucao577/go-release-action
name: build-go-binary
on:
release:
types: [created] # 表示在创建新的 Release 时触发
permissions:
contents: write
packages: write
jobs:
build-go-binary:
runs-on: ubuntu-latest
strategy:
matrix:
goos: [linux, windows, darwin] # 需要打包的系统
goarch: [amd64, arm64] # 需要打包的架构
steps:
- uses: actions/checkout@v4
- uses: wangyoucao577/go-release-action@v1.49
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: 1.22
md5sum: false
extra_files: ./README.md ./conf
================================================
FILE: .gitignore
================================================
.codex-cache/
server/logs/
================================================
FILE: Dockerfile
================================================
FROM golang:1.23.0
ENV GOPROXY=https://goproxy.cn,https://goproxy.io,direct
LABEL maintainer="Kevin Zang"
WORKDIR /code
COPY . .
RUN /bin/bash ./build.sh
EXPOSE 1935 8080 4433 5544 8083 8084 1290 30000-30100/udp 6001/udp 4888/udp
CMD export LD_LIBRARY_PATH=/usr/local/lib/ && ./run.sh
================================================
FILE: LICENSE
================================================
The MIT License (MIT)
Copyright (c) 2023 Chef
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
================================================
FILE: README.md
================================================
# lalmax
lalmax是在lal的基础上集成第三方库,可以提供SRT、RTC、mp4、gb28181、onvif等解决方案
# 编译
./build.sh
# 运行
./run.sh或者./lalmax -c conf/lalmax.conf.json
# 配置说明
lalmax.conf.json 配置主要由 2 部分组成
(1) lalmax: lalmax 扩展能力配置,例如 SRT、RTC、HTTP-FMP4、GB28181 等,具体配置说明见[config.md](./document/config.md)
(2) lal: lal 原生配置,例如 RTMP、RTSP、HTTP-FLV、HLS-TS、录制、鉴权等,具体配置说明见[lal_config.md](./document/lal_config.md)。对外建议统一使用 lalmax 的 API Gateway、HTTP API 和 Hook API 门面;`lal.http_api` 仅建议在调试 lal 原生行为时临时开启,说明见[api_gateway.md](./document/api_gateway.md)、[lal_api.md](./document/lal_api.md)、[hook_api.md](./document/hook_api.md) 与 [hook_plugin_architecture.md](./document/hook_plugin_architecture.md)
旧版平铺配置和 lal_config_path 仍兼容,但推荐使用 lalmax/lal 两个顶层标签维护单个配置文件。
# docker运行
```
docker build -t lalmax:init ./
docker run -it -p 1935:1935 -p 8080:8080 -p 4433:4433 -p 5544:5544 -p 8084:8084 -p 30000-30100:30000-30100/udp -p 1290:1290 -p 6001:6001/udp lalmax:init
```
# 架构

# 支持的协议
## 推流
(1) RTSP
(2) SRT
(3) RTMP
(4) RTC(WHIP)
(5) GB28181
具体的推流 URL 地址见[流地址说明](./document/stream_url.md)
## 拉流
(1) RTSP
(2) SRT
(3) RTMP
(4) HLS(S)-TS
(5) HTTP(S)-FLV
(6) HTTP(S)-TS
(7) RTC(WHEP)
(8) HTTP(S)-FMP4
(9) HLS(S)-FMP4/LLHLS
具体的拉流 URL 地址见[流地址说明](./document/stream_url.md)
## [SRT](./document/srt.md)
(1)使用gosrt库
(2)暂时不支持SRT加密
(3)支持H264/H265/AAC
(4)可以对接OBS/VLC
```
推流url
srt://127.0.0.1:6001?streamid=#!::h=test110,m=publish
拉流url
srt://127.0.0.1:6001?streamid=#!::h=test110,m=request
```
## [WebRTC](./document/rtc.md)
(1)支持WHIP推流和WHEP拉流,暂时只支持POST信令
(2)支持H264/G711A/G711U/OPUS
(3)可以对接OBS、vue-wish
(4)WHEP支持对接Safari HEVC
(5)支持datachannel,只支持对接jessibuca播放器
(6)WHIP支持对接OBS 30.2 beta HEVC
datachannel播放地址:webrtc://127.0.0.1:1290/webrtc/play/live/test110
```
WHIP推流url
http(s)://127.0.0.1:1290/webrtc/whip?streamid=test110
WHEP拉流url
http(s)://127.0.0.1:1290/webrtc/whep?streamid=test110
```
## Http-fmp4
(1) 支持H264/H265/AAC/G711A/G711U
```
拉流url
http(s)://127.0.0.1:1290/live/m4s/test110.mp4
```
## HLS(fmp4/Low Latency)
(1) 支持H264/H265/AAC/OPUS
```
拉流url
http(s)://127.0.0.1:1290/live/hls/test110/index.m3u8
```
## [GB28181](./document/gb28181.md)
(1) 作为SIP服务器与设备进行SIP交互,使用单端口/多端口收流
(2) 提供相关API获取设备信息、播放某通道等功能
(3) 支持H264/H265/AAC/G711A/G711U
(4) 支持TCP/UDP
# QQ交流群
11818248
================================================
FILE: build.sh
================================================
#!/usr/bin/env bash
echo "build lalmax"
go build -o lalmax main.go
================================================
FILE: conf/cert.pem
================================================
-----BEGIN CERTIFICATE-----
MIICpDCCAYwCCQDWutSYrD7joDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
b2NhbGhvc3QwHhcNMjAwOTA3MTAxNzI2WhcNMzAwOTA1MTAxNzI2WjAUMRIwEAYD
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDh
dywjAnWKbaQCBtNA9mbvqVsM2S7MStaw9JkcZ45L/MtKxUoP/1egqdADtBPeZxZl
XPxx+vcox9uO2nPZ+OyjedzMtgddK8ix0u2QPdOoc8+HW0fYGKO+YOXKUXUpawIg
ZUhkUZAgrvlZUIewlZ9T0zMAsN3PUuZtg8ux+V3fY28l2QuulC5Q68i8m5vPVwj8
QRitxtKj66fE7Ut5xIc9XAuFJcvYVFSoZuB3/xNbyVev1e2bAe4kYq/+Jt2CTZf4
y7ESQsJn1Ybj0ippOFp9ZJq53roqEF9L0jUKNxNnJHhNc6niUfYehfvAvqXq/QiU
B2qXpZonaq0AICMYFb+nAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAG98/mR8poSA
MepIK6CfZsBzbDkl01R4+dw8RY9SwnOgFE5ddTh955UQvK1ORlVrTgBGkpH3djQd
1I4ADvrYIYHekgoRQX+fFNGyviEftzUUV4aq04JTttrrWrilgoF356pkkID0xSsq
9dr7at36wzV/Sbt9DrW8S9iBX6aUk3PPDxHJhi6xl+bYE6lepVlQ27FZjONo6cpg
MnoRnsOhi/T+VHDUOaR+Xl4I77hESq6ipnV0GJfAAJ7R0hjfBmRbdIxo88iHN8vf
iBHq2THAF+mzApI85ASltPNF+i8ZX0Fn9CvJFwci5eiaoEjjXMbon5X6/n5ovnW/
SaAkF3AobD0=
-----END CERTIFICATE-----
================================================
FILE: conf/key.pem
================================================
-----BEGIN RSA PRIVATE KEY-----
MIIEogIBAAKCAQEA4XcsIwJ1im2kAgbTQPZm76lbDNkuzErWsPSZHGeOS/zLSsVK
D/9XoKnQA7QT3mcWZVz8cfr3KMfbjtpz2fjso3nczLYHXSvIsdLtkD3TqHPPh1tH
2BijvmDlylF1KWsCIGVIZFGQIK75WVCHsJWfU9MzALDdz1LmbYPLsfld32NvJdkL
rpQuUOvIvJubz1cI/EEYrcbSo+unxO1LecSHPVwLhSXL2FRUqGbgd/8TW8lXr9Xt
mwHuJGKv/ibdgk2X+MuxEkLCZ9WG49IqaThafWSaud66KhBfS9I1CjcTZyR4TXOp
4lH2HoX7wL6l6v0IlAdql6WaJ2qtACAjGBW/pwIDAQABAoIBADTClml64daK4Z43
yqehAWWD0/Klv/W+bY7rLgkfkoTlmwzcLgCgV/kYw7yaHywkI3GE2O4zNDMu0YoU
RJf1UCrREYI19nMvE7/JBB6E2UrKDv41thIzcd3S/vLhLPGMQOsjyFTxYTDEwUTN
O3NvD+GlwoGe4cjqNVHbTYdQO09SnN7lJOIhetJ5MQWvH5kiOE+xh9AxEpYOI2K9
RMKKaUOBQjm0Q0ezMSZ1gfwDh7i642uDvVpeQ8uDg+1BZ/ulAJpaL6pE4KTUfCAR
ygyQajvj2c93x/876ueacRBRECkvwoliOuQE8SmbjqQ5oDikHD8BNgbIwb/snZbt
x3bj6kECgYEA+k5c9ZpqSkTtUK+lCVDV4zfSHOn//6J9pFr/f2o44nTsVUuO7Oqi
jxikbx/BjEc6veRnLu2aqqMHuw7wV6EnS7Ikbc0lRNcXsBgN0WYbT26CLmAx0G1t
9DFyPdGT7/LZzNa11YqK/NuL2EFQ06SC7JA23+yfI5EJjOR0V6npU7sCgYEA5pgm
9PjyyfCvOF6wPbDRyGO359aXXtavKGPomOrV1FvTGt282NpbOOoOa7XlD3013A7i
pRGiDYZO0FS/2ajYXz/uAaBh0LvKPhTZaXcezJ9GfA2HqeuTbQwhUsj17TLbgC1Y
7xSbwMwt8uRh3KdbLf220U/WHLPJMjXsME9nBwUCgYAnyCyeHFyoUSwmlsP0JxTX
eBe84LP/PSQa6xuQdKF13H9zTv74SJJti80WnEV2thtv8s0zeDAMzrx7znQEeWh1
b2q6yNATkNwC8M/BaCkPBtFJ7Z/9MGc5WGJ/0L9ic4aKN9XOiqZsabhgNoFSIeNt
Fb6i+EiSroqGCgkzpZ2f4QKBgHEqrLu+zVBjyWpNtgqgk2PX5HJn8yO9EnstBQK/
BS/R3Lmrprl5+BjnbSpZO1Atr9gOihZen/wpNNazMPA+F+ou8rxjnH2XG7r5+nTy
2++qHypUbYbrsQ9sS5JYQ7EkK2stVh8HKyUkT0yL3qcujuX0RNtWZgryBMSaiA5x
eWuNAoGAI/87JrId6LzL9RSJFnXtkbYDNw1Zf7OTtjcXytn5U64sG/DEQNt8m6um
q/JNM32vH8Fz+JcRVVGJlN2bSsxYxpIzhd7SBS7Cq0a6gHFeRsqcTA5qidxbUPLn
itJ84Oz1Zo2U5MG+Zj+2sLm4v6611RkYyOiSEMvvV6ZJ/TGBPDc=
-----END RSA PRIVATE KEY-----
================================================
FILE: conf/lalmax.conf.json
================================================
{
"lalmax": {
"srt_config": {
"enable": true,
"addr": ":6001"
},
"rtc_config": {
"enable": true,
"ice_host_nat_to_ips": [],
"ice_udp_mux_port": 4888,
"ice_tcp_mux_port": 4888
},
"http_config": {
"http_listen_addr": ":1290",
"enable_https": true,
"https_listen_addr": ":1233",
"https_cert_file": "./conf/cert.pem",
"https_key_file": "./conf/key.pem",
"ctrl_auth_whitelist": {
"ips": [],
"secrets": []
}
},
"fmp4_config": {
"http": {
"enable": true
},
"hls": {
"enable": true,
"segment_count": 7,
"segment_duration": 1,
"part_duration": 200,
"low_latency": false
}
},
"logic_config": {
"gop_cache_num": 1,
"single_gop_max_frame_num": 0
},
"server_id": "1",
"http_notify": {
"enable": false,
"update_interval_sec": 5,
"on_update": "http://127.0.0.1:10101/on_update",
"on_group_start": "http://127.0.0.1:10101/on_group_start",
"on_group_stop": "http://127.0.0.1:10101/on_group_stop",
"on_stream_active": "http://127.0.0.1:10101/on_stream_active",
"on_pub_start": "http://127.0.0.1:10101/on_pub_start",
"on_pub_stop": "http://127.0.0.1:10101/on_pub_stop",
"on_sub_start": "http://127.0.0.1:10101/on_sub_start",
"on_sub_stop": "http://127.0.0.1:10101/on_sub_stop",
"on_relay_pull_start": "http://127.0.0.1:10101/on_relay_pull_start",
"on_relay_pull_stop": "http://127.0.0.1:10101/on_relay_pull_stop",
"on_rtmp_connect": "http://127.0.0.1:10101/on_rtmp_connect",
"on_server_start": "http://127.0.0.1:10101/on_server_start",
"on_hls_make_ts": "http://127.0.0.1:10101/on_hls_make_ts"
}
},
"lal": {
"# doc of config": "./document/lal_config.md",
"conf_version": "v0.4.1",
"rtmp": {
"enable": true,
"addr": ":1935",
"rtmps_enable": true,
"rtmps_addr": ":4935",
"rtmps_cert_file": "./conf/cert.pem",
"rtmps_key_file": "./conf/key.pem",
"gop_num": 1,
"single_gop_max_frame_num": 0,
"merge_write_size": 0
},
"in_session": {
"add_dummy_audio_enable": false,
"add_dummy_audio_wait_audio_ms": 150
},
"default_http": {
"http_listen_addr": ":8080",
"https_listen_addr": ":4433",
"https_cert_file": "./conf/cert.pem",
"https_key_file": "./conf/key.pem"
},
"httpflv": {
"enable": true,
"enable_https": true,
"url_pattern": "/",
"gop_num": 0,
"single_gop_max_frame_num": 0
},
"hls": {
"enable": false,
"enable_https": false,
"url_pattern": "/hls/",
"out_path": "./lal_record/hls/",
"fragment_duration_ms": 3000,
"fragment_num": 6,
"delete_threshold": 6,
"cleanup_mode": 1,
"use_memory_as_disk_flag": false,
"sub_session_timeout_ms": 30000,
"sub_session_hash_key": ""
},
"httpts": {
"enable": false,
"enable_https": false,
"url_pattern": "/",
"gop_num": 0,
"single_gop_max_frame_num": 0
},
"rtsp": {
"enable": true,
"addr": ":5544",
"rtsps_enable": true,
"rtsps_addr": ":5322",
"rtsps_cert_file": "./conf/cert.pem",
"rtsps_key_file": "./conf/key.pem",
"out_wait_key_frame_flag": true,
"auth_enable": false,
"auth_method": 1,
"username": "q191201771",
"password": "pengrl"
},
"record": {
"enable_flv": false,
"flv_out_path": "./lal_record/flv/",
"enable_mpegts": false,
"mpegts_out_path": "./lal_record/mpegts"
},
"relay_push": {
"enable": false,
"addr_list": []
},
"static_relay_pull": {
"enable": false,
"addr": ""
},
"http_api": {
"enable": false,
"addr": ":8083"
},
"server_id": "1",
"simple_auth": {
"key": "q191201771",
"dangerous_lal_secret": "pengrl",
"pub_rtmp_enable": false,
"sub_rtmp_enable": false,
"sub_httpflv_enable": false,
"sub_httpts_enable": false,
"pub_rtsp_enable": false,
"sub_rtsp_enable": false,
"hls_m3u8_enable": false
},
"pprof": {
"enable": true,
"addr": ":8084"
},
"log": {
"level": 2,
"filename": "./logs/lalmax.log",
"is_to_stdout": true,
"is_rotate_daily": true,
"short_file_flag": true,
"timestamp_flag": true,
"timestamp_with_ms_flag": true,
"level_flag": true,
"assert_behavior": 1
},
"debug": {
"log_group_interval_sec": 30,
"log_group_max_group_num": 10,
"log_group_max_sub_num_per_group": 10
}
}
}
================================================
FILE: config/config.go
================================================
package config
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
)
var defaultConfig Config
type Config struct {
SrtConfig SrtConfig `json:"srt_config"` // srt配置
RtcConfig RtcConfig `json:"rtc_config"` // rtc配置
HttpConfig HttpConfig `json:"http_config"` // http/https配置
Fmp4Config Fmp4Config `json:"fmp4_config"` // fmp4配置
GB28181Config GB28181Config `json:"gb28181_config"` // gb28181配置
ServerId string `json:"server_id"` // http 通知唯一标识
HttpNotifyConfig HttpNotifyConfig `json:"http_notify"` // http 通知配置
LalSvrConfigPath string `json:"lal_config_path"` // lal配置文件路径,兼容旧版配置
LogicConfig LogicConfig `json:"logic_config"` // 扩展流组配置
LalRawContent []byte `json:"-"` // lal 原始配置内容
ConfFilePath string `json:"-"` // 配置文件路径,用于持久化
}
type SrtConfig struct {
Enable bool `json:"enable"` // srt服务使能配置
Addr string `json:"addr"` // srt服务监听地址
}
type RtcConfig struct {
Enable bool `json:"enable"` // rtc服务使能配置
ICEHostNATToIPs []string `json:"ice_host_nat_to_ips"` // rtc服务公网IP,未设置使用内网
ICEUDPMuxPort int `json:"ice_udp_mux_port"` // rtc udp mux port
ICETCPMuxPort int `json:"ice_tcp_mux_port"` // rtc tcp mux port
WriteChanSize int `json:"write_chan_size"`
}
type HttpConfig struct {
ListenAddr string `json:"http_listen_addr"` // http服务监听地址
EnableHttps bool `json:"enable_https"` // https使能标志
HttpsListenAddr string `json:"https_listen_addr"` // https监听地址
HttpsCertFile string `json:"https_cert_file"` // https cert 文件
HttpsKeyFile string `json:"https_key_file"` // https key 文件
CtrlAuthWhitelist CtrlAuthWhitelist `json:"ctrl_auth_whitelist"`
}
// CtrlAuthWhitelist 控制类接口鉴权。
type CtrlAuthWhitelist struct {
IPs []string // 允许访问的远程 IP,零值时不生效
Secrets []string // 认证信息,零值时不生效
}
type Fmp4Config struct {
Http Fmp4HttpConfig `json:"http"`
Hls Fmp4HlsConfig `json:"hls"`
}
type Fmp4HttpConfig struct {
Enable bool `json:"enable"` // http-fmp4使能标志
}
type Fmp4HlsConfig struct {
Enable bool `json:"enable"` // hls使能标志
SegmentCount int `json:"segment_count"` // 分片个数,llhls默认7个
SegmentDuration int `json:"segment_duration"` // hls分片时长,默认1s
PartDuration int `json:"part_duration"` // llhls part时长,默认200ms
LowLatency bool `json:"low_latency"` // 是否开启llhls
}
type GB28181Config struct {
Enable bool `json:"enable"` // gb28181使能标志
ListenAddr string `json:"listen_addr"` // gb28181监听地址
SipIP string `json:"sip_ip"` // sip 服务器公网IP
SipPort uint16 `json:"sip_port"` // sip 服务器端口,默认 5060
Serial string `json:"serial"` // sip 服务器 id, 默认 34020000002000000001
Realm string `json:"realm"` // sip 服务器域,默认 3402000000
Username string `json:"username"` // sip 服务器账号
Password string `json:"password"` // sip 服务器密码
KeepaliveInterval int `json:"keepalive_interval"` // 心跳包时长
QuickLogin bool `json:"quick_login"` // 快速登陆,有keepalive就认为在线
MediaConfig GB28181MediaConfig `json:"media_config"` // 媒体服务器配置
}
type GB28181MediaConfig struct {
MediaIp string `json:"media_ip"` // 流媒体IP,用于在SDP中指定
ListenPort uint16 `json:"listen_port"` // tcp,udp监听端口 默认启动
MultiPortMaxIncrement uint16 `json:"multi_port_max_increment"` // 多端口范围 ListenPort+1至ListenPort+MultiPortMax
}
// ZlmCompatHookConfig ZLM 兼容 hook URL 配置
// 为什么独立结构体:隔离 ZLM 适配层,lalmax 原有字段保持不变
type ZlmCompatHookConfig struct {
ZlmOnStreamChanged string `json:"zlm_on_stream_changed"`
ZlmOnServerKeepalive string `json:"zlm_on_server_keepalive"`
ZlmOnStreamNoneReader string `json:"zlm_on_stream_none_reader"`
ZlmOnRtpServerTimeout string `json:"zlm_on_rtp_server_timeout"`
ZlmOnRecordMp4 string `json:"zlm_on_record_mp4"`
ZlmOnPublish string `json:"zlm_on_publish"`
ZlmOnPlay string `json:"zlm_on_play"`
ZlmOnStreamNotFound string `json:"zlm_on_stream_not_found"`
ZlmOnServerStarted string `json:"zlm_on_server_started"`
}
// HasZlmHooks 任一 ZLM 兼容 hook 字段有值即返回 true
// 为什么:ZLM 回调与 lalmax 原有回调二选一,此方法为判断条件
func (c ZlmCompatHookConfig) HasZlmHooks() bool {
return c.ZlmOnStreamChanged != "" ||
c.ZlmOnServerKeepalive != "" ||
c.ZlmOnStreamNoneReader != "" ||
c.ZlmOnRtpServerTimeout != "" ||
c.ZlmOnRecordMp4 != "" ||
c.ZlmOnPublish != "" ||
c.ZlmOnPlay != "" ||
c.ZlmOnStreamNotFound != ""
}
type HttpNotifyConfig struct {
Enable bool `json:"enable"`
UpdateIntervalSec int `json:"update_interval_sec"`
KeepaliveIntervalSec int `json:"keepalive_interval_sec"`
HookTimeoutSec int `json:"hook_timeout_sec"`
OnServerStart string `json:"on_server_start"`
OnUpdate string `json:"on_update"`
OnGroupStart string `json:"on_group_start"`
OnGroupStop string `json:"on_group_stop"`
OnStreamActive string `json:"on_stream_active"`
OnPubStart string `json:"on_pub_start"`
OnPubStop string `json:"on_pub_stop"`
OnSubStart string `json:"on_sub_start"`
OnSubStop string `json:"on_sub_stop"`
OnRelayPullStart string `json:"on_relay_pull_start"`
OnRelayPullStop string `json:"on_relay_pull_stop"`
OnRtmpConnect string `json:"on_rtmp_connect"`
OnHlsMakeTs string `json:"on_hls_make_ts"`
// --- ZLM 兼容 hook 配置 ---
ZlmCompatHookConfig
}
type LogicConfig struct {
GopCacheNum int `json:"gop_cache_num"`
SingleGopMaxFrameNum int `json:"single_gop_max_frame_num"`
}
func Open(filepath string) error {
data, err := ioutil.ReadFile(filepath)
if err != nil {
return err
}
err = Unmarshal(data)
if err != nil {
return err
}
return nil
}
func Unmarshal(data []byte) error {
var file struct {
LalMax json.RawMessage `json:"lalmax"`
Lal json.RawMessage `json:"lal"`
}
if err := json.Unmarshal(data, &file); err != nil {
return err
}
var cfg Config
if len(file.LalMax) != 0 {
if err := unmarshalConfig(file.LalMax, &cfg); err != nil {
return err
}
cfg.LalRawContent = append([]byte(nil), file.Lal...)
} else {
if err := unmarshalConfig(data, &cfg); err != nil {
return err
}
}
defaultConfig = cfg
return nil
}
func unmarshalConfig(data []byte, cfg *Config) error {
if err := json.Unmarshal(data, cfg); err != nil {
return err
}
var raw map[string]json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
if cfg.LalSvrConfigPath == "" {
var legacy struct {
LalSvrConfigPath string `json:"lal_config_path:"`
}
if err := json.Unmarshal(data, &legacy); err != nil {
return err
}
cfg.LalSvrConfigPath = legacy.LalSvrConfigPath
}
if _, ok := raw["logic_config"]; !ok {
var legacy struct {
LogicConfig LogicConfig `json:"hook_config"`
}
if err := json.Unmarshal(data, &legacy); err != nil {
return err
}
cfg.LogicConfig = legacy.LogicConfig
}
if _, ok := raw["fmp4_config"]; !ok {
var legacy struct {
Http Fmp4HttpConfig `json:"httpfmp4_config"`
Hls Fmp4HlsConfig `json:"hls_config"`
}
if err := json.Unmarshal(data, &legacy); err != nil {
return err
}
cfg.Fmp4Config.Http = legacy.Http
cfg.Fmp4Config.Hls = legacy.Hls
}
return nil
}
func GetConfig() *Config {
return &defaultConfig
}
// SaveToFile 将当前配置持久化到配置文件
// 为什么:setServerConfig 动态修改后需落盘,重启后配置仍生效
func (c *Config) SaveToFile() error {
if c.ConfFilePath == "" {
return nil
}
data, err := os.ReadFile(c.ConfFilePath)
if err != nil {
return fmt.Errorf("read config file: %w", err)
}
var file map[string]json.RawMessage
if err := json.Unmarshal(data, &file); err != nil {
return fmt.Errorf("parse config file: %w", err)
}
lalmax, err := json.MarshalIndent(c, "", " ")
if err != nil {
return fmt.Errorf("marshal lalmax config: %w", err)
}
file["lalmax"] = lalmax
out, err := json.MarshalIndent(file, "", " ")
if err != nil {
return fmt.Errorf("marshal config file: %w", err)
}
out = append(out, '\n')
return os.WriteFile(c.ConfFilePath, out, 0o644)
}
================================================
FILE: config/config_test.go
================================================
package config
import (
"strings"
"testing"
)
func TestUnmarshalStructuredConfig(t *testing.T) {
raw := []byte(`{
"lalmax": {
"srt_config": {
"enable": true,
"addr": ":6001"
},
"server_id": "lalmax-1"
},
"lal": {
"rtmp": {
"enable": true,
"addr": ":1935"
}
}
}`)
if err := Unmarshal(raw); err != nil {
t.Fatalf("unmarshal structured config: %v", err)
}
cfg := GetConfig()
if !cfg.SrtConfig.Enable || cfg.SrtConfig.Addr != ":6001" {
t.Fatalf("unexpected srt config: %+v", cfg.SrtConfig)
}
if cfg.ServerId != "lalmax-1" {
t.Fatalf("unexpected server id: %s", cfg.ServerId)
}
if !strings.Contains(string(cfg.LalRawContent), `"rtmp"`) {
t.Fatalf("lal raw content not preserved: %s", string(cfg.LalRawContent))
}
}
func TestUnmarshalLegacyConfig(t *testing.T) {
raw := []byte(`{
"srt_config": {
"enable": true,
"addr": ":6001"
},
"httpfmp4_config": {
"enable": true
},
"hls_config": {
"enable": true,
"segment_count": 3,
"segment_duration": 2,
"part_duration": 100,
"low_latency": true
},
"hook_config": {
"gop_cache_num": 3,
"single_gop_max_frame_num": 120
},
"lal_config_path:": "./conf/lalserver.conf.json"
}`)
if err := Unmarshal(raw); err != nil {
t.Fatalf("unmarshal legacy config: %v", err)
}
cfg := GetConfig()
if !cfg.SrtConfig.Enable || cfg.SrtConfig.Addr != ":6001" {
t.Fatalf("unexpected srt config: %+v", cfg.SrtConfig)
}
if cfg.LalSvrConfigPath != "./conf/lalserver.conf.json" {
t.Fatalf("unexpected lal config path: %s", cfg.LalSvrConfigPath)
}
if cfg.LogicConfig.GopCacheNum != 3 || cfg.LogicConfig.SingleGopMaxFrameNum != 120 {
t.Fatalf("unexpected legacy logic config: %+v", cfg.LogicConfig)
}
if !cfg.Fmp4Config.Http.Enable {
t.Fatalf("unexpected legacy fmp4 http config: %+v", cfg.Fmp4Config.Http)
}
if !cfg.Fmp4Config.Hls.Enable || cfg.Fmp4Config.Hls.SegmentCount != 3 || cfg.Fmp4Config.Hls.SegmentDuration != 2 || cfg.Fmp4Config.Hls.PartDuration != 100 || !cfg.Fmp4Config.Hls.LowLatency {
t.Fatalf("unexpected legacy fmp4 hls config: %+v", cfg.Fmp4Config.Hls)
}
if len(cfg.LalRawContent) != 0 {
t.Fatalf("legacy config should not set lal raw content: %s", string(cfg.LalRawContent))
}
}
func TestUnmarshalStructuredFmp4ConfigKeepsExplicitZero(t *testing.T) {
raw := []byte(`{
"lalmax": {
"fmp4_config": {
"http": {
"enable": false
},
"hls": {
"enable": true,
"segment_count": 0,
"segment_duration": 0,
"part_duration": 0,
"low_latency": false
}
},
"httpfmp4_config": {
"enable": true
},
"hls_config": {
"enable": true,
"segment_count": 3,
"segment_duration": 2,
"part_duration": 100,
"low_latency": true
}
}
}`)
if err := Unmarshal(raw); err != nil {
t.Fatalf("unmarshal structured config: %v", err)
}
cfg := GetConfig()
if cfg.Fmp4Config.Http.Enable {
t.Fatalf("explicit fmp4 http config should not be overwritten: %+v", cfg.Fmp4Config.Http)
}
if !cfg.Fmp4Config.Hls.Enable || cfg.Fmp4Config.Hls.SegmentCount != 0 || cfg.Fmp4Config.Hls.SegmentDuration != 0 || cfg.Fmp4Config.Hls.PartDuration != 0 || cfg.Fmp4Config.Hls.LowLatency {
t.Fatalf("explicit fmp4 hls config should not be overwritten: %+v", cfg.Fmp4Config.Hls)
}
}
func TestUnmarshalStructuredLogicConfigKeepsExplicitZero(t *testing.T) {
raw := []byte(`{
"lalmax": {
"logic_config": {
"gop_cache_num": 0,
"single_gop_max_frame_num": 0
},
"hook_config": {
"gop_cache_num": 3,
"single_gop_max_frame_num": 120
}
}
}`)
if err := Unmarshal(raw); err != nil {
t.Fatalf("unmarshal structured config: %v", err)
}
cfg := GetConfig()
if cfg.LogicConfig.GopCacheNum != 0 || cfg.LogicConfig.SingleGopMaxFrameNum != 0 {
t.Fatalf("explicit logic config should not be overwritten: %+v", cfg.LogicConfig)
}
}
================================================
FILE: document/api.md
================================================
# HTTP API 总览
`lalmax` 对外建议统一只暴露一个 HTTP 管理入口,也就是 `lalmax.http_config.http_listen_addr`,示例配置通常是 `:1290`。
`lal.http_api` 仍然可以保留给排查 `lal` 原生行为时使用,但默认建议关闭,不要和 `lalmax` 的管理入口混用。
建议配合以下文档一起看:
- [api_gateway.md](./api_gateway.md):统一入口和状态聚合说明
- [hook_api.md](./hook_api.md):Hook 查询与订阅接口
- [hook_plugin_architecture.md](./hook_plugin_architecture.md):HookHub 与插件化架构
- [lal_api.md](./lal_api.md):`lal` 原生 HTTP API 的定位和兼容关系
## 基本约定
- 对外统一入口默认是 `http://127.0.0.1:1290`
- `/api/stat/*`、`/api/ctrl/*`、`/api/hook/*` 共用同一套鉴权配置:`lalmax.http_config.ctrl_auth_whitelist`
- 如果鉴权失败,HTTP 状态码仍然是 `200`,返回体里的 `error_code` 为 `401`
- 除 `GET /api/hook/stream` 之外,其余接口都返回 JSON
统一返回结构如下:
```json
{
"error_code": 0,
"desp": "succ",
"data": {}
}
```
常见 `error_code` 如下:
| error_code | desp | 说明 |
| --- | --- | --- |
| 0 | succ | 调用成功 |
| 401 | Unauthorized | 鉴权失败 |
| 1001 | group not found | 流分组不存在,或者仅传 `stream_name` 时无法唯一定位 |
| 1002 | param missing | 必填参数缺失 |
| 1003 | session not found | 会话不存在 |
| 2001 | 具体错误信息见 `desp` | `start_relay_pull` 执行失败 |
| 2002 | 具体错误信息见 `desp` | `start_rtp_pub` 执行失败 |
## 当前接口列表
### 统计接口
- `GET /api/stat/group`
- `GET /api/stat/all_group`
- `GET /api/stat/lal_info`
### 控制接口
- `POST /api/ctrl/start_relay_pull`
- `GET /api/ctrl/stop_relay_pull`
- `POST /api/ctrl/stop_relay_pull`
- `POST /api/ctrl/kick_session`
- `POST /api/ctrl/start_rtp_pub`
- `POST /api/ctrl/stop_rtp_pub`
### Hook 接口
- `GET /api/hook/recent`
- `GET /api/hook/stream`
## 统计接口
### `GET /api/stat/group`
查询单个流分组的当前状态。
请求参数:
- `stream_name`:必填,流名
- `app_name`:可选,应用名
说明:
- 如果同一个 `stream_name` 只对应一个流分组,可以只传 `stream_name`
- 如果同一个 `stream_name` 在多个 `app_name` 下都存在,建议同时传 `app_name + stream_name`
- 当前返回结果会在兼容 `lal` 原生字段的基础上,额外补充 `lalmax.ext_subs`
请求示例:
```bash
curl "http://127.0.0.1:1290/api/stat/group?stream_name=test110"
curl "http://127.0.0.1:1290/api/stat/group?app_name=live&stream_name=test110"
```
返回示例:
```json
{
"error_code": 0,
"desp": "succ",
"data": {
"stream_name": "test110",
"app_name": "live",
"audio_codec": "AAC",
"video_codec": "H264",
"video_width": 1920,
"video_height": 1080,
"pub": {
"session_id": "RTMPPUB1",
"protocol": "RTMP",
"base_type": "PUB"
},
"subs": [
{
"session_id": "RTMPSUB1",
"protocol": "RTMP",
"base_type": "SUB"
},
{
"session_id": "whep-123",
"protocol": "WHEP",
"base_type": "SUB"
}
],
"pull": {
"base_type": "PULL"
},
"in_frame_per_sec": [],
"lalmax": {
"ext_subs": [
{
"session_id": "whep-123",
"protocol": "WHEP",
"base_type": "SUB"
}
]
}
}
}
```
字段语义:
- `subs`:统一后的订阅者视图,包含 `lal` 原生订阅者和 `lalmax` 扩展订阅者
- `lalmax.ext_subs`:只包含 `lalmax` 扩展层维护的订阅者,便于业务侧区分来源
### `GET /api/stat/all_group`
查询当前所有流分组。
请求示例:
```bash
curl "http://127.0.0.1:1290/api/stat/all_group"
```
返回示例:
```json
{
"error_code": 0,
"desp": "succ",
"data": {
"groups": [
{
"stream_name": "test110",
"app_name": "live",
"lalmax": {
"ext_subs": []
}
}
]
}
}
```
其中 `groups[*]` 的结构和 `/api/stat/group` 的 `data` 完全一致。
### `GET /api/stat/lal_info`
查询服务基础信息。
请求示例:
```bash
curl "http://127.0.0.1:1290/api/stat/lal_info"
```
该接口直接返回内嵌 `lal` 的运行信息,常见字段包括:
- `server_id`
- `bin_info`
- `lal_version`
- `api_version`
- `notify_version`
- `start_time`
## 控制接口
### `POST /api/ctrl/start_relay_pull`
让服务主动去远端拉流。
请求体为 JSON,必填字段:
- `url`
常用可选字段:
- `stream_name`
- `pull_timeout_ms`
- `pull_retry_num`
- `auto_stop_pull_after_no_out_ms`
- `rtsp_mode`
- `debug_dump_packet`
当前程序里的默认值如下:
- `pull_timeout_ms` 默认 `10000`
- `pull_retry_num` 默认 `0`
- `auto_stop_pull_after_no_out_ms` 默认 `-1`
- `rtsp_mode` 默认 `0`,也就是 TCP
请求示例:
```bash
curl -H "Content-Type: application/json" \
-X POST \
-d "{\"url\":\"rtmp://127.0.0.1/live/test110\"}" \
"http://127.0.0.1:1290/api/ctrl/start_relay_pull"
```
说明:
- 接口返回成功,只代表命令已经被接受
- 是否真的拉流成功,需要看后续状态接口,或者看 Hook 事件中的 `on_relay_pull_start`、`on_update`
### `GET /api/ctrl/stop_relay_pull`
### `POST /api/ctrl/stop_relay_pull`
关闭指定的 relay pull 会话。
当前实现里,这个接口无论用 `GET` 还是 `POST`,都从查询参数里读取:
- `stream_name`:必填
请求示例:
```bash
curl "http://127.0.0.1:1290/api/ctrl/stop_relay_pull?stream_name=test110"
curl -X POST "http://127.0.0.1:1290/api/ctrl/stop_relay_pull?stream_name=test110"
```
说明:
- 也可以用 `kick_session` 关闭 pull 会话
### `POST /api/ctrl/kick_session`
强制关闭指定会话。
请求体为 JSON,必填字段:
- `stream_name`
- `session_id`
请求示例:
```bash
curl -H "Content-Type: application/json" \
-X POST \
-d "{\"stream_name\":\"test110\",\"session_id\":\"FLVSUB1\"}" \
"http://127.0.0.1:1290/api/ctrl/kick_session"
```
适用对象:
- 推流会话
- 拉流会话
- relay pull 会话
### `POST /api/ctrl/start_rtp_pub`
打开一个 GB28181/RTP 接收会话。
请求体为 JSON,必填字段:
- `stream_name`
常用可选字段:
- `port`
- `timeout_ms`
- `is_tcp_flag`
- `debug_dump_packet`
当前程序里的默认值:
- `timeout_ms` 默认 `60000`
请求示例:
```bash
curl -H "Content-Type: application/json" \
-X POST \
-d "{\"stream_name\":\"gb28181-test\",\"port\":0}" \
"http://127.0.0.1:1290/api/ctrl/start_rtp_pub"
```
说明:
- `port=0` 表示由服务自动分配端口
- 成功后会返回 `session_id` 和最终监听端口
### `POST /api/ctrl/stop_rtp_pub`
关闭 GB28181/RTP 接收会话。
当前实现支持两种传参方式:
- 查询参数:`stream_name` 或 `session_id`
- JSON 请求体:`stream_name` 或 `session_id`
两者至少传一个。
请求示例:
```bash
curl -X POST "http://127.0.0.1:1290/api/ctrl/stop_rtp_pub?stream_name=gb28181-test"
curl -H "Content-Type: application/json" \
-X POST \
-d "{\"session_id\":\"PSSUB1\"}" \
"http://127.0.0.1:1290/api/ctrl/stop_rtp_pub"
```
成功后会返回被关闭的 `session_id`。
## Hook 接口
### `GET /api/hook/recent`
读取最近的 Hook 事件快照。
常用查询参数:
- `limit`:返回条数,默认 `20`
- `app_name`
- `stream_name`
- `session_id`
- `event`
- `events`:多个事件名,逗号分隔
请求示例:
```bash
curl "http://127.0.0.1:1290/api/hook/recent?limit=5"
curl "http://127.0.0.1:1290/api/hook/recent?stream_name=test110&events=on_group_start,on_stream_active,on_group_stop,on_update"
```
### `GET /api/hook/stream`
使用 SSE 持续订阅 Hook 事件。
它和 `/api/hook/recent` 使用同一套过滤参数。连接建立后,会先回放最近一批命中的事件,然后继续推送实时事件。
请求示例:
```bash
curl -N "http://127.0.0.1:1290/api/hook/stream"
curl -N "http://127.0.0.1:1290/api/hook/stream?stream_name=test110&events=on_update,on_group_stop"
```
返回格式示例:
```text
id: 12
event: on_pub_start
data: {"server_id":"1","session_id":"RTMPPUB1","protocol":"RTMP","base_type":"PUB","stream_name":"test110"}
```
当前 Hook 体系里的事件名称、语义、过滤规则和插件化接入方式,请直接参考 [hook_api.md](./hook_api.md) 和 [hook_plugin_architecture.md](./hook_plugin_architecture.md)。
================================================
FILE: document/api_gateway.md
================================================
# API Gateway
`lalmax` 作为 `lal` 的统一 API 网关,对外建议只暴露一个 HTTP 入口。
Hook 体系的详细设计见 [hook_plugin_architecture.md](./hook_plugin_architecture.md)。
默认入口来自:
```json
{
"lalmax": {
"http_config": {
"http_listen_addr": ":1290"
}
}
}
```
## Exposed Routes
### Stat
- `GET /api/stat/group`
- `GET /api/stat/all_group`
- `GET /api/stat/lal_info`
### Control
- `POST /api/ctrl/start_relay_pull`
- `GET /api/ctrl/stop_relay_pull`
- `POST /api/ctrl/stop_relay_pull`
- `POST /api/ctrl/kick_session`
- `POST /api/ctrl/start_rtp_pub`
- `POST /api/ctrl/stop_rtp_pub`
### Hook
- `GET /api/hook/recent`
- `GET /api/hook/stream`
## Why Use lalmax Gateway
- `lal` 原生流状态仍由 `lal` 负责,避免双事实源
- `lalmax` 在响应中补充扩展协议订阅者统计
- hook 事件统一从 `lalmax` 读取,不必同时维护 HTTP notify 和内部状态
- 控制接口、查询接口、hook 接口共用一套鉴权策略
## Group Visibility
`lalmax` 获取 group 视图的方式是:
1. 通过内嵌 `lal` 的 `StatAllGroup()` 获取原生 group 快照
2. 通过 `lalmax/logic` 获取扩展订阅者状态
3. 聚合成统一视图后再对外返回或分发到 hook hub
这样 `lalmax` 可以知道 `lal group` 中所有流的原生状态,同时保留自己的扩展消费层状态。
## Stat Response Extension
`/api/stat/group` 和 `/api/stat/all_group` 在保持 `lal` 原有字段的同时,会额外返回一个 `lalmax` 扩展块。
兼容原则如下:
- 原有 `stream_name`、`app_name`、`pub`、`subs`、`pull`、`in_frame_per_sec` 等字段继续保留
- `subs` 仍然表示统一后的订阅者视图,其中会合并 `lal` 原生订阅者和 `lalmax` 扩展订阅者
- `lalmax.ext_subs` 只列出来自 `lalmax` 扩展层的订阅者,便于业务侧区分来源
示例:
```json
{
"error_code": 0,
"desp": "succ",
"data": {
"stream_name": "camera01",
"app_name": "live",
"pub": {},
"subs": [
{
"session_id": "RTMPSUB1",
"protocol": "RTMP"
},
{
"session_id": "whep-123",
"protocol": "WHEP"
}
],
"pull": {},
"in_frame_per_sec": [],
"lalmax": {
"ext_subs": [
{
"session_id": "whep-123",
"protocol": "WHEP"
}
]
}
}
}
```
如果业务只想拿 `lal` 原生兼容视图,可以继续只读原字段;如果业务需要知道 `lalmax` 在该流上维护了哪些扩展订阅者,则读取 `lalmax.ext_subs`。
## Control API Scope
`/api/ctrl/*` 仍然保持轻量控制接口定位,不会在响应中额外塞入完整流状态、订阅者列表或 `lalmax` 扩展统计。
原因是:
- 控制接口的职责是执行动作并返回动作结果
- 流状态属于查询语义,应统一从 `/api/stat/*` 获取
- 避免控制响应膨胀,降低兼容性和调用方解析成本
================================================
FILE: document/config.md
================================================
# lalmax 配置说明
本文档说明 `conf/lalmax.conf.json` 里 `lalmax` 这一段的配置。
`lal` 原生配置请看 [lal_config.md](./lal_config.md)。
## 推荐配置结构
当前程序推荐使用一个统一的配置文件,并按顶层标签拆开:
```json
{
"lalmax": {
"server_id": "1",
"srt_config": {},
"rtc_config": {},
"http_config": {},
"fmp4_config": {},
"logic_config": {},
"http_notify": {},
"gb28181_config": {}
},
"lal": {}
}
```
说明:
- `lalmax`:`lalmax` 自己的扩展能力配置
- `lal`:内嵌 `lal` 的原生配置
如果同时提供了顶层 `lal` 标签,程序会优先使用这段内容作为 `lal` 的原生配置,不再读取 `lal_config_path` 指向的文件。
## srt_config
SRT 服务配置。
- `enable`:是否启用 SRT
- `addr`:SRT 监听地址,示例 `:6001`
示例:
```json
{
"enable": true,
"addr": ":6001"
}
```
## rtc_config
RTC 服务配置。目前主要用于 WHIP、WHEP 和 Jessibuca 播放链路。
- `enable`:是否启用 RTC
- `ice_host_nat_to_ips`:对外暴露的 ICE 地址列表;为空时使用本机可用地址
- `ice_udp_mux_port`:ICE UDP 复用端口
- `ice_tcp_mux_port`:ICE TCP 复用端口
- `write_chan_size`:RTC 订阅侧写队列大小;如果填 `0`,程序会自动使用 `1024`
示例:
```json
{
"enable": true,
"ice_host_nat_to_ips": ["192.168.0.1"],
"ice_udp_mux_port": 4888,
"ice_tcp_mux_port": 4888,
"write_chan_size": 1024
}
```
## http_config
`lalmax` 自己的 HTTP/HTTPS 配置。管理接口、RTC 信令、HTTP-FMP4、HLS-FMP4/LLHLS 都依赖这里。
- `http_listen_addr`:HTTP 监听地址,示例 `:1290`
- `enable_https`:是否启用 HTTPS
- `https_listen_addr`:HTTPS 监听地址
- `https_cert_file`:HTTPS 证书文件
- `https_key_file`:HTTPS 私钥文件
- `ctrl_auth_whitelist`:管理接口鉴权配置
`ctrl_auth_whitelist` 的字段如下:
- `secrets`:允许的令牌列表,请求时通过查询参数 `token` 传入
- `ips`:允许访问的客户端 IP 列表
当前鉴权覆盖范围:
- `/api/stat/*`
- `/api/ctrl/*`
- `/api/hook/*`
规则说明:
- 如果 `secrets` 和 `ips` 都为空,表示不做鉴权
- 如果两者都配置了,请求必须同时满足两项
- 鉴权失败时,HTTP 状态码仍然是 `200`,返回体里的 `error_code` 是 `401`
示例:
```json
{
"http_listen_addr": ":1290",
"enable_https": true,
"https_listen_addr": ":1233",
"https_cert_file": "./conf/cert.pem",
"https_key_file": "./conf/key.pem",
"ctrl_auth_whitelist": {
"ips": ["192.168.1.10"],
"secrets": ["EC3D1536-5D93-4BD6-9FBD-96A52CB1596D"]
}
}
```
## fmp4_config
`lalmax` 的 FMP4 相关配置,分成 `http` 和 `hls` 两段。
### fmp4_config.http
HTTP-FMP4 配置。
- `enable`:是否启用 HTTP-FMP4
### fmp4_config.hls
HLS-FMP4 / LLHLS 配置。
- `enable`:是否启用 HLS-FMP4 / LLHLS
- `segment_count`:m3u8 保留的切片数量
- `segment_duration`:切片时长,单位秒
- `part_duration`:LLHLS part 时长,单位毫秒
- `low_latency`:是否启用低延迟 HLS
示例:
```json
{
"http": {
"enable": true
},
"hls": {
"enable": true,
"segment_count": 7,
"segment_duration": 1,
"part_duration": 200,
"low_latency": false
}
}
```
## logic_config
`lalmax` 扩展流分组配置。
- `gop_cache_num`:GOP 缓存数量
- `single_gop_max_frame_num`:单个 GOP 最多缓存多少帧;`0` 表示自动判断
示例:
```json
{
"gop_cache_num": 1,
"single_gop_max_frame_num": 0
}
```
## server_id
服务实例标识。
这个值会出现在:
- Hook 事件的 `server_id`
- HTTP 回调的 payload
示例:
```json
"server_id": "1"
```
## http_notify
内置 HTTP 回调插件配置。
先说明两件事:
- 这段配置控制的是“是否向外发 HTTP 回调”
- 不影响内部 HookHub、本地插件注册、`/api/hook/*` 查询和订阅能力
字段如下:
- `enable`:是否启用内置 HTTP 回调插件
- `update_interval_sec`:周期性生成 `on_update` 事件的间隔秒数
- `on_server_start`
- `on_update`
- `on_group_start`
- `on_group_stop`
- `on_stream_active`
- `on_pub_start`
- `on_pub_stop`
- `on_sub_start`
- `on_sub_stop`
- `on_relay_pull_start`
- `on_relay_pull_stop`
- `on_rtmp_connect`
- `on_hls_make_ts`
关于 `update_interval_sec`,当前程序的行为是:
- 大于 `0` 时,`lalmax` 会按这个周期向 HookHub 发布 `on_update`
- 即使 `enable=false`,这些事件依然会进入 HookHub,也能被 `/api/hook/*` 和进程内插件看到
- 只有在 `enable=true` 且对应回调地址非空时,内置插件才会真正向外发 HTTP 请求
示例:
```json
{
"enable": true,
"update_interval_sec": 5,
"on_update": "http://127.0.0.1:10101/on_update",
"on_group_start": "http://127.0.0.1:10101/on_group_start",
"on_group_stop": "http://127.0.0.1:10101/on_group_stop",
"on_stream_active": "http://127.0.0.1:10101/on_stream_active",
"on_pub_start": "http://127.0.0.1:10101/on_pub_start",
"on_pub_stop": "http://127.0.0.1:10101/on_pub_stop",
"on_sub_start": "http://127.0.0.1:10101/on_sub_start",
"on_sub_stop": "http://127.0.0.1:10101/on_sub_stop",
"on_relay_pull_start": "http://127.0.0.1:10101/on_relay_pull_start",
"on_relay_pull_stop": "http://127.0.0.1:10101/on_relay_pull_stop",
"on_rtmp_connect": "http://127.0.0.1:10101/on_rtmp_connect",
"on_server_start": "http://127.0.0.1:10101/on_server_start",
"on_hls_make_ts": "http://127.0.0.1:10101/on_hls_make_ts"
}
```
建议:
- 对外统一只配置 `lalmax.http_notify`
- `lal` 配置段里的原生 `http_notify` 建议保持关闭
- 如果两边同时往外发,尤其都带 `on_update`,很容易出现重复回调
Hook 事件的具体语义请看 [hook_api.md](./hook_api.md)。
## gb28181_config
GB28181 服务配置。
字段如下:
- `enable`:是否启用 GB28181
- `listen_addr`:SIP 服务监听 IP,默认会补成 `0.0.0.0`
- `sip_ip`:SIP 对外地址,生成设备交互内容时会用到
- `sip_port`:SIP 端口,默认 `5060`
- `serial`:平台 ID,默认 `34020000002000000001`
- `realm`:平台域,默认 `3402000000`
- `username`:认证用户名
- `password`:认证密码
- `keepalive_interval`:设备心跳周期,默认 `60`
- `quick_login`:是否允许设备通过 Keepalive 快速建档
- `media_config`:媒体端口配置
`media_config` 字段如下:
- `media_ip`:在 SDP 中对外声明的媒体 IP;默认 `0.0.0.0`
- `listen_port`:固定媒体端口起点;默认 `30000`
- `multi_port_max_increment`:多端口模式下可分配的附加端口范围;默认 `3000`
示例:
```json
{
"enable": true,
"listen_addr": "0.0.0.0",
"sip_ip": "100.100.100.101",
"sip_port": 5060,
"serial": "34020000002000000001",
"realm": "3402000000",
"username": "admin",
"password": "admin123",
"keepalive_interval": 60,
"quick_login": false,
"media_config": {
"media_ip": "100.100.100.101",
"listen_port": 30000,
"multi_port_max_increment": 3000
}
}
```
## 兼容说明
当前程序还兼容一部分旧配置写法:
- 旧版平铺配置仍然可以读
- `lal_config_path` 仍然保留兼容
- 如果没有 `logic_config`,会尝试兼容旧字段 `hook_config`
- 如果没有 `fmp4_config`,会尝试兼容旧字段 `httpfmp4_config` 和 `hls_config`
但新项目建议统一使用当前这套结构,也就是:
- 顶层使用 `lalmax` 和 `lal`
- `lalmax` 内部使用当前代码里的 snake_case 字段名
## 相关文档
- [lal_config.md](./lal_config.md):`lal` 原生配置
- [api.md](./api.md):统一管理 API 总览
- [hook_api.md](./hook_api.md):Hook 查询与订阅接口
- [hook_plugin_architecture.md](./hook_plugin_architecture.md):HookHub 与插件化架构
================================================
FILE: document/gb28181.md
================================================
# GB28181
lalmax的gb28181功能为单端口监听(TCP/UDP监听端口可以使用tcp_listen_port和udp_listen_port进行配置),根据INVITE消息中的ssrc来区分具体流名,详细的配置见gb28181_config
# GB28181相关HTTP API
目前主要提供的API如下
[/api/gb/device_infos](#apigbdevice_infos)
[/api/gb/update_all_notify](#apigbupdate_all_notify)
[/api/gb/update_notify](#apigbupdate_notify)
[/api/gb/start_play](#apigbstart_play)
[/api/gb/ptz_direction](#apigbptz_direction)
[/api/gb/ptz_zoom](#apigbptz_zoom)
[/api/gb/ptz_fi](#apigbptz_fi)
[/api/gb/ptz_preset](#apigbptz_preset)
[/api/gb/ptz_stop](#apigbptz_stop)
返回信息格式如下
```
{
"code": <int64>, // 状态码
"msg": <string>, // 状态码对应的解释
"data": <any> // 具体返回信息
}
其中code和msg的对应关系如下
1000: success
1001: 请求参数错误
1002: 服务繁忙
1003: 设备暂时未注册
1004: 设备停止播放错误
```
## /api/gb/device_infos
API含义: 获取注册的设备信息
Method: GET
data信息:
```
"data": {
"device_items": [
{
"device_id": <string>, // 设备ID
"channels": [ // 通道信息
{
"channel_id": <string>, // 通道ID
"name": <string>, // 设备名称
"manufacturer": <string>, // 制造厂商
"owner: <string>, // 设备归属
"civilCode": <string>, // 行政区划编码
"address": <string>, // 地址
"status": <string>, // 设备状态,ON/OFF
"longitude": <string>, // 经度
"latitude": <string> // 纬度
}
]
}
]
}
```
示例
```
curl http://127.0.0.1:1290/api/gb/device_infos -X GET
{
"code":1000,
"msg":"success",
"data":{
"device_items":[
{
"device_id":"34020000001320000001",
"channels":[
{
"channel_id":"34020000001320000001",
"name":"Camera 01",
"manufacturer":"Hikvision",
"owner":"Owner",
"civilCode":"3402000000",
"address":"Address",
"status":"ON",
"longitude":"",
"latitude":""
}
]
}
]
}
}
```
## /api/gb/update_all_notify
API含义: 更新全部信息
Method: POST
请求body信息: 无
data信息: 无
示例:
```
curl http://127.0.0.1:1290/api/gb/update_all_notify -X POST
{
"code":1000,
"msg":"success"
}
```
## /api/gb/update_notify
API含义: 更新某个设备信息
Method: POST
请求body信息
```
{
"device_id": <string> // 设备ID
}
```
data信息: 无
示例:
```
curl "http://127.0.0.1:1290/api/gb/update_notify" -X POST -d '{"device_id": "34020000001320000001"}'
{
"code":1000,
"msg":"success"
}
```
## /api/gb/start_play
API含义: 播放某通道
Method: POST
请求body信息:
```
{
"device_id": <string>, // 设备ID
"channel_id": <string>, // 通道ID
"network": <string>, // 传输协议类型, tcp/udp
"stream_name": <string> // 对应的流名,不指定的话就使用channel_id
"single_port": <bool> // 是否单端口
"dump_file_name": <string> // dump文件路径
}
```
data信息:
```
{
"stream_name": <string> // 流名
}
```
示例:
```
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}'
{
"code":1000,
"msg":"success"
"data": {
"stream_name": "test001"
}
}
```
## /api/gb/stop_play
API含义: 停止播放某通道
Method: POST
请求body信息:
```
{
"device_id": <string>, // 设备ID
"channel_id": <string>, // 通道ID
"stream_name": <string> // ssrc对应的流名,不指定的话就使用channel_id
}
```
data信息: 无
示例:
```
curl "http://127.0.0.1:1290/api/gb/stop_play" -X POST -d '{"device_id": "34020000001320000001", "channel_id": "34020000001320000001", "stream_name": "test001}'
{
"code":1000,
"msg":"success"
}
```
## /api/gb/ptz_direction
API含义: ptz 方向控制
Method: POST
请求body信息:
```
{
"device_id": <string>, // 设备ID
"channel_id": <string>, // 通道ID
"up": <bool>, // 上
"down": <bool> // 下
"left": <bool> // 左
"right": <bool> // 右
"speed": <int> // 步长,1~8
}
```
## /api/gb/ptz_zoom
API含义: 镜头变倍
Method: POST
请求body信息:
```
{
"device_id": <string>, // 设备ID
"channel_id": <string>, // 通道ID
"zoom_out": <bool>, // 缩小
"zoom_in": <bool> // 放大
"speed": <int> // 步长,1~8
}
```
## /api/gb/ptz_fi
API含义: 光圈控制和聚焦控制
Method: POST
请求body信息:
```
{
"device_id": <string>, // 设备ID
"channel_id": <string>, // 通道ID
"iris_in": <bool>, // 光圈小
"iris_out": <bool> // 光圈大
"focus_near": <bool> // 聚焦近
"focus_far": <bool> // 聚焦远
"speed": <int> // 步长,1~8
}
```
## /api/gb/ptz_preset
API含义: 预置位操作
Method: POST
请求body信息:
```
{
"device_id": <string>, // 设备ID
"channel_id": <string>, // 通道ID
"cmd": <int>, // 0:添加,1:删除,2:调用
"point": <int> // 预置点
}
```
## /api/gb/ptz_stop
API含义: 停止ptz
Method: POST
请求body信息:
```
{
"device_id": <string>, // 设备ID
"channel_id": <string>, // 通道ID
}
```
# 海康设备接入

================================================
FILE: document/hook_api.md
================================================
# Hook API
`lalmax` 统一托管 `lal` 的 notify 事件,并补充 `lalmax` 自身扩展订阅状态。
如果需要理解完整分层、调用链、插件职责和设计边界,见 [hook_plugin_architecture.md](./hook_plugin_architecture.md)。
默认建议:
- 对外状态与控制走 `lalmax` 的 `/api/stat/*` 和 `/api/ctrl/*`
- 对外 hook 事件读取也走 `lalmax`
- `lal.http_api` 和外部业务直接对接 `lal` 原生 notify 只作为调试手段
## Event Source
`lalmax` 内部将以下事件统一写入 hook hub:
- `on_server_start`
- `on_update`
- `on_group_start`
- `on_group_stop`
- `on_stream_active`
- `on_pub_start`
- `on_pub_stop`
- `on_sub_start`
- `on_sub_stop`
- `on_relay_pull_start`
- `on_relay_pull_stop`
- `on_rtmp_connect`
- `on_hls_make_ts`
其中 `on_update` 的 `groups` 数据已经过 `lalmax` 聚合,包含:
- `lal` 原生 group 状态
- `lalmax` 扩展订阅者统计
与 `/api/stat/group`、`/api/stat/all_group` 一样,`on_update.groups[*]` 中的 `subs` 也是统一聚合后的订阅列表;如果业务需要显式区分 `lalmax` 扩展订阅者,建议结合 stat API 中的 `lalmax.ext_subs` 使用。
`on_group_start`、`on_stream_active` 和 `on_group_stop` 是 `lalmax` 基于统一输入流生命周期直接生成的事件,payload 结构如下:
```json
{
"server_id": "1",
"app_name": "live",
"stream_name": "test110"
}
```
注意:当前上游 `lal` 的 `WithOnHookSession` 只直接提供 `streamName`,因此这类 group 生命周期事件里的 `app_name` 在部分场景下可能为空,不能把它当成始终可靠存在的字段。
三者的语义区别是:
- `on_group_start`: 流生命周期进入 `lalmax`
- `on_stream_active`: 收到首个音频或视频 RTMP 消息,只触发一次
- `on_group_stop`: 流生命周期结束。业务上要判断“没有流了”,应使用这个事件
其中“没有流了”不单独新增新的 hook,仍统一使用 `on_group_stop`。
## HTTP API
### `GET /api/hook/recent`
读取最近 hook 事件快照。
请求参数:
- `limit`: 可选,返回事件数量,默认 `20`
- `app_name`: 可选,只返回指定 app 的事件
- `stream_name`: 可选,只返回指定流的事件
- `session_id`: 可选,只返回指定会话的事件
- `event`: 可选,只返回单个事件类型
- `events`: 可选,逗号分隔的多个事件类型
示例:
```bash
curl "http://127.0.0.1:1290/api/hook/recent?limit=5"
curl "http://127.0.0.1:1290/api/hook/recent?stream_name=test110&events=on_group_start,on_stream_active,on_group_stop,on_update"
```
响应示例:
```json
{
"error_code": 0,
"desp": "succ",
"data": {
"events": [
{
"id": 12,
"event": "on_pub_start",
"timestamp": "2026-04-24T15:20:11.123456789+08:00",
"payload": {
"server_id": "1",
"session_id": "RTMPPUB1",
"protocol": "RTMP",
"base_type": "PUB",
"stream_name": "test110"
}
}
]
}
}
```
### `GET /api/hook/stream`
以 `Server-Sent Events` 持续订阅 hook 事件。
示例:
```bash
curl -N http://127.0.0.1:1290/api/hook/stream
curl -N "http://127.0.0.1:1290/api/hook/stream?stream_name=test110&events=on_group_start,on_stream_active,on_group_stop,on_update"
```
返回格式:
```text
id: 12
event: on_pub_start
data: {"server_id":"1","session_id":"RTMPPUB1","protocol":"RTMP","base_type":"PUB","stream_name":"test110"}
```
连接建立后会先回放最近一批事件,再进入实时流。
## In-Process Usage
如果业务代码和 `lalmax` 在同一进程内,可以直接使用:
```go
hub := serverInstance.HookHub()
_, ch, cancel := hub.Subscribe(64)
defer cancel()
for event := range ch {
// event.Event
// event.Payload
}
```
## Plugin Usage
如果具体业务希望由插件处理,而不是把逻辑写进 `lalmax` 主流程,可以注册 hook 插件:
```go
type BizPlugin struct{}
func (p *BizPlugin) Name() string { return "biz-plugin" }
func (p *BizPlugin) OnHookEvent(event server.HookEvent) error {
// 业务处理
return nil
}
cancel, err := serverInstance.RegisterHookPlugin(&BizPlugin{}, server.HookPluginOptions{
Filter: server.NewHookEventFilter("live", "test110", "", []string{
server.HookEventPubStart,
server.HookEventPubStop,
}),
})
if err != nil {
panic(err)
}
defer cancel()
```
当前默认的 HTTP notify 转发已经作为内置插件存在,外部业务插件只需要关注自己的处理逻辑。
## Notes
- `/api/hook/*` 使用和 `/api/stat/*`、`/api/ctrl/*` 相同的鉴权中间件
- 当前 `lal` 的 `WithOnHookSession` 回调只提供 `streamName`,不提供 `appName`
- 因此扩展订阅者与 `app_name` 的精确归属能力仍受上游 hook 入参限制
- 建议只使用 `lalmax.http_notify` 作为对外 webhook 配置;如果 `lal` 配置段也单独开启原生 `http_notify`,尤其是 `update_interval_sec`,可能出现重复的 `on_update`
- `on_group_start` / `on_stream_active` / `on_group_stop` 比基于 `on_update` 快照 diff 的方案更实时,也更不容易漏掉短生命周期流
- `on_update` 仍然建议保留给状态快照、巡检和最终一致对账使用
================================================
FILE: document/hook_plugin_architecture.md
================================================
# Hook Plugin Architecture
本文档详细说明 `lalmax` 当前的 Hook 体系设计,包括:
- 为什么需要由 `lalmax` 统一托管 hook
- 事件从 `lal` 到业务插件的完整调用链
- `HookHub`、过滤器、插件调度器各自的职责
- 默认 HTTP notify 在新架构中的位置
- 业务插件的推荐接入方式
- 当前设计边界与后续演进方向
## 1. 设计目标
这套 Hook 架构的目标不是把业务逻辑写进 `lalmax`,而是把 `lalmax` 固定为一个稳定的媒体事件平台层。
核心目标:
- `lal` 继续作为原生流状态事实源
- `lalmax` 统一聚合原生状态和扩展订阅状态
- `lalmax` 统一对外暴露 Hook 读取能力
- 具体业务处理通过插件完成,而不是散落在主流程中
- 慢业务不能阻塞媒体主链路
一句话概括:
`lalmax` 负责“采集、聚合、过滤、分发”,业务插件负责“消费和处理”。
## 2. 分层结构
当前 Hook 链路分为 4 层。
### 2.1 `lal` 原生事件层
`lal` 通过 `INotifyHandler` 向外抛出原生事件,例如:
- `OnServerStart`
- `OnUpdate`
- `OnPubStart`
- `OnPubStop`
- `OnSubStart`
- `OnSubStop`
- `OnRelayPullStart`
- `OnRelayPullStop`
- `OnRtmpConnect`
- `OnHlsMakeTs`
这一层只负责产生事件,不负责业务分发。
在 `lalmax` 这一层,还会基于统一输入流生命周期派生额外的 group 生命周期事件:
- `on_group_start`
- `on_stream_active`
- `on_group_stop`
### 2.2 `lalmax` HookHub 层
`lalmax` 使用 [http_notify.go](./../server/http_notify.go) 中的 `HttpNotify` 作为统一 HookHub。
它当前承担 5 类职责:
1. 接住 `lal` 发出的原生 notify 事件
2. 对 `on_update` 的 group 数据做聚合增强
3. 为事件补充过滤所需的元数据
4. 将事件写入历史缓存,并提供 SSE/Recent 读取
5. 将事件异步分发给插件
虽然这个结构体名字仍叫 `HttpNotify`,但职责已经不只是“发 HTTP 回调”,而是整个 Hook 总线。
### 2.3 过滤层
过滤逻辑在 [hook_filter.go](./../server/hook_filter.go)。
这层负责统一定义事件匹配规则,当前支持:
- `app_name`
- `stream_name`
- `session_id`
- `event`
- `events`
这一层的意义是“统一语义”,保证:
- `/api/hook/recent`
- `/api/hook/stream`
- 业务插件注册过滤
三者使用同一套过滤规则,而不是每处自己实现一套判断逻辑。
### 2.4 插件层
插件接口在 [hook_plugin.go](./../server/hook_plugin.go):
```go
type HookPlugin interface {
Name() string
OnHookEvent(event HookEvent) error
}
```
插件层只关心一件事:收到匹配事件后做自己的业务处理。
典型插件可以是:
- HTTP webhook 转发
- Kafka 生产者
- Redis Stream 写入器
- 数据库落表
- 业务内存回调
- 审计日志插件
## 3. 事件调用链
以 `OnPubStart` 为例,完整调用链如下:
```text
lal native event
-> HttpNotify.NotifyPubStart(info)
-> publish(HookEventPubStart, info)
-> 填充过滤元数据
-> 写入 history
-> 推送给 SSE / recent 订阅者
-> dispatchPlugins(event)
-> 匹配到的插件各自异步消费
```
以 `OnUpdate` 为例,还会多一步聚合:
```text
lal native update
-> HttpNotify.NotifyUpdate(info)
-> 聚合 lal group + lalmax 扩展订阅者
-> publish(HookEventUpdate, mergedInfo)
-> history / SSE / plugin dispatch
```
而 `on_group_start` / `on_stream_active` / `on_group_stop` 并不是在 `OnUpdate` 流程内 diff 生成的,而是直接跟随输入流生命周期与首个媒体消息触发:
```text
group/media lifecycle
-> WithOnHookSession create
-> Group.OnMsg first real media
-> Group.OnStop
-> publish(HookEventGroupStart / HookEventStreamActive / HookEventGroupStop, info)
```
这意味着:
- 查询接口拿到的是聚合后的视图
- hook 事件里的 `on_update` 也是聚合后的视图
- HTTP notify 与插件消费看到的是同一份增强数据
## 4. 为什么默认 HTTP notify 也做成插件
旧模式下,`NotifyPubStart/NotifyUpdate/...` 会直接在主流程里发 HTTP POST。
这样做的问题是:
- HTTP 转发是业务出口的一种,不应该写死在主流程
- 后续增加 Kafka、Redis、数据库 sink 时会继续污染主流程
- 不同业务出口的生命周期与重试策略难以统一管理
现在的做法是:
- 主流程只负责 `publish`
- 默认 HTTP notify 转发实现为内置插件
- 内置插件文件在 [hook_builtin_http_plugin.go](./../server/hook_builtin_http_plugin.go)
这样后续无论新增什么业务出口,都和默认 HTTP notify 处于同一层级。
## 5. HookEvent 结构说明
对外公开的事件结构是:
```go
type HookEvent struct {
ID int64
Event string
Timestamp string
Payload json.RawMessage
}
```
其中:
- `ID` 用于事件顺序控制
- `Event` 是事件类型名,例如 `on_pub_start`
- `Timestamp` 是事件产生时间
- `Payload` 是具体事件数据
此外,内部还会维护用于过滤的元数据,例如:
- `sessionID`
- `streamName`
- `appName`
- `groupKeys`
这些字段不直接暴露给外部 API,但会用于:
- 路由层过滤
- 插件过滤
- `on_update` 的 group 命中判断
## 6. 过滤语义
过滤规则统一由 `HookEventFilter.Match` 决定。
### 6.1 单会话事件
例如:
- `on_group_start`
- `on_stream_active`
- `on_group_stop`
- `on_pub_start`
- `on_pub_stop`
- `on_sub_start`
- `on_sub_stop`
- `on_relay_pull_start`
- `on_relay_pull_stop`
除 group 级事件外,这类事件会直接携带:
- `session_id`
- `stream_name`
- `app_name`
因此过滤时按单个流或单个会话精准匹配。
其中 `on_group_start` / `on_stream_active` / `on_group_stop` 是 group 级别事件,没有 `session_id`,只携带:
- `stream_name`
- `app_name`
其中 `app_name` 当前并不保证始终非空,它仍受上游 `WithOnHookSession` 只提供 `streamName` 的限制。
### 6.2 `on_update`
`on_update` 一次可能携带多个 group。
因此内部会把它展开成一组 `groupKeys`,过滤时判断:
- 是否有任意一个 group 命中过滤条件
也就是说,一个 `on_update` 事件只要包含目标流,就会被保留。
`on_group_start` / `on_stream_active` / `on_group_stop` 是直接跟随输入流生命周期产生的,因此比基于 `on_update` 快照 diff 的方案更实时,也更不容易漏掉短生命周期流。
其中:
- `on_group_start` 表示 group 生命周期开始
- `on_stream_active` 表示首个音频或视频消息真正到达,只触发一次
- `on_group_stop` 表示 group 生命周期结束,也是“没有流了”应使用的事件
但当前仍有一个边界:
- 上游 `lal` 的 `WithOnHookSession` 只提供 `streamName`
- 因此这类 direct lifecycle hook 的 `app_name` 归属能力仍受上游接口限制
- 如果同时保留 `lal` 原生 `http_notify` 和 `lalmax` 自己的 HookHub 出口,尤其同时配置两个 `update_interval_sec`,`on_update` 可能重复
### 6.3 当前支持的过滤条件
- `app_name`
- `stream_name`
- `session_id`
- `event`
- `events`
建议:
- 单流订阅优先同时带 `app_name + stream_name`
- 精确追踪某个连接时使用 `session_id`
- 降低噪音时优先限制 `event/events`
## 7. 业务插件如何接入
业务代码和 `lalmax` 同进程时,推荐直接注册插件。
### 7.1 最小插件示例
```go
type BizPlugin struct{}
func (p *BizPlugin) Name() string {
return "biz-plugin"
}
func (p *BizPlugin) OnHookEvent(event server.HookEvent) error {
// 业务处理
return nil
}
```
### 7.2 注册示例
```go
cancel, err := serverInstance.RegisterHookPlugin(&BizPlugin{}, server.HookPluginOptions{
Filter: server.NewHookEventFilter("live", "test110", "", []string{
server.HookEventPubStart,
server.HookEventPubStop,
server.HookEventUpdate,
}),
BufferSize: 64,
})
if err != nil {
panic(err)
}
defer cancel()
```
### 7.3 字段说明
- `Name()`
用作插件唯一标识。重复名称不允许重复注册。
- `Filter`
用于控制这个插件只消费自己关心的事件。
- `BufferSize`
用于控制插件异步队列大小。
### 7.4 为什么推荐插件而不是直接改主流程
因为主流程的职责应该稳定,而业务处理天然是变化的。
如果把每个业务都写进主流程,会出现:
- 发布一个新业务就要改核心代码
- 多业务逻辑互相影响
- 回归成本越来越高
- 业务异常更容易污染核心链路
插件化之后,核心层和业务层边界清晰很多。
## 8. 插件调度模型
插件分发是异步的,每个插件有自己的缓冲队列。
调度模型:
```text
publish(event)
-> 遍历已注册插件
-> 根据 Filter 判断是否命中
-> 命中则投递到该插件自己的 queue
-> 插件 goroutine 从 queue 中消费
```
这个模型的含义是:
- 插件之间互不阻塞
- 插件不会反压媒体主链路
- 某个慢插件只影响自己
当前策略下,如果插件队列满了:
- 当前事件会被丢弃
- 记录 warn 日志
这是有意选择,优先保证媒体主链路稳定。
## 9. 当前默认行为
当前系统启动后,默认会注册一个内置插件:
- `builtin-http-notify`
它负责把事件按旧配置转发到:
- `on_server_start`
- `on_update`
- `on_group_start`
- `on_stream_active`
- `on_group_stop`
- `on_pub_start`
- `on_pub_stop`
- `on_sub_start`
- `on_sub_stop`
- `on_relay_pull_start`
- `on_relay_pull_stop`
- `on_rtmp_connect`
- `on_hls_make_ts`
这意味着旧的 `http_notify` 配置仍然可用,但实现方式已经改成:
```text
HookHub -> builtin-http-notify plugin -> HTTP callback
```
而不再是主流程直接发 HTTP。
## 10. API、SSE、插件三者关系
三者读的是同一个 HookHub。
### 10.1 `/api/hook/recent`
适合:
- 排查最近事件
- 调试过滤表达式
- 运维观察
### 10.2 `/api/hook/stream`
适合:
- 实时消费
- 调试前端或外部观察程序
- 对接轻量事件订阅方
### 10.3 插件
适合:
- 同进程业务接入
- 需要更复杂处理逻辑
- 需要将事件转发到第三方系统
三者的事件源一致,过滤语义一致,只是使用方式不同。
## 11. 推荐使用方式
### 11.1 业务和 `lalmax` 同进程
优先用插件:
- 延迟低
- 无需再走 HTTP
- 易于封装业务逻辑
### 11.2 业务和 `lalmax` 不同进程
优先用:
- `/api/hook/stream`
- 或内置 HTTP notify 插件
### 11.3 需要统一平台出口
可以继续在插件层增加:
- Kafka 插件
- Redis 插件
- 数据库存档插件
## 12. 当前边界与限制
### 12.1 `app_name` 边界
当前上游 `lal` 的 `WithOnHookSession` 仍只提供 `streamName`,不提供 `appName`。
这意味着:
- `lal` 原生 group 状态本身是可信的
- 但扩展订阅者与 `app_name` 的精确归属能力仍受上游输入限制
因此文档里一直建议:
- 需要精确路由时,尽量同时使用 `app_name + stream_name`
### 12.2 插件可靠性策略
当前插件队列满时是丢弃策略,不是阻塞策略,也不是持久化重试策略。
这是为了媒体主链路稳定。
如果未来某类插件需要强可靠投递,建议不要直接在 `lalmax` 内核层强推重试,而是:
- 插件内自己做持久化
- 或者把事件转发给外部消息系统
### 12.3 当前插件装配方式
目前插件仍然通过代码注册。
也就是说:
- 你需要拿到 `LalMaxServer`
- 调用 `RegisterHookPlugin(...)`
下一步可以继续演进成“配置化装配”,由配置声明启用哪些插件和参数。
## 13. 后续演进建议
比较合理的后续方向有 3 个。
### 13.1 插件配置化装配
目标:
- 不用业务代码手动注册插件
- 配置文件直接声明插件列表、参数、过滤条件
### 13.2 标准化插件参数
例如统一定义:
- HTTP webhook 插件参数
- Kafka 插件参数
- Redis 插件参数
### 13.3 更强的可靠性模型
例如:
- 插件失败重试
- 死信队列
- 插件级别熔断
- 指标与监控
## 14. 小结
现在的 Hook 架构已经完成了从“固定 HTTP 回调实现”到“统一 HookHub + 插件化业务处理”的转换。
当前职责边界可以概括为:
- `lal`: 原生媒体事件事实源
- `lalmax` HookHub: 聚合、过滤、缓存、分发
- 插件: 具体业务处理
这套结构的核心价值是:
- 主流程稳定
- 业务接入灵活
- 多业务可并存
- 后续扩展成本更低
================================================
FILE: document/lal_api.md
================================================
# lal Native HTTP API
`lalmax` 内嵌运行 `lal`。默认情况下,对外建议统一使用 `lalmax` 自己的 API 门面:
- `lalmax.http_config.http_listen_addr` 下的 `/api/stat/*`
- `lalmax.http_config.http_listen_addr` 下的 `/api/ctrl/*`
- `lalmax.http_config.http_listen_addr` 下的 `/api/hook/*`
`lal.http_api` 只建议在调试 `lal` 原生行为时临时开启。
默认配置:
```json
{
"http_api": {
"enable": false,
"addr": ":8083"
}
}
```
启用后可访问:
```text
http://127.0.0.1:8083
```
## Native Endpoints
`lal` 原生 HTTP API 当前主要包含:
- `GET /lal.html`
- `GET /api/stat/lal_info`
- `GET /api/stat/all_group`
- `GET /api/stat/group`
- `POST /api/ctrl/start_relay_pull`
- `GET /api/ctrl/stop_relay_pull`
- `POST /api/ctrl/kick_session`
- `POST /api/ctrl/start_rtp_pub`
## Recommended Gateway
推荐直接使用 `lalmax` API,因为它会在 `lal` 原生结果基础上补充:
- `lalmax` 扩展订阅者统计
- 更完整的统一状态视图
- 统一的 hook 事件读取能力
- 统一鉴权入口
默认地址:
```text
http://127.0.0.1:1290/api/stat/group
http://127.0.0.1:1290/api/stat/all_group
http://127.0.0.1:1290/api/stat/lal_info
http://127.0.0.1:1290/api/ctrl/start_relay_pull
http://127.0.0.1:1290/api/ctrl/stop_relay_pull
http://127.0.0.1:1290/api/ctrl/kick_session
http://127.0.0.1:1290/api/ctrl/start_rtp_pub
http://127.0.0.1:1290/api/ctrl/stop_rtp_pub
http://127.0.0.1:1290/api/hook/recent
http://127.0.0.1:1290/api/hook/stream
```
## Compatibility Notes
- `lalmax` 的 `/api/ctrl/*` 请求/响应结构与 `lal` 原生 API 基本保持一致
- `lalmax` 的 `/api/stat/group` 和 `/api/stat/all_group` 会在兼容 `lal` 原有字段的基础上新增 `lalmax` 扩展块
- `stop_relay_pull` 在 `lalmax` 中兼容 `GET`
- `stat/group` 在 `lalmax` 中会优先结合 `app_name + stream_name` 做更精确的 group 匹配
- `on_update` 等 hook 事件在 `lalmax` 中已经过聚合增强
## Stat API Extension
`lalmax` 的统计接口会返回两层信息:
1. `lal` 兼容层
也就是原有的 `stream_name`、`app_name`、`pub`、`subs`、`pull`、`in_frame_per_sec` 等字段
2. `lalmax` 扩展层
当前主要是 `lalmax.ext_subs`
其中:
- `subs` 是聚合后的统一订阅列表
- `lalmax.ext_subs` 是其中来自 `lalmax` 扩展协议层的子集
这意味着调用方如果完全按照 `lal` 老接口解析,通常仍然可以工作;如果要区分哪些订阅者是 `lalmax` 自己维护的,就再读取 `lalmax.ext_subs`。
控制类接口不附带这些扩展状态。如果执行控制动作后还需要查看最新流状态,应再调用 `/api/stat/group` 或 `/api/stat/all_group`。
## Debug Usage
只有在以下场景,才建议单独开启 `lal.http_api`:
- 排查 `lal` 原生 HTTP API 行为
- 对比 `lal` 原始 group 数据和 `lalmax` 聚合数据
- 调试上游 `lal` 升级后的兼容性
================================================
FILE: document/lal_config.md
================================================
# lal 原生配置说明
本文档说明 `conf/lalmax.conf.json` 中 `lal` 配置段的常用字段。`lal` 配置段会直接传给 lal 原生服务,用于 RTMP、RTSP、HTTP-FLV、HLS-TS、HTTP-TS、录制、鉴权和原生 HTTP API。
## rtmp
- `enable`: 是否启用 RTMP 服务。
- `addr`: RTMP 监听地址,例如 `:1935`。
- `rtmps_enable`: 是否启用 RTMPS。
- `rtmps_addr`: RTMPS 监听地址,例如 `:4935`。
- `rtmps_cert_file`: RTMPS 证书文件路径。
- `rtmps_key_file`: RTMPS 私钥文件路径。
- `gop_num`: RTMP 拉流 GOP 缓存数量。
- `single_gop_max_frame_num`: 单个 GOP 最大缓存帧数,`0` 表示不限制。
- `merge_write_size`: 合并写大小,`0` 表示关闭合并写。
## in_session
- `add_dummy_audio_enable`: 没有音频时是否补静音音频。
- `add_dummy_audio_wait_audio_ms`: 等待真实音频的时间,超过后才补静音音频。
## default_http
HTTP 类协议的默认监听配置。HTTP-FLV、HTTP-TS、HLS-TS 未单独配置监听地址时,会使用这里的地址。
- `http_listen_addr`: 默认 HTTP 监听地址,例如 `:8080`。
- `https_listen_addr`: 默认 HTTPS 监听地址,例如 `:4433`。
- `https_cert_file`: HTTPS 证书文件路径。
- `https_key_file`: HTTPS 私钥文件路径。
## httpflv
- `enable`: 是否启用 HTTP-FLV。
- `enable_https`: 是否启用 HTTPS HTTP-FLV。
- `url_pattern`: URL 路径匹配前缀。示例配置为 `/`,因此 `/live/test110.flv` 可用。
- `gop_num`: HTTP-FLV GOP 缓存数量。
- `single_gop_max_frame_num`: 单个 GOP 最大缓存帧数。
## hls
这里是 lal 原生 HLS-TS 配置,不是 lalmax 的 HLS-FMP4/LLHLS 配置。
- `enable`: 是否启用 HLS-TS。
- `enable_https`: 是否启用 HTTPS HLS-TS。
- `url_pattern`: HLS-TS URL 路径前缀,常用 `/hls/`。
- `out_path`: HLS-TS 文件输出目录。
- `fragment_duration_ms`: 单个 TS 分片时长。
- `fragment_num`: m3u8 中保留的分片数量。
- `delete_threshold`: 清理旧分片的阈值。
- `cleanup_mode`: 清理模式。
- `use_memory_as_disk_flag`: 是否使用内存模拟磁盘。
- `sub_session_timeout_ms`: HLS 拉流会话超时时间。
- `sub_session_hash_key`: HLS 会话哈希 key。
## httpts
- `enable`: 是否启用 HTTP-TS。
- `enable_https`: 是否启用 HTTPS HTTP-TS。
- `url_pattern`: URL 路径匹配前缀。示例配置为 `/`,因此 `/live/test110.ts` 可用。
- `gop_num`: HTTP-TS GOP 缓存数量。
- `single_gop_max_frame_num`: 单个 GOP 最大缓存帧数。
## rtsp
- `enable`: 是否启用 RTSP。
- `addr`: RTSP 监听地址,例如 `:5544`。
- `rtsps_enable`: 是否启用 RTSPS。
- `rtsps_addr`: RTSPS 监听地址,例如 `:5322`。
- `rtsps_cert_file`: RTSPS 证书文件路径。
- `rtsps_key_file`: RTSPS 私钥文件路径。
- `out_wait_key_frame_flag`: RTSP 拉流是否等待关键帧后再输出。
- `auth_enable`: 是否启用 RTSP 鉴权。
- `auth_method`: 鉴权方式。
- `username`: RTSP 鉴权用户名。
- `password`: RTSP 鉴权密码。
## record
- `enable_flv`: 是否启用 FLV 录制。
- `flv_out_path`: FLV 录制输出目录。
- `enable_mpegts`: 是否启用 MPEG-TS 录制。
- `mpegts_out_path`: MPEG-TS 录制输出目录。
## relay_push
- `enable`: 是否启用静态转推。
- `addr_list`: 转推目标地址列表。
## static_relay_pull
- `enable`: 是否启用静态回源拉流。
- `addr`: 静态回源地址。
## http_api
- `enable`: 是否启用 lal 原生 HTTP API。
- `addr`: lal 原生 HTTP API 监听地址,例如 `:8083`。
接口说明见 [lal_api.md](./lal_api.md)。
## simple_auth
简单鉴权配置,鉴权值通常按 `key + streamName` 计算。
- `key`: 鉴权 key。
- `dangerous_lal_secret`: 管理类接口使用的 secret。
- `pub_rtmp_enable`: 是否启用 RTMP 推流鉴权。
- `sub_rtmp_enable`: 是否启用 RTMP 拉流鉴权。
- `sub_httpflv_enable`: 是否启用 HTTP-FLV 拉流鉴权。
- `sub_httpts_enable`: 是否启用 HTTP-TS 拉流鉴权。
- `pub_rtsp_enable`: 是否启用 RTSP 推流鉴权。
- `sub_rtsp_enable`: 是否启用 RTSP 拉流鉴权。
- `hls_m3u8_enable`: 是否启用 HLS m3u8 鉴权。
## pprof
- `enable`: 是否启用 pprof。
- `addr`: pprof 监听地址,例如 `:8084`。
## log
- `level`: 日志级别。
- `filename`: 日志文件路径。
- `is_to_stdout`: 是否输出到标准输出。
- `is_rotate_daily`: 是否按天切分日志。
- `short_file_flag`: 是否打印短文件名。
- `timestamp_flag`: 是否打印时间戳。
- `timestamp_with_ms_flag`: 时间戳是否包含毫秒。
- `level_flag`: 是否打印日志级别。
- `assert_behavior`: 断言行为。
## debug
- `log_group_interval_sec`: group 状态日志输出间隔。
- `log_group_max_group_num`: 单次最多输出的 group 数量。
- `log_group_max_sub_num_per_group`: 单个 group 最多输出的订阅者数量。
================================================
FILE: document/rtc.md
================================================
# WebRTC(WHIP/WHEP)
WebRTC在刚发布的时候仅仅专注于VoIP和点对点用例,它仅限于几个并发的浏览器,并且不能扩展,缺少标准信令交互,故很难用于直播场景。
在此背景下,WHIP和WHEP这2个标准的提出,补齐了信令交互这一个环节,使WebRTC可以运用在直播场景。
## WHIP(WebRTC-HTTP Ingestion Protocol)
协议链接:https://datatracker.ietf.org/doc/html/draft-murillo-whip-02
WHIP(WebRTC-HTTP Ingestion Protocol)是Milicast的技术团队提出的,在与媒体服务器通信时,WHIP提供了使用标准信令协议的编码软件和硬件,这样就可以实现厂商的WebRTC推流。WHIP在WebRTC上增加了一个简单的信令层,可用于将WebRTC发布者连接到WebRTC媒体服务器,发布者只发送媒体而不接收媒体。
### 交互流程
```
+-------------+ +---------------+ +--------------+ +---------------+
| WHIP client | | WHIP endpoint | | Media Server | | WHIP Resource |
+--+----------+ +---------+-----+ +------+-------+ +--------|------+
| | | |
| | | |
|HTTP POST (SDP Offer) | | |
+------------------------>+ | |
|201 Created (SDP answer) | | |
+<------------------------+ | |
| ICE REQUEST | |
+--------------------------------------->+ |
| ICE RESPONSE | |
|<---------------------------------------+ |
| DTLS SETUP | |
|<======================================>| |
| RTP/RTCP FLOW | |
+<-------------------------------------->+ |
| HTTP DELETE |
+---------------------------------------------------------->+
| 200 OK |
<-----------------------------------------------------------x
```
(1)WHIP client使用HTTP POST请求执行单次SDP Offer/Answer,以便在编码器/媒体生产者(WHIP客户端)和广播接收端点(媒体服务器)之间建立ICE/DTLS会话。
(2)一旦ICE/DTLS会话建立,媒体将从编码器/媒体生成器(WHIP客户端)单向流向广播接收端点(媒体服务器)。为了降低复杂性,不支持SDP重新协商,因此在完成通过HTTP的初始SDP Offer/Answer后,不能添加或删除任何track或stream。
(3)HTTP POST请求的内容类型为“application/sdp”,并包含作为主体的SDP Offer。WHIP端点将生成一个SDP Answer并返回一个“201 Created”响应,内容类型为“application/SDP”。
## WHEP(WebRTC-HTTP Egress Protocol)
协议链接:https://datatracker.ietf.org/doc/html/draft-murillo-whep-02
WHEP(WebRTC-HTTP Egress Protocol)也是在WebRTC上增加了一个简单的信令层,可用于将WebRTC播放者连接到WebRTC媒体服务器,播放者只接收媒体,不发送媒体。
```
+-------------+ +---------------+ +--------------+ +---------------+
| WHEP Player | | WHEP endpoint | | Media Server | | WHEP Resource |
+--+----------+ +---------+-----+ +------+-------+ +--------|------+
| | | |
| | | |
|HTTP POST (SDP Offer) | | |
+------------------------>+ | |
|201 Created (SDP answer) | | |
+<------------------------+ | |
| ICE REQUEST | |
+--------------------------------------->+ |
| ICE RESPONSE | |
|<---------------------------------------+ |
| DTLS SETUP | |
|<======================================>| |
| RTP/RTCP FLOW | |
+<-------------------------------------->+ |
| HTTP DELETE |
+---------------------------------------------------------->+
| 200 OK |
<-----------------------------------------------------------x
```
(1)WHEP Player使用HTTP POST请求执行单次SDP Offer/Answer,以便在WHEP Player和媒体服务器之间建立ICE/DTLS会话。
(2)一旦ICE/DTLS会话建立,媒体将从媒体服务器流向WHEP Player。为了降低复杂性,不支持SDP重新协商,因此在完成通过HTTP的初始SDP Offer/Answer后,不能添加或删除任何track或stream。
(3)HTTP POST请求的内容类型为“application/sdp”,并包含作为主体的SDP Offer。WHEP端点将生成一个SDP Answer并返回一个“201 Created”响应,内容类型为“application/SDP”。
## lalmax RTC
lalmax支持WHIP推流和WHEP拉流
视频:H264
音频:G711A/G711U
WHIP可以使用[vue-wish](https://github.com/zllovesuki/vue-wish)、[OBS](https://github.com/obsproject/obs-studio/actions/runs/5227109208?pr=7926)测试
WHEP拉流可以使用[vue-wish](https://github.com/zllovesuki/vue-wish)测试
### OBS测试效果
使用OBS进行whip推流到lalmax中,并用vue-wish拉流,测试延时可以做到200ms以内
OBS推流配置

vue-wish拉流效果

================================================
FILE: document/srt.md
================================================
# SRT
SRT(Secure Reliable Transport)的简称,主要优化在不可靠网络(非阻塞导致的丢包)环境下实时音视频的传输性能
## 特点
(1) 基于UDP的用户态协议栈
(2) 抗丢包能力强&低延时
(3) 传输负载无关
(4) 传输加密
## 应用场景
(1) 上行最后一公里推流加速
(2) CDN内部传输分发加速
(3) 丢包重传率比较高的场景
## 支持的流媒体服务和工具
(1) OBS
(2) VLC
(3) FFmpeg,编译需集成libsrt
(4) SRS
(5) ZLMediaKit
(6) LALMax
## 测试
(1) 启动LalMax服务
(2) 使用OBS进行推流,在"直播"中输入srt的推流地址

(3) VLC进行播放
在VLC中设置streamid,这部分填streamid后面的所有信息

输入streamid前面的部分进行拉流


================================================
FILE: document/stream_url.md
================================================
# 流地址说明
本文档使用 `conf/lalmax.conf.json` 的默认配置举例,默认流名为 `test110`。
## 基本规则
- `lal` 原生能力使用 `lal` 配置段中的端口,例如 RTMP、RTSP、HTTP-FLV、HLS-TS、HTTP-TS。
- `lalmax` 扩展能力使用 `lalmax` 配置段中的端口,例如 SRT、WHIP/WHEP、HTTP-FMP4、HLS-FMP4/LLHLS。
- 当前 lal 使用简单流管理时主要按 `streamName` 匹配。示例中的 `/live/test110` 里,`test110` 是流名,`live` 可作为常用路径前缀。
- lalmax 扩展拉流接口支持可选 `app_name` 参数,用于未来多 appName 同 streamName 的精确匹配;不传时仍按历史 `streamName` 兼容查找。
- HTTP-FLV、HTTP-TS、HLS-TS 的路径还受 `lal.httpflv.url_pattern`、`lal.httpts.url_pattern`、`lal.hls.url_pattern` 影响。示例配置中 HTTP-FLV 的 `url_pattern` 为 `/`,因此 `/live/test110.flv` 可用。
- HTTPS、RTMPS、RTSPS 依赖配置中的证书文件,浏览器或播放器可能需要信任测试证书。
## 推流地址
### RTMP
```text
rtmp://127.0.0.1:1935/live/test110
```
FFmpeg 示例:
```bash
ffmpeg -re -i demo.flv -c:a copy -c:v copy -f flv rtmp://127.0.0.1:1935/live/test110
```
如果开启 RTMPS:
```text
rtmps://127.0.0.1:4935/live/test110
```
### RTSP
```text
rtsp://127.0.0.1:5544/live/test110
```
FFmpeg 示例:
```bash
ffmpeg -re -i demo.flv -c:a copy -c:v copy -f rtsp rtsp://127.0.0.1:5544/live/test110
```
如果开启 RTSPS:
```text
rtsps://127.0.0.1:5322/live/test110
```
### SRT
```text
srt://127.0.0.1:6001?streamid=#!::h=test110,m=publish
```
`h` 表示流名,`m=publish` 表示推流。
### WebRTC WHIP
```text
http://127.0.0.1:1290/webrtc/whip?streamid=test110
https://127.0.0.1:1233/webrtc/whip?streamid=test110
```
WHIP 使用 HTTP POST 传输 SDP offer,通常由 OBS、WHIP 客户端或 WebRTC 工具调用。
### GB28181
GB28181 不是普通 URL 推流。设备通过 SIP 注册到 lalmax,平台再通过 API 控制播放。详见 [gb28181.md](./gb28181.md)。
## 拉流地址
### RTMP
```text
rtmp://127.0.0.1:1935/live/test110
```
ffplay 示例:
```bash
ffplay rtmp://127.0.0.1:1935/live/test110
```
### RTSP
```text
rtsp://127.0.0.1:5544/live/test110
```
ffplay 示例:
```bash
ffplay rtsp://127.0.0.1:5544/live/test110
```
如果开启 RTSPS:
```text
rtsps://127.0.0.1:5322/live/test110
```
### HTTP-FLV
```text
http://127.0.0.1:8080/live/test110.flv
https://127.0.0.1:4433/live/test110.flv
```
ffplay 示例:
```bash
ffplay http://127.0.0.1:8080/live/test110.flv
```
### HTTP-TS
需要启用 `lal.httpts.enable`。
```text
http://127.0.0.1:8080/live/test110.ts
https://127.0.0.1:4433/live/test110.ts
```
### HLS-TS
需要启用 `lal.hls.enable`。
```text
http://127.0.0.1:8080/hls/test110/playlist.m3u8
http://127.0.0.1:8080/hls/test110/record.m3u8
http://127.0.0.1:8080/hls/test110.m3u8
```
### SRT
```text
srt://127.0.0.1:6001?streamid=#!::h=test110,m=request
```
`h` 表示流名,`m=request` 表示拉流。
### WebRTC WHEP
```text
http://127.0.0.1:1290/webrtc/whep?streamid=test110
https://127.0.0.1:1233/webrtc/whep?streamid=test110
```
如果需要指定 appName:
```text
http://127.0.0.1:1290/webrtc/whep?streamid=test110&app_name=live
```
WHEP 使用 HTTP POST 传输 SDP offer,通常由 WHEP 播放器或 WebRTC 工具调用。
### Jessibuca DataChannel
```text
webrtc://127.0.0.1:1290/webrtc/play/live/test110
```
如果需要指定 appName:
```text
webrtc://127.0.0.1:1290/webrtc/play/live/test110?app_name=live
```
### HTTP-FMP4
```text
http://127.0.0.1:1290/live/m4s/test110.mp4
https://127.0.0.1:1233/live/m4s/test110.mp4
```
如果需要指定 appName:
```text
http://127.0.0.1:1290/live/m4s/test110.mp4?app_name=live
```
### HLS-FMP4/LLHLS
需要启用 `lalmax.fmp4_config.hls.enable`。
```text
http://127.0.0.1:1290/live/hls/test110/index.m3u8
https://127.0.0.1:1233/live/hls/test110/index.m3u8
```
如果需要指定 appName:
```text
http://127.0.0.1:1290/live/hls/test110/index.m3u8?app_name=live
```
如果需要低延迟 HLS,设置 `lalmax.fmp4_config.hls.low_latency` 为 `true`。
================================================
FILE: fmp4/hls/server.go
================================================
package hls
import (
"sync"
"time"
config "github.com/q191201771/lalmax/config"
"github.com/gin-gonic/gin"
"github.com/q191201771/lal/pkg/base"
"github.com/q191201771/naza/pkg/nazalog"
)
type HlsServer struct {
sessions sync.Map
conf config.Fmp4HlsConfig
invalidSessions sync.Map
}
func NewHlsServer(conf config.Fmp4HlsConfig) *HlsServer {
svr := &HlsServer{
conf: conf,
}
go svr.cleanInvalidSession()
return svr
}
func (s *HlsServer) NewHlsSession(streamName string) {
s.NewHlsSessionWithAppName("", streamName)
}
func (s *HlsServer) NewHlsSessionWithAppName(appName, streamName string) {
nazalog.Infof("new hls session, appName:%s, streamName:%s", appName, streamName)
session := NewHlsSessionWithAppName(appName, streamName, s.conf)
s.sessions.Store(hlsSessionKey(appName, streamName), session)
}
func (s *HlsServer) OnMsg(streamName string, msg base.RtmpMsg) {
s.OnMsgWithAppName("", streamName, msg)
}
func (s *HlsServer) OnMsgWithAppName(appName, streamName string, msg base.RtmpMsg) {
value, ok := s.sessions.Load(hlsSessionKey(appName, streamName))
if ok {
session := value.(*HlsSession)
session.OnMsg(msg)
}
}
func (s *HlsServer) OnStop(streamName string) {
s.OnStopWithAppName("", streamName)
}
func (s *HlsServer) OnStopWithAppName(appName, streamName string) {
key := hlsSessionKey(appName, streamName)
value, ok := s.sessions.Load(key)
if ok {
session := value.(*HlsSession)
s.invalidSessions.Store(session.SessionId, session)
s.sessions.Delete(key)
}
}
func (s *HlsServer) HandleRequest(ctx *gin.Context) {
streamName := ctx.Param("streamid")
appName := ctx.Query("app_name")
if session, ok := s.getSession(appName, streamName); ok {
session.HandleRequest(ctx)
}
}
func (s *HlsServer) getSession(appName, streamName string) (*HlsSession, bool) {
value, ok := s.sessions.Load(hlsSessionKey(appName, streamName))
if ok {
return value.(*HlsSession), true
}
if appName != "" {
return nil, false
}
var found *HlsSession
matchCount := 0
s.sessions.Range(func(_, value interface{}) bool {
session := value.(*HlsSession)
if session.streamName != streamName {
return true
}
found = session
matchCount++
return matchCount <= 1
})
if matchCount != 1 {
return nil, false
}
return found, true
}
type sessionKey struct {
appName string
streamName string
}
func hlsSessionKey(appName, streamName string) sessionKey {
return sessionKey{
appName: appName,
streamName: streamName,
}
}
func (s *HlsServer) cleanInvalidSession() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for range ticker.C {
s.invalidSessions.Range(func(k, v interface{}) bool {
session := v.(*HlsSession)
nazalog.Info("clean invalid session, streamName:", session.streamName, " sessionId:", k)
session.OnStop()
s.invalidSessions.Delete(k)
return true
})
}
}
================================================
FILE: fmp4/hls/session.go
================================================
package hls
import (
"time"
config "github.com/q191201771/lalmax/config"
"github.com/bluenviron/gohlslib"
"github.com/bluenviron/gohlslib/pkg/codecs"
"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
"github.com/gin-gonic/gin"
"github.com/q191201771/lal/pkg/avc"
"github.com/q191201771/lal/pkg/base"
"github.com/q191201771/lal/pkg/hevc"
"github.com/q191201771/naza/pkg/nazalog"
uuid "github.com/satori/go.uuid"
)
type HlsSession struct {
muxer *gohlslib.Muxer
done bool
data []Frame
audioCodecId int
videoCodecId int
maxMsgSize int
appName string
streamName string
sps []byte
pps []byte
vps []byte
asc []byte
startAudioPts time.Duration
startVideoPts time.Duration
audioStartPTSFilled bool
videoStartPTSFilled bool
SessionId string
}
func NewHlsSession(streamName string, conf config.Fmp4HlsConfig) *HlsSession {
return NewHlsSessionWithAppName("", streamName, conf)
}
func NewHlsSessionWithAppName(appName, streamName string, conf config.Fmp4HlsConfig) *HlsSession {
variant := gohlslib.MuxerVariantFMP4
if conf.LowLatency {
variant = gohlslib.MuxerVariantLowLatency
}
u, _ := uuid.NewV4()
session := &HlsSession{
muxer: &gohlslib.Muxer{
Variant: variant,
},
audioCodecId: -1,
videoCodecId: -1,
maxMsgSize: 128,
data: make([]Frame, 10)[0:0],
appName: appName,
streamName: streamName,
SessionId: u.String(),
}
uid, _ := uuid.NewV4()
session.SessionId = uid.String()
if !conf.LowLatency && conf.SegmentCount > 0 {
// fmp4模式下可以设置分片个数
session.muxer.SegmentCount = conf.SegmentCount
}
if conf.LowLatency && conf.PartDuration > 0 {
// llhls设置part duration
session.muxer.PartDuration = time.Millisecond * time.Duration(conf.PartDuration)
}
if conf.SegmentDuration > 0 {
session.muxer.SegmentDuration = time.Second * time.Duration(conf.SegmentDuration)
}
return session
}
func (session *HlsSession) OnMsg(msg base.RtmpMsg) {
if session.done {
if msg.Header.MsgTypeId == base.RtmpTypeIdVideo {
if msg.IsVideoKeySeqHeader() {
session.videoCodecId = int(msg.VideoCodecId())
if msg.IsAvcKeySeqHeader() {
var err error
session.sps, session.pps, err = avc.ParseSpsPpsFromSeqHeader(msg.Payload)
if err != nil {
nazalog.Error("ParseSpsPpsFromSeqHeader err:", err)
}
} else {
session.vps, session.sps, session.pps, _ = hevc.ParseVpsSpsPpsFromSeqHeaderWithoutMalloc(msg.Payload)
}
} else {
nals, err := avc.SplitNaluAvcc(msg.Payload[5:])
if err != nil {
nazalog.Error(err)
return
}
var nalus [][]byte
if msg.IsAvcKeyNalu() || msg.IsHevcKeyNalu() {
if msg.IsAvcKeyNalu() {
nalus = append(nalus, session.sps)
nalus = append(nalus, session.pps)
} else {
nalus = append(nalus, session.vps)
nalus = append(nalus, session.sps)
nalus = append(nalus, session.pps)
}
}
nalus = append(nalus, nals...)
pts := time.Millisecond*time.Duration(msg.Pts()) - session.startVideoPts
err = session.muxer.WriteH26x(time.Now(), pts, nalus)
if err != nil {
nazalog.Error("hls-fmp4 WriteH26x failed, err:", err)
}
}
} else {
if session.audioCodecId == int(base.RtmpSoundFormatAac) {
pts := time.Millisecond*time.Duration(msg.Dts()) - session.startAudioPts
err := session.muxer.WriteMPEG4Audio(time.Now(), pts, [][]byte{msg.Payload[2:]})
if err != nil {
nazalog.Error("hls-fmp4 WriteMPEG4Audio failed, err:", err)
}
} else if session.audioCodecId == int(base.RtmpSoundFormatOpus) {
pts := time.Millisecond*time.Duration(msg.Dts()) - session.startAudioPts
err := session.muxer.WriteOpus(time.Now(), pts, [][]byte{msg.Payload[1:]})
if err != nil {
nazalog.Error("hls-fmp4 WriteOpus failed, err:", err)
}
}
}
return
}
switch msg.Header.MsgTypeId {
case base.RtmpTypeIdAudio:
session.audioCodecId = int(msg.AudioCodecId())
if session.audioCodecId == int(base.RtmpSoundFormatAac) {
if msg.IsAacSeqHeader() {
session.asc = msg.Payload[2:]
} else {
if !session.audioStartPTSFilled {
session.startAudioPts = time.Millisecond * time.Duration(msg.Dts())
session.audioStartPTSFilled = true
}
pts := time.Millisecond*time.Duration(msg.Dts()) - session.startAudioPts
frame := Frame{
ntp: time.Now(),
pts: pts,
au: [][]byte{msg.Payload[2:]},
codecType: msg.AudioCodecId(),
}
session.data = append(session.data, frame)
}
} else if session.audioCodecId == int(base.RtmpSoundFormatOpus) {
if !session.audioStartPTSFilled {
session.startAudioPts = time.Millisecond * time.Duration(msg.Dts())
session.audioStartPTSFilled = true
}
pts := time.Millisecond*time.Duration(msg.Dts()) - session.startAudioPts
frame := Frame{
ntp: time.Now(),
pts: pts,
au: [][]byte{msg.Payload[1:]},
codecType: msg.AudioCodecId(),
}
session.data = append(session.data, frame)
} else {
return
}
case base.RtmpTypeIdVideo:
if msg.IsVideoKeySeqHeader() {
session.videoCodecId = int(msg.VideoCodecId())
if msg.IsAvcKeySeqHeader() {
var err error
session.sps, session.pps, err = avc.ParseSpsPpsFromSeqHeader(msg.Payload)
if err != nil {
nazalog.Error("ParseSpsPpsFromSeqHeader err:", err)
}
} else {
session.vps, session.sps, session.pps, _ = hevc.ParseVpsSpsPpsFromSeqHeaderWithoutMalloc(msg.Payload)
}
} else {
nals, err := avc.SplitNaluAvcc(msg.Payload[5:])
if err != nil {
return
}
if !session.videoStartPTSFilled {
session.startVideoPts = time.Millisecond * time.Duration(msg.Pts())
session.videoStartPTSFilled = true
}
var nalus [][]byte
if msg.IsAvcKeyNalu() || msg.IsHevcKeyNalu() {
if msg.IsAvcKeyNalu() {
nalus = append(nalus, session.sps)
nalus = append(nalus, session.pps)
} else {
nalus = append(nalus, session.vps)
nalus = append(nalus, session.sps)
nalus = append(nalus, session.pps)
}
}
nalus = append(nalus, nals...)
pts := time.Millisecond*time.Duration(msg.Pts()) - session.startVideoPts
frame := Frame{
ntp: time.Now(),
pts: pts,
au: nalus,
codecType: msg.VideoCodecId(),
}
session.data = append(session.data, frame)
}
}
if session.videoCodecId != -1 && session.audioCodecId != -1 {
session.drain()
return
}
if len(session.data) >= session.maxMsgSize {
session.drain()
return
}
}
func (session *HlsSession) drain() {
if session.videoCodecId != -1 {
if session.videoCodecId == int(base.RtmpCodecIdAvc) {
session.muxer.VideoTrack = &gohlslib.Track{
Codec: &codecs.H264{
SPS: session.sps,
PPS: session.pps,
},
}
} else if session.videoCodecId == int(base.RtmpCodecIdHevc) {
session.muxer.VideoTrack = &gohlslib.Track{
Codec: &codecs.H265{
VPS: session.vps,
SPS: session.sps,
PPS: session.pps,
},
}
}
}
if session.audioCodecId != -1 {
if session.audioCodecId == int(base.RtmpSoundFormatAac) {
var mpegConf mpeg4audio.Config
err := mpegConf.Unmarshal(session.asc)
if err != nil {
nazalog.Error(err)
return
}
session.muxer.AudioTrack = &gohlslib.Track{
Codec: &codecs.MPEG4Audio{
Config: mpegConf,
},
}
} else if session.audioCodecId == int(base.RtmpSoundFormatOpus) {
session.muxer.AudioTrack = &gohlslib.Track{
Codec: &codecs.Opus{
ChannelCount: 1,
},
}
}
}
if err := session.muxer.Start(); err != nil {
nazalog.Error(err)
return
}
for _, data := range session.data {
if (data.codecType == base.RtmpCodecIdAvc || data.codecType == base.RtmpCodecIdHevc) && session.videoCodecId != -1 {
err := session.muxer.WriteH26x(data.ntp, data.pts, data.au)
if err != nil {
nazalog.Error("hls-fmp4 WriteH26x failed, err:", err)
continue
}
} else if session.audioCodecId != -1 {
if data.codecType == base.RtmpSoundFormatAac {
err := session.muxer.WriteMPEG4Audio(data.ntp, data.pts, data.au)
if err != nil {
nazalog.Error("hls-fmp4 WriteMPEG4Audio failed, err:", err)
continue
}
} else if data.codecType == base.RtmpSoundFormatOpus {
err := session.muxer.WriteOpus(data.ntp, data.pts, data.au)
if err != nil {
nazalog.Error("hls-fmp4 WriteMPEG4Audio failed, err:", err)
continue
}
} else {
// gohlslib不支持g711
continue
}
}
}
session.done = true
}
func (session *HlsSession) OnStop() {
if session.done {
session.muxer.Close()
}
}
func (session *HlsSession) HandleRequest(ctx *gin.Context) {
nazalog.Info("handle hls request, appName:", session.appName, " streamName:", session.streamName, " path:", ctx.Request.URL.Path)
session.muxer.Handle(ctx.Writer, ctx.Request)
}
type Frame struct {
ntp time.Time
pts time.Duration
au [][]byte
codecType uint8
}
================================================
FILE: fmp4/http-fmp4/server.go
================================================
package httpfmp4
import (
"github.com/gin-gonic/gin"
)
type HttpFmp4Server struct {
}
func NewHttpFmp4Server() *HttpFmp4Server {
svr := &HttpFmp4Server{}
return svr
}
func (s *HttpFmp4Server) HandleRequest(c *gin.Context) {
streamid := c.Param("streamid")
appName := c.Query("app_name")
session := NewHttpFmp4Session(appName, streamid)
session.handleSession(c)
}
================================================
FILE: fmp4/http-fmp4/session.go
================================================
package httpfmp4
import (
"errors"
"net/http"
"strings"
"sync"
"time"
"github.com/q191201771/lalmax/fmp4/muxer"
maxlogic "github.com/q191201771/lalmax/logic"
"github.com/gofrs/uuid"
"github.com/q191201771/naza/pkg/connection"
"github.com/gin-gonic/gin"
"github.com/q191201771/lal/pkg/base"
"github.com/q191201771/naza/pkg/nazalog"
)
var ErrWriteChanFull = errors.New("Fmp4 Session write channel full")
var (
readBufSize = 4096 // session connection读缓冲的大小
wChanSize = 256 // session 发送数据时,channel 的大小
)
type HttpFmp4Session struct {
appName string
streamid string
group *maxlogic.Group
subscriberId string
rtmp2Fmp4Remuxer *muxer.Rtmp2Fmp4Remuxer
w gin.ResponseWriter
conn connection.Connection
disposeOnce sync.Once
log nazalog.Logger
}
func NewHttpFmp4Session(appName, streamid string) *HttpFmp4Session {
streamid = strings.TrimSuffix(streamid, ".mp4")
u, _ := uuid.NewV4()
session := &HttpFmp4Session{
appName: appName,
streamid: streamid,
subscriberId: u.String(),
log: nazalog.WithPrefix(u.String()),
}
session.rtmp2Fmp4Remuxer = muxer.NewRtmp2Fmp4Remuxer(session).WithLog(session.log)
session.log.Infof("create http fmp4 session, appName:%s, streamid:%s", appName, streamid)
return session
}
func (session *HttpFmp4Session) OnInitFmp4(init []byte) {
session.conn.Write(init)
}
func (session *HttpFmp4Session) OnFmp4Packets(currentPart *muxer.MuxerPart, lastSampleDuration time.Duration, end bool, isVideo bool) {
if currentPart != nil {
if err := currentPart.Encode(lastSampleDuration, end); err == nil {
session.conn.Write(currentPart.Bytes())
}
}
}
func (session *HttpFmp4Session) Dispose() error {
return session.dispose()
}
func (session *HttpFmp4Session) dispose() error {
var retErr error
session.disposeOnce.Do(func() {
session.OnStop()
if session.conn == nil {
retErr = base.ErrSessionNotStarted
return
}
retErr = session.conn.Close()
})
return retErr
}
func (session *HttpFmp4Session) handleSession(c *gin.Context) {
ok, group := maxlogic.GetGroupManagerInstance().GetGroup(maxlogic.NewStreamKey(session.appName, session.streamid))
if !ok {
nazalog.Errorf("stream is not found, appName:%s, streamid:%s", session.appName, session.streamid)
c.Status(http.StatusNotFound)
return
}
session.group = group
session.w = c.Writer
c.Header("Content-Type", "video/mp4")
c.Header("Connection", "close")
c.Header("Expires", "-1")
h, ok := session.w.(http.Hijacker)
if !ok {
nazalog.Error("gin response does not implement http.Hijacker")
return
}
conn, bio, err := h.Hijack()
if err != nil {
nazalog.Errorf("hijack failed. err=%+v", err)
return
}
if bio.Reader.Buffered() != 0 || bio.Writer.Buffered() != 0 {
nazalog.Errorf("hijack but buffer not empty. rb=%d, wb=%d", bio.Reader.Buffered(), bio.Writer.Buffered())
}
session.conn = connection.New(conn, func(option *connection.Option) {
option.ReadBufSize = readBufSize
option.WriteChanSize = wChanSize
})
if err = session.writeHttpHeader(session.w.Header()); err != nil {
nazalog.Errorf("session writeHttpHeader. err=%+v", err)
return
}
session.group.AddSubscriber(maxlogic.SubscriberInfo{
SubscriberID: session.subscriberId,
Protocol: maxlogic.SubscriberProtocolHTTPFMP4,
}, session)
go func() {
readBuf := make([]byte, 1024)
_, err = session.conn.Read(readBuf)
session.dispose()
}()
}
func (session *HttpFmp4Session) writeHttpHeader(header http.Header) error {
p := make([]byte, 0, 1024)
p = append(p, []byte("HTTP/1.1 200 OK\r\n")...)
for k, vs := range header {
for _, v := range vs {
p = append(p, k...)
p = append(p, ": "...)
for i := 0; i < len(v); i++ {
b := v[i]
if b <= 31 {
// prevent response splitting.
b = ' '
}
p = append(p, b)
}
p = append(p, "\r\n"...)
}
}
p = append(p, "\r\n"...)
return session.write(p)
}
func (session *HttpFmp4Session) write(buf []byte) (err error) {
if session.conn != nil {
_, err = session.conn.Write(buf)
}
return err
}
func (session *HttpFmp4Session) OnMsg(msg base.RtmpMsg) {
if session.rtmp2Fmp4Remuxer != nil {
session.rtmp2Fmp4Remuxer.FeedRtmpMessage(msg)
}
}
func (session *HttpFmp4Session) OnStop() {
if session.group != nil {
session.group.RemoveSubscriber(session.subscriberId)
}
}
func (session *HttpFmp4Session) GetSubscriberStat() maxlogic.SubscriberStat {
if session == nil || session.conn == nil {
return maxlogic.SubscriberStat{}
}
connStat := session.conn.GetStat()
stat := maxlogic.SubscriberStat{
ReadBytesSum: connStat.ReadBytesSum,
WroteBytesSum: connStat.WroteBytesSum,
}
if remoteAddr := session.conn.RemoteAddr(); remoteAddr != nil {
stat.RemoteAddr = remoteAddr.String()
}
return stat
}
================================================
FILE: fmp4/muxer/codec.go
================================================
package muxer
import (
"bytes"
"github.com/q191201771/lal/pkg/aac"
)
type Codec interface {
IsVideo() bool
Equal(other Codec) bool
String() string
}
type CodecH264 struct {
SPS []byte
PPS []byte
}
func (c *CodecH264) IsVideo() bool {
return true
}
func (c *CodecH264) Equal(other Codec) bool {
if other2, ok := other.(*CodecH264); ok {
return bytes.Equal(c.SPS, other2.SPS) && bytes.Equal(c.PPS, other2.PPS)
}
return false
}
func (c *CodecH264) String() string {
return "H264"
}
type CodecH265 struct {
SPS []byte
PPS []byte
VPS []byte
}
func (c *CodecH265) IsVideo() bool {
return true
}
func (c *CodecH265) Equal(other Codec) bool {
if other2, ok := other.(*CodecH265); ok {
return bytes.Equal(c.SPS, other2.SPS) && bytes.Equal(c.PPS, other2.PPS) && bytes.Equal(c.VPS, other2.VPS)
}
return false
}
func (c *CodecH265) String() string {
return "H265"
}
type CodecAAC struct {
Ctx *aac.AscContext
AscData []byte
}
func (c *CodecAAC) IsVideo() bool {
return false
}
func (c *CodecAAC) Equal(other Codec) bool {
if other2, ok := other.(*CodecAAC); ok {
return bytes.Equal(c.AscData, other2.AscData)
}
return false
}
func (c *CodecAAC) String() string {
return "AAC"
}
type CodecOpus struct {
ChannelCount int
}
func (c *CodecOpus) IsVideo() bool {
return false
}
func (c *CodecOpus) Equal(other Codec) bool {
return false
}
func (c *CodecOpus) String() string {
return "OPUS"
}
================================================
FILE: fmp4/muxer/file_writer.go
================================================
package muxer
import (
"fmt"
"os"
"path/filepath"
"time"
"github.com/q191201771/lal/pkg/base"
)
type Fmp4Record struct {
curfw *FileWriter
initMp4 []byte
recordInterval int
enableRecordByInterval bool
streamName string
recordPath string
curDuration float64
hasWriteInit bool
currentPart *MuxerPart
currentFileNextPartId uint64
currentFileLastPartVideoStartDts time.Duration
currentFIleLastPartAudioStartDts time.Duration
}
func NewFmp4Record(recordInterval int, enableRecordByInterval bool, streamName, recordPath string) *Fmp4Record {
r := &Fmp4Record{
recordInterval: recordInterval,
enableRecordByInterval: enableRecordByInterval,
streamName: streamName,
recordPath: recordPath,
}
if !r.enableRecordByInterval {
err := r.createFile()
if err != nil {
return nil
}
}
return r
}
func (r *Fmp4Record) createFile() (err error) {
r.curfw = &FileWriter{}
filename := fmt.Sprintf("%s-%d.mp4", r.streamName, time.Now().Unix())
filenameWithPath := filepath.Join(r.recordPath, filename)
if err := r.curfw.Create(filenameWithPath); err != nil {
Log.Errorf("[%s] record fmp4 open file failed. filename=%s, err=%+v", filenameWithPath, err)
r.curfw = nil
return err
}
return
}
func (r *Fmp4Record) WriteInitFmp4(init []byte) {
r.initMp4 = init
}
func (r *Fmp4Record) WriteFmp4Segment(part *MuxerPart, lastSampleDuration time.Duration, end bool) (err error) {
if r.enableRecordByInterval {
r.WriteMultiFile(part, lastSampleDuration, end)
} else {
r.writeSingleFile(part, lastSampleDuration, end)
}
return
}
func (r *Fmp4Record) WriteMultiFile(part *MuxerPart, lastSampleDuration time.Duration, end bool) {
if part == nil {
return
}
if r.curfw == nil {
err := r.createFile()
if err != nil {
Log.Error("create file failed, err:", err)
return
}
r.curDuration = 0
r.currentFileLastPartVideoStartDts = 0
r.currentFIleLastPartAudioStartDts = 0
r.currentFileNextPartId = 0
r.curfw.Write(r.initMp4)
}
if r.currentFileLastPartVideoStartDts == 0 {
r.currentFileLastPartVideoStartDts = part.StartVideoDts()
}
baseDecodeVideoTime := part.StartVideoDts() - r.currentFileLastPartVideoStartDts
if r.currentFIleLastPartAudioStartDts == 0 {
r.currentFIleLastPartAudioStartDts = part.StartAudioDts()
}
baseDecodeAudioTime := part.StartAudioDts() - r.currentFIleLastPartAudioStartDts
if end {
part.SetVideoStartDts(baseDecodeVideoTime)
part.SetAudioStartDts(baseDecodeAudioTime)
part.SetPartId(r.currentFileNextPartId)
part.Encode(lastSampleDuration, end)
r.curfw.Write(part.Bytes())
// 结束写文件
r.curfw.Dispose()
r.curfw = nil
} else {
curpartduration := part.CalcDuration(lastSampleDuration, end)
part.SetVideoStartDts(baseDecodeVideoTime)
part.SetAudioStartDts(baseDecodeAudioTime)
part.SetPartId(r.currentFileNextPartId)
part.Encode(lastSampleDuration, end)
r.curfw.Write(part.Bytes())
r.currentFileNextPartId++
r.curDuration += curpartduration.Seconds()
if r.curDuration >= float64(r.recordInterval) {
// 结束写文件
r.curfw.Dispose()
r.curfw = nil
}
}
}
func (r *Fmp4Record) writeSingleFile(part *MuxerPart, lastSampleDuration time.Duration, end bool) {
if r.hasWriteInit {
err := part.Encode(lastSampleDuration, false)
if err != nil {
Log.Errorf("encode muxer part failed: %v", err)
return
}
r.curfw.Write(part.Bytes())
} else {
r.curfw.Write(r.initMp4)
err := part.Encode(lastSampleDuration, false)
if err != nil {
Log.Errorf("encode muxer part failed: %v", err)
return
}
r.curfw.Write(part.Bytes())
r.hasWriteInit = true
}
}
func (r *Fmp4Record) Dispose() error {
if r.curfw != nil {
return r.curfw.Dispose()
}
return nil
}
type FileWriter struct {
fp *os.File
}
func (fw *FileWriter) Create(filename string) (err error) {
fw.fp, err = os.Create(filename)
return
}
func (fw *FileWriter) Write(b []byte) (err error) {
if fw.fp == nil {
return base.ErrFileNotExist
}
_, err = fw.fp.Write(b)
return
}
func (fw *FileWriter) Dispose() error {
if fw.fp == nil {
return base.ErrFileNotExist
}
return fw.fp.Close()
}
func (fw *FileWriter) Name() string {
if fw.fp == nil {
return ""
}
return fw.fp.Name()
}
================================================
FILE: fmp4/muxer/flac_box.go
================================================
package muxer
import (
"github.com/abema/go-mp4"
)
func BoxTypeFlac() mp4.BoxType { return mp4.StrToBoxType("fLaC") }
func init() {
mp4.AddAnyTypeBoxDef(&mp4.AudioSampleEntry{}, BoxTypeFlac())
}
/*
func BoxTypeFlac() mp4.BoxType {
return mp4.StrToBoxType("fLaC")
}
func init() {
mp4.AddBoxDef(&FlacBox{})
}
type FlacBox struct {
mp4.FullBox `mp4:"0,extend"`
}
func (f *FlacBox) GetType() mp4.BoxType {
return BoxTypeFlac()
}
*/
func BoxTypeDfla() mp4.BoxType {
return mp4.StrToBoxType("dfLa")
}
func init() {
mp4.AddBoxDef(&DflaBox{})
}
type DflaBox struct {
mp4.BaseCustomFieldObject
Data []byte `mp4:"0,size=8,dynamic"`
}
func (d *DflaBox) GetType() mp4.BoxType {
return BoxTypeDfla()
}
/*
func (d *DflaBox) GetFieldLength(name string, ctx mp4.Context) uint {
switch name {
case "NALUnit":
return uint(d.Length)
}
return 0
}
*/
// AddFlag adds the flag
func (d *DflaBox) AddFlag(uint32) {}
func (d *DflaBox) CheckFlag(uint32) bool {
return false
}
func (d *DflaBox) GetFlags() uint32 {
return 0
}
func (d *DflaBox) GetVersion() uint8 {
return 0
}
func (d *DflaBox) RemoveFlag(uint32) {
}
func (d *DflaBox) SetFlags(uint32) {
}
func (d *DflaBox) SetVersion(uint8) {
}
================================================
FILE: fmp4/muxer/init.go
================================================
package muxer
import (
"fmt"
"io"
"github.com/abema/go-mp4"
"github.com/q191201771/lal/pkg/aac"
"github.com/q191201771/lal/pkg/hevc"
)
// Specification: ISO 14496-1, Table 5
const (
objectTypeIndicationVisualISO14496part2 = 0x20
objectTypeIndicationAudioISO14496part3 = 0x40
objectTypeIndicationVisualISO1318part2Main = 0x61
objectTypeIndicationAudioISO11172part3 = 0x6B
objectTypeIndicationVisualISO10918part1 = 0x6C
)
// Specification: ISO 14496-1, Table 6
const (
streamTypeVisualStream = 0x04
streamTypeAudioStream = 0x05
)
func h265FindParams(params []mp4.HEVCNaluArray) ([]byte, []byte, []byte, error) {
var vps []byte
var sps []byte
var pps []byte
for _, arr := range params {
switch hevc.ParseNaluType(arr.NaluType) {
case hevc.NaluTypeVps, hevc.NaluTypeSps, hevc.NaluTypePps:
if arr.NumNalus != 1 {
return nil, nil, nil, fmt.Errorf("multiple VPS/SPS/PPS are not supported")
}
}
switch hevc.ParseNaluType(arr.NaluType) {
case hevc.NaluTypeVps:
vps = arr.Nalus[0].NALUnit
case hevc.NaluTypeSps:
sps = arr.Nalus[0].NALUnit
case hevc.NaluTypePps:
pps = arr.Nalus[0].NALUnit
}
}
if vps == nil {
return nil, nil, nil, fmt.Errorf("VPS not provided")
}
if sps == nil {
return nil, nil, nil, fmt.Errorf("SPS not provided")
}
if pps == nil {
return nil, nil, nil, fmt.Errorf("PPS not provided")
}
return vps, sps, pps, nil
}
func h264FindParams(avcc *mp4.AVCDecoderConfiguration) ([]byte, []byte, error) {
if len(avcc.SequenceParameterSets) > 1 {
return nil, nil, fmt.Errorf("multiple SPS are not supported")
}
var sps []byte
if len(avcc.SequenceParameterSets) == 1 {
sps = avcc.SequenceParameterSets[0].NALUnit
}
if len(avcc.PictureParameterSets) > 1 {
return nil, nil, fmt.Errorf("multiple PPS are not supported")
}
var pps []byte
if len(avcc.PictureParameterSets) == 1 {
pps = avcc.PictureParameterSets[0].NALUnit
}
return sps, pps, nil
}
func esdsFindDecoderConf(descriptors []mp4.Descriptor) *mp4.DecoderConfigDescriptor {
for _, desc := range descriptors {
if desc.Tag == mp4.DecoderConfigDescrTag {
return desc.DecoderConfigDescriptor
}
}
return nil
}
func esdsFindDecoderSpecificInfo(descriptors []mp4.Descriptor) []byte {
for _, desc := range descriptors {
if desc.Tag == mp4.DecSpecificInfoTag {
return desc.Data
}
}
return nil
}
// Init is a fMP4 initialization block.
type Init struct {
Tracks []*InitTrack
}
// Marshal encodes a fMP4 initialization file.
func (i *Init) Marshal(w io.WriteSeeker) error {
/*
|ftyp|
|moov|
| |mvhd|
| |trak|
| |trak|
| |....|
| |mvex|
| | |trex|
| | |trex|
| | |....|
*/
mw := newMP4Writer(w)
_, err := mw.writeBox(&mp4.Ftyp{ // <ftyp/>
MajorBrand: [4]byte{'m', 'p', '4', '2'},
MinorVersion: 1,
CompatibleBrands: []mp4.CompatibleBrandElem{
{CompatibleBrand: [4]byte{'m', 'p', '4', '1'}},
{CompatibleBrand: [4]byte{'m', 'p', '4', '2'}},
{CompatibleBrand: [4]byte{'i', 's', 'o', 'm'}},
{CompatibleBrand: [4]byte{'h', 'l', 's', 'f'}},
},
})
if err != nil {
return err
}
_, err = mw.writeBoxStart(&mp4.Moov{}) // <moov>
if err != nil {
return err
}
_, err = mw.writeBox(&mp4.Mvhd{ // <mvhd/>
Timescale: 1000,
Rate: 65536,
Volume: 256,
Matrix: [9]int32{0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000},
NextTrackID: 4294967295,
})
if err != nil {
return err
}
for _, track := range i.Tracks {
err = track.marshal(mw)
if err != nil {
return err
}
}
_, err = mw.writeBoxStart(&mp4.Mvex{}) // <mvex>
if err != nil {
return err
}
for _, track := range i.Tracks {
_, err = mw.writeBox(&mp4.Trex{ // <trex/>
TrackID: uint32(track.ID),
DefaultSampleDescriptionIndex: 1,
})
if err != nil {
return err
}
}
err = mw.writeBoxEnd() // </mvex>
if err != nil {
return err
}
err = mw.writeBoxEnd() // </moov>
if err != nil {
return err
}
return nil
}
// Unmarshal decodes a fMP4 initialization block.
func (i *Init) Unmarshal(r io.ReadSeeker) error {
type readState int
const (
waitingTrak readState = iota
waitingTkhd
waitingMdhd
waitingCodec
waitingAv1C
waitingVpcC
waitingHvcC
waitingAvcC
waitingVideoEsds
waitingAudioEsds
waitingDOps
waitingDac3
waitingPcmC
)
state := waitingTrak
var curTrack *InitTrack
/*
var width int
var height int
var sampleRate int
var channelCount int
*/
_, err := mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) {
if !h.BoxInfo.IsSupportedType() {
if state != waitingTrak {
i.Tracks = i.Tracks[:len(i.Tracks)-1]
state = waitingTrak
}
} else {
switch h.BoxInfo.Type.String() {
case "moov":
return h.Expand()
case "trak":
if state != waitingTrak {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
curTrack = &InitTrack{}
i.Tracks = append(i.Tracks, curTrack)
state = waitingTkhd
return h.Expand()
case "tkhd":
if state != waitingTkhd {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
tkhd := box.(*mp4.Tkhd)
curTrack.ID = int(tkhd.TrackID)
state = waitingMdhd
case "mdia":
return h.Expand()
case "mdhd":
if state != waitingMdhd {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
mdhd := box.(*mp4.Mdhd)
curTrack.TimeScale = mdhd.Timescale
state = waitingCodec
case "minf", "stbl", "stsd":
return h.Expand()
case "avc1":
if state != waitingCodec {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
state = waitingAvcC
return h.Expand()
case "avcC":
if state != waitingAvcC {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
avcc := box.(*mp4.AVCDecoderConfiguration)
sps, pps, err := h264FindParams(avcc)
if err != nil {
return nil, err
}
curTrack.Codec = &CodecH264{
SPS: sps,
PPS: pps,
}
state = waitingTrak
/*
case "vp09":
if state != waitingCodec {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
vp09 := box.(*mp4.VisualSampleEntry)
width = int(vp09.Width)
height = int(vp09.Height)
state = waitingVpcC
return h.Expand()
*/
/*
case "vpcC":
if state != waitingVpcC {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
vpcc := box.(*mp4.VpcC)
curTrack.Codec = &CodecVP9{
Width: width,
Height: height,
Profile: vpcc.Profile,
BitDepth: vpcc.BitDepth,
ChromaSubsampling: vpcc.ChromaSubsampling,
ColorRange: vpcc.VideoFullRangeFlag != 0,
}
state = waitingTrak
*/
case "vp08": // VP8, not supported yet
return nil, nil
case "hev1", "hvc1":
if state != waitingCodec {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
state = waitingHvcC
return h.Expand()
case "hvcC":
if state != waitingHvcC {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
hvcc := box.(*mp4.HvcC)
vps, sps, pps, err := h265FindParams(hvcc.NaluArrays)
if err != nil {
return nil, err
}
curTrack.Codec = &CodecH265{
VPS: vps,
SPS: sps,
PPS: pps,
}
state = waitingTrak
case "av01":
if state != waitingCodec {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
state = waitingAv1C
return h.Expand()
case "Opus":
if state != waitingCodec {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
state = waitingDOps
return h.Expand()
case "dOps":
if state != waitingDOps {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
dops := box.(*mp4.DOps)
curTrack.Codec = &CodecOpus{
ChannelCount: int(dops.OutputChannelCount),
}
state = waitingTrak
case "mp4v":
if state != waitingCodec {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
mp4v := box.(*mp4.VisualSampleEntry)
width := int(mp4v.Width)
height := int(mp4v.Height)
Log.Info("width:", width, " height:", height)
state = waitingVideoEsds
return h.Expand()
case "mp4a":
if state != waitingCodec {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
mp4a := box.(*mp4.AudioSampleEntry)
sampleRate := int(mp4a.SampleRate / 65536)
channelCount := int(mp4a.ChannelCount)
Log.Info("sampleRate:", sampleRate, " channelCount:", channelCount)
state = waitingAudioEsds
return h.Expand()
case "esds":
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
esds := box.(*mp4.Esds)
conf := esdsFindDecoderConf(esds.Descriptors)
if conf == nil {
return nil, fmt.Errorf("unable to find decoder config")
}
switch state {
case waitingVideoEsds:
switch conf.ObjectTypeIndication {
case objectTypeIndicationVisualISO14496part2:
spec := esdsFindDecoderSpecificInfo(esds.Descriptors)
if spec == nil {
return nil, fmt.Errorf("unable to find decoder specific info")
}
/*
curTrack.Codec = &CodecMPEG4Video{
Config: spec,
}
*/
case objectTypeIndicationVisualISO1318part2Main:
spec := esdsFindDecoderSpecificInfo(esds.Descriptors)
if spec == nil {
return nil, fmt.Errorf("unable to find decoder specific info")
}
/*
curTrack.Codec = &CodecMPEG1Video{
Config: spec,
}
*/
case objectTypeIndicationVisualISO10918part1:
/*
curTrack.Codec = &CodecMJPEG{
Width: width,
Height: height,
}
*/
default:
return nil, fmt.Errorf("unsupported object type indication: 0x%.2x", conf.ObjectTypeIndication)
}
state = waitingTrak
case waitingAudioEsds:
switch conf.ObjectTypeIndication {
case objectTypeIndicationAudioISO14496part3:
spec := esdsFindDecoderSpecificInfo(esds.Descriptors)
if spec == nil {
return nil, fmt.Errorf("unable to find decoder specific info")
}
ascCtx, err := aac.NewAscContext(spec)
if err != nil {
return nil, fmt.Errorf("NewAscContext failed, err: %w", err)
}
curTrack.Codec = &CodecAAC{
Ctx: ascCtx,
AscData: spec,
}
case objectTypeIndicationAudioISO11172part3:
/*
curTrack.Codec = &CodecMPEG1Audio{
SampleRate: sampleRate,
ChannelCount: channelCount,
}
*/
default:
return nil, fmt.Errorf("unsupported object type indication: 0x%.2x", conf.ObjectTypeIndication)
}
default:
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
state = waitingTrak
case "ac-3":
if state != waitingCodec {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
ac3 := box.(*mp4.AudioSampleEntry)
sampleRate := int(ac3.SampleRate / 65536)
channelCount := int(ac3.ChannelCount)
Log.Info("sampleRate:", sampleRate, " channelCount:", channelCount)
state = waitingDac3
return h.Expand()
case "dac3":
if state != waitingDac3 {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
/*
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
dac3 := box.(*mp4.Dac3)
curTrack.Codec = &C{
SampleRate: sampleRate,
ChannelCount: channelCount,
Fscod: dac3.Fscod,
Bsid: dac3.Bsid,
Bsmod: dac3.Bsmod,
Acmod: dac3.Acmod,
LfeOn: dac3.LfeOn != 0,
BitRateCode: dac3.BitRateCode,
}
*/
state = waitingTrak
case "ipcm":
if state != waitingCodec {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
ac3 := box.(*mp4.AudioSampleEntry)
sampleRate := int(ac3.SampleRate / 65536)
channelCount := int(ac3.ChannelCount)
Log.Info("sampleRate:", sampleRate, " channelCount:", channelCount)
state = waitingPcmC
return h.Expand()
/*
case "pcmC":
if state != waitingPcmC {
return nil, fmt.Errorf("unexpected box '%v'", h.BoxInfo.Type)
}
box, _, err := h.ReadPayload()
if err != nil {
return nil, err
}
pcmc := box.(*mp4.PcmC)
curTrack.Codec = &CodecLPCM{
LittleEndian: (pcmc.FormatFlags & 0x01) != 0,
BitDepth: int(pcmc.PCMSampleSize),
SampleRate: sampleRate,
ChannelCount: channelCount,
}
state = waitingTrak
*/
}
}
return nil, nil
})
if err != nil {
return err
}
if state != waitingTrak {
return fmt.Errorf("parse error")
}
if len(i.Tracks) == 0 {
return fmt.Errorf("no tracks found")
}
return nil
}
================================================
FILE: fmp4/muxer/init_track.go
================================================
package muxer
import (
"fmt"
"github.com/abema/go-mp4"
"github.com/bluenviron/mediacommon/pkg/codecs/h265"
"github.com/q191201771/lal/pkg/avc"
"github.com/q191201771/lal/pkg/hevc"
)
func boolToUint8(v bool) uint8 {
if v {
return 1
}
return 0
}
// InitTrack is a track of Init.
type InitTrack struct {
// ID, starts from 1.
ID int
// time scale.
TimeScale uint32
// maximum bitrate.
// it defaults to 1MB for video tracks, 128k for audio tracks.
MaxBitrate uint32
// average bitrate.
// it defaults to 1MB for video tracks, 128k for audio tracks.
AvgBitrate uint32
// codec.
Codec Codec
}
func (it *InitTrack) marshal(w *mp4Writer) error {
/*
|trak|
| |tkhd|
| |mdia|
| | |mdhd|
| | |hdlr|
| | |minf|
| | | |vmhd| (video)
| | | |smhd| (audio)
| | | |dinf|
| | | | |dref|
| | | | | |url|
| | | |stbl|
| | | | |stsd|
| | | | | |av01| (AV1)
| | | | | | |av1C|
| | | | | | |btrt|
| | | | | |vp09| (VP9)
| | | | | | |vpcC|
| | | | | | |btrt|
| | | | | |hev1| (H265)
| | | | | | |hvcC|
| | | | | | |btrt|
| | | | | |avc1| (H264)
| | | | | | |avcC|
| | | | | | |btrt|
| | | | | |mp4v| (MPEG-4/2/1 video, MJPEG)
| | | | | | |esds|
| | | | | | |btrt|
| | | | | |Opus| (Opus)
| | | | | | |dOps|
| | | | | | |btrt|
| | | | | |mp4a| (MPEG-4/1 audio)
| | | | | | |esds|
| | | | | | |btrt|
| | | | | |ac-3| (AC-3)
| | | | | | |dac3|
| | | | | | |btrt|
| | | | | |ipcm| (LPCM)
| | | | | | |pcmC|
| | | | | | |btrt|
| | | | | |fLaC| (FLAC)
| | | | |stts|
| | | | |stsc|
| | | | |stsz|
| | | | |stco|
*/
var width int
var height int
_, err := w.writeBoxStart(&mp4.Trak{}) // <trak>
if err != nil {
return err
}
if it.Codec == nil {
return fmt.Errorf("codec is not for track")
}
switch codec := it.Codec.(type) {
case *CodecH264:
if len(codec.SPS) == 0 || len(codec.PPS) == 0 {
return fmt.Errorf("H264 parameters not provided")
}
var ctx avc.Context
err = avc.ParseSps(codec.SPS, &ctx)
if err != nil {
return fmt.Errorf("h264 parse sps failed")
}
width = int(ctx.Width)
height = int(ctx.Height)
case *CodecH265:
if len(codec.SPS) == 0 || len(codec.PPS) == 0 || len(codec.VPS) == 0 {
return fmt.Errorf("H265 parameters not provided")
}
var ctx hevc.Context
err = hevc.ParseSps(codec.SPS, &ctx)
if err != nil {
return fmt.Errorf("hevc parse sps failed")
}
width = int(ctx.PicWidthInLumaSamples)
height = int(ctx.PicHeightInLumaSamples)
}
if it.Codec == nil {
return nil
}
if it.Codec.IsVideo() {
_, err = w.writeBox(&mp4.Tkhd{ // <tkhd/>
FullBox: mp4.FullBox{
Flags: [3]byte{0, 0, 3},
},
TrackID: uint32(it.ID),
Width: uint32(width * 65536),
Height: uint32(height * 65536),
Matrix: [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000},
})
if err != nil {
return err
}
} else {
_, err = w.writeBox(&mp4.Tkhd{ // <tkhd/>
FullBox: mp4.FullBox{
Flags: [3]byte{0, 0, 3},
},
TrackID: uint32(it.ID),
AlternateGroup: 1,
Volume: 256,
Matrix: [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000},
})
if err != nil {
return err
}
}
_, err = w.writeBoxStart(&mp4.Mdia{}) // <mdia>
if err != nil {
return err
}
_, err = w.writeBox(&mp4.Mdhd{ // <mdhd/>
Timescale: it.TimeScale,
Language: [3]byte{'u', 'n', 'd'},
})
if err != nil {
return err
}
if it.Codec.IsVideo() {
_, err = w.writeBox(&mp4.Hdlr{ // <hdlr/>
HandlerType: [4]byte{'v', 'i', 'd', 'e'},
Name: "VideoHandler",
})
if err != nil {
return err
}
} else {
_, err = w.writeBox(&mp4.Hdlr{ // <hdlr/>
HandlerType: [4]byte{'s', 'o', 'u', 'n'},
Name: "SoundHandler",
})
if err != nil {
return err
}
}
_, err = w.writeBoxStart(&mp4.Minf{}) // <minf>
if err != nil {
return err
}
if it.Codec.IsVideo() {
_, err = w.writeBox(&mp4.Vmhd{ // <vmhd/>
FullBox: mp4.FullBox{
Flags: [3]byte{0, 0, 1},
},
})
if err != nil {
return err
}
} else {
_, err = w.writeBox(&mp4.Smhd{}) // <smhd/>
if err != nil {
return err
}
}
_, err = w.writeBoxStart(&mp4.Dinf{}) // <dinf>
if err != nil {
return err
}
_, err = w.writeBoxStart(&mp4.Dref{ // <dref>
EntryCount: 1,
})
if err != nil {
return err
}
_, err = w.writeBox(&mp4.Url{ // <url/>
FullBox: mp4.FullBox{
Flags: [3]byte{0, 0, 1},
},
})
if err != nil {
return err
}
err = w.writeBoxEnd() // </dref>
if err != nil {
return err
}
err = w.writeBoxEnd() // </dinf>
if err != nil {
return err
}
_, err = w.writeBoxStart(&mp4.Stbl{}) // <stbl>
if err != nil {
return err
}
_, err = w.writeBoxStart(&mp4.Stsd{ // <stsd>
EntryCount: 1,
})
if err != nil {
return err
}
maxBitrate := it.MaxBitrate
if maxBitrate == 0 {
if it.Codec.IsVideo() {
maxBitrate = 1000000
} else {
maxBitrate = 128825
}
}
avgBitrate := it.AvgBitrate
if avgBitrate == 0 {
if it.Codec.IsVideo() {
avgBitrate = 1000000
} else {
avgBitrate = 128825
}
}
switch codec := it.Codec.(type) {
case *CodecH264:
_, err = w.writeBoxStart(&mp4.VisualSampleEntry{ // <avc1>
SampleEntry: mp4.SampleEntry{
AnyTypeBox: mp4.AnyTypeBox{
Type: mp4.BoxTypeAvc1(),
},
DataReferenceIndex: 1,
},
Width: uint16(width),
Height: uint16(height),
Horizresolution: 4718592,
Vertresolution: 4718592,
FrameCount: 1,
Depth: 24,
PreDefined3: -1,
})
if err != nil {
return err
}
var ctx avc.Context
err = avc.ParseSps(codec.SPS, &ctx)
if err != nil {
return fmt.Errorf("h264 parse sps failed")
}
_, err = w.writeBox(&mp4.AVCDecoderConfiguration{ // <avcc/>
AnyTypeBox: mp4.AnyTypeBox{
Type: mp4.BoxTypeAvcC(),
},
ConfigurationVersion: 1,
Profile: ctx.Profile,
ProfileCompatibility: codec.SPS[2],
Level: ctx.Level,
LengthSizeMinusOne: 3,
NumOfSequenceParameterSets: 1,
SequenceParameterSets: []mp4.AVCParameterSet{
{
Length: uint16(len(codec.SPS)),
NALUnit: codec.SPS,
},
},
NumOfPictureParameterSets: 1,
PictureParameterSets: []mp4.AVCParameterSet{
{
Length: uint16(len(codec.PPS)),
NALUnit: codec.PPS,
},
},
})
if err != nil {
return err
}
case *CodecH265:
_, err = w.writeBoxStart(&mp4.VisualSampleEntry{ // <hev1>
SampleEntry: mp4.SampleEntry{
AnyTypeBox: mp4.AnyTypeBox{
Type: mp4.BoxTypeHev1(),
},
DataReferenceIndex: 1,
},
Width: uint16(width),
Height: uint16(height),
Horizresolution: 4718592,
Vertresolution: 4718592,
FrameCount: 1,
Depth: 24,
PreDefined3: -1,
})
if err != nil {
return err
}
var ctx hevc.Context
err = hevc.ParseSps(codec.SPS, &ctx)
if err != nil {
return fmt.Errorf("hevc parse sps failed")
}
_, err = w.writeBox(&mp4.HvcC{ // <hvcC/>
ConfigurationVersion: 1,
GeneralProfileIdc: ctx.GeneralProfileIdc,
GeneralProfileCompatibility: Uint32ToBoolSlice(ctx.GeneralProfileCompatibilityFlags),
GeneralConstraintIndicator: [6]uint8{
codec.SPS[7], codec.SPS[8], codec.SPS[9],
codec.SPS[10], codec.SPS[11], codec.SPS[12],
},
GeneralLevelIdc: ctx.GeneralLevelIdc,
// MinSpatialSegmentationIdc
// ParallelismType
ChromaFormatIdc: uint8(ctx.ChromaFormat),
BitDepthLumaMinus8: uint8(ctx.BitDepthLumaMinus8),
BitDepthChromaMinus8: uint8(ctx.BitDepthChromaMinus8),
// AvgFrameRate
// ConstantFrameRate
NumTemporalLayers: 1,
// TemporalIdNested
LengthSizeMinusOne: 3,
NumOfNaluArrays: 3,
NaluArrays: []mp4.HEVCNaluArray{
{
NaluType: byte(h265.NALUType_VPS_NUT),
NumNalus: 1,
Nalus: []mp4.HEVCNalu{{
Length: uint16(len(codec.VPS)),
NALUnit: codec.VPS,
}},
},
{
NaluType: byte(h265.NALUType_SPS_NUT),
NumNalus: 1,
Nalus: []mp4.HEVCNalu{{
Length: uint16(len(codec.SPS)),
NALUnit: codec.SPS,
}},
},
{
NaluType: byte(h265.NALUType_PPS_NUT),
NumNalus: 1,
Nalus: []mp4.HEVCNalu{{
Length: uint16(len(codec.PPS)),
NALUnit: codec.PPS,
}},
},
},
})
if err != nil {
return err
}
case *CodecAAC:
sampleRate, _ := codec.Ctx.GetSamplingFrequency()
_, err = w.writeBoxStart(&mp4.AudioSampleEntry{ // <mp4a>
SampleEntry: mp4.SampleEntry{
AnyTypeBox: mp4.AnyTypeBox{
Type: mp4.BoxTypeMp4a(),
},
DataReferenceIndex: 1,
},
ChannelCount: uint16(codec.Ctx.ChannelConfiguration),
SampleSize: 16,
SampleRate: uint32(sampleRate * 65536),
})
if err != nil {
return err
}
_, err = w.writeBox(&mp4.Esds{ // <esds/>
Descriptors: []mp4.Descriptor{
{
Tag: mp4.ESDescrTag,
Size: 32 + uint32(len(codec.AscData)),
ESDescriptor: &mp4.ESDescriptor{
ESID: uint16(it.ID),
},
},
{
Tag: mp4.DecoderConfigDescrTag,
Size: 18 + uint32(len(codec.Ctx.Pack())),
DecoderConfigDescriptor: &mp4.DecoderConfigDescriptor{
ObjectTypeIndication: objectTypeIndicationAudioISO14496part3,
StreamType: streamTypeAudioStream,
Reserved: true,
MaxBitrate: maxBitrate,
AvgBitrate: avgBitrate,
},
},
{
Tag: mp4.DecSpecificInfoTag,
Size: uint32(len(codec.AscData)),
Data: codec.AscData,
},
{
Tag: mp4.SLConfigDescrTag,
Size: 1,
Data: []byte{0x02},
},
},
})
if err != nil {
return err
}
case *CodecOpus:
_, err = w.writeBoxStart(&mp4.AudioSampleEntry{ // <Opus>
SampleEntry: mp4.SampleEntry{
AnyTypeBox: mp4.AnyTypeBox{
Type: mp4.BoxTypeOpus(),
},
DataReferenceIndex: 1,
},
ChannelCount: uint16(codec.ChannelCount),
SampleSize: 16,
SampleRate: 48000 * 65536,
})
if err != nil {
return err
}
_, err = w.writeBox(&mp4.DOps{ // <dOps/>
OutputChannelCount: uint8(codec.ChannelCount),
PreSkip: 312,
InputSampleRate: 48000,
})
if err != nil {
return err
}
}
_, err = w.writeBox(&mp4.Btrt{ // <btrt/>
MaxBitrate: maxBitrate,
AvgBitrate: avgBitrate,
})
if err != nil {
return err
}
err = w.writeBoxEnd() // </*>
if err != nil {
return err
}
err = w.writeBoxEnd() // </stsd>
if err != nil {
return err
}
_, err = w.writeBox(&mp4.Stts{ // <stts/>
})
if err != nil {
return err
}
_, err = w.writeBox(&mp4.Stsc{ // <stsc/>
})
if err != nil {
return err
}
_, err = w.writeBox(&mp4.Stsz{ // <stsz/>
})
if err != nil {
return err
}
_, err = w.writeBox(&mp4.Stco{ // <stco/>
})
if err != nil {
return err
}
err = w.writeBoxEnd() // </stbl>
if err != nil {
return err
}
err = w.writeBoxEnd() // </minf>
if err != nil {
return err
}
err = w.writeBoxEnd() // </mdia>
if err != nil {
return err
}
err = w.writeBoxEnd() // </trak>
if err != nil {
return err
}
return nil
}
func Uint32ToBoolSlice(num uint32) [32]bool {
var boolSlice [32]bool
for i := 0; i < 32; i++ {
boolSlice[i] = num&(1<<i) != 0
}
return boolSlice
}
================================================
FILE: fmp4/muxer/mp4_writer.go
================================================
package muxer
import (
"io"
"github.com/abema/go-mp4"
)
type mp4Writer struct {
w *mp4.Writer
}
func newMP4Writer(w io.WriteSeeker) *mp4Writer {
return &mp4Writer{
w: mp4.NewWriter(w),
}
}
func (w *mp4Writer) writeBoxStart(box mp4.IImmutableBox) (int, error) {
bi := &mp4.BoxInfo{
Type: box.GetType(),
}
var err error
bi, err = w.w.StartBox(bi)
if err != nil {
return 0, err
}
_, err = mp4.Marshal(w.w, box, mp4.Context{})
if err != nil {
return 0, err
}
return int(bi.Offset), nil
}
func (w *mp4Writer) writeBoxEnd() error {
_, err := w.w.EndBox()
return err
}
func (w *mp4Writer) writeBox(box mp4.IImmutableBox) (int, error) {
off, err := w.writeBoxStart(box)
if err != nil {
return 0, err
}
err = w.writeBoxEnd()
if err != nil {
return 0, err
}
return off, nil
}
func (w *mp4Writer) rewriteBox(off int, box mp4.IImmutableBox) error {
prevOff, err := w.w.Seek(0, io.SeekCurrent)
if err != nil {
return err
}
_, err = w.w.Seek(int64(off), io.SeekStart)
if err != nil {
return err
}
_, err = w.writeBoxStart(box)
if err != nil {
return err
}
err = w.writeBoxEnd()
if err != nil {
return err
}
_, err = w.w.Seek(prevOff, io.SeekStart)
if err != nil {
return err
}
return nil
}
================================================
FILE: fmp4/muxer/muxer.go
================================================
package muxer
import (
"fmt"
"time"
"github.com/q191201771/lal/pkg/avc"
"github.com/q191201771/lal/pkg/base"
"github.com/q191201771/lalmax/utils"
"github.com/q191201771/naza/pkg/nazalog"
)
func AudioTimeScale(c Codec) uint32 {
switch codec := c.(type) {
case *CodecAAC:
samplerate, _ := codec.Ctx.GetSamplingFrequency()
return uint32(samplerate)
case *CodecOpus:
return 48000
}
return 0
}
func TsToTime(ts uint32) time.Duration {
return time.Millisecond * time.Duration(ts)
}
type Muxer struct {
VideoTrack *Track
AudioTrack *Track
nextTrackId uint32
initFmp4 []byte
hasinitVideo bool
hasinitAudio bool
vps, sps, pps []byte
auidoTimeScale uint32
lastVideoDts time.Duration
lastAudioDts time.Duration
log nazalog.Logger
VideoDtsDecoder *utils.DtsDecoder
AudioDtsDecoder *utils.DtsDecoder
}
func NewMuxer() *Muxer {
return &Muxer{
nextTrackId: 1,
log: Log,
}
}
func (m *Muxer) WithLog(log nazalog.Logger) {
m.log = log
}
func (m *Muxer) AddVideoTrack(c Codec) {
switch codec := c.(type) {
case *CodecH264:
m.sps = codec.SPS
m.pps = codec.PPS
case *CodecH265:
m.vps = codec.VPS
m.sps = codec.SPS
m.pps = codec.PPS
default:
m.log.Errorf("invalid video codec")
return
}
m.VideoTrack = NewTrack(c, m.nextTrackId, 90000)
m.nextTrackId++
}
func (m *Muxer) AddAudioTrack(c Codec) {
m.auidoTimeScale = AudioTimeScale(c)
m.AudioTrack = NewTrack(c, m.nextTrackId, m.auidoTimeScale)
m.nextTrackId++
}
func (m *Muxer) AudioTimeScale() uint32 {
return m.auidoTimeScale
}
func (m *Muxer) GetInitMp4() []byte {
if m.initFmp4 == nil {
init := &Init{}
if m.VideoTrack != nil {
init.Tracks = append(init.Tracks, &InitTrack{
ID: int(m.VideoTrack.TrackId),
TimeScale: 90000,
Codec: m.VideoTrack.Codec,
})
}
if m.AudioTrack != nil {
init.Tracks = append(init.Tracks, &InitTrack{
ID: int(m.AudioTrack.TrackId),
TimeScale: m.auidoTimeScale,
Codec: m.AudioTrack.Codec,
})
}
var w Buffer
err := init.Marshal(&w)
if err != nil {
m.log.Errorf("marshal init fmp4 failed: %v", err)
return nil
}
m.initFmp4 = w.Bytes()
}
return m.initFmp4
}
func (m *Muxer) Pack(msg base.RtmpMsg) (*PartSample, error) {
if msg.Header.MsgTypeId == base.RtmpTypeIdVideo && !msg.IsVideoKeySeqHeader() {
return m.FeedVideo(msg)
} else if msg.Header.MsgTypeId == base.RtmpTypeIdAudio && !msg.IsAacSeqHeader() {
return m.FeedAudio(msg)
}
return nil, fmt.Errorf("invalid msg type")
}
func (m *Muxer) FeedVideo(msg base.RtmpMsg) (*PartSample, error) {
if m.VideoTrack == nil {
return nil, fmt.Errorf("no video track")
}
randomAccess := false
var nalus [][]byte
if msg.IsVideoKeySeqHeader() {
return nil, fmt.Errorf("msg is video key seq header")
}
if !m.hasinitVideo {
if !msg.IsVideoKeyNalu() {
return nil, fmt.Errorf("first video require key frame")
}
}
var sample *PartSample
if m.VideoDtsDecoder == nil {
m.VideoDtsDecoder = utils.NewDtsDecoder(0, 90000, msg.Dts())
}
dts := m.VideoDtsDecoder.Decode(msg.Dts())
if !m.hasinitVideo {
m.lastVideoDts = dts
m.hasinitVideo = true
}
sample_duration := uint32(durationGoToMp4(dts-m.lastVideoDts, 90000))
switch msg.VideoCodecId() {
case base.RtmpCodecIdAvc:
nals, _ := avc.SplitNaluAvcc(msg.Payload[5:])
if msg.IsAvcKeyNalu() {
randomAccess = true
nalus = append(nalus, m.sps)
nalus = append(nalus, m.pps)
nalus = append(nalus, nals...)
} else {
nalus = append(nalus, nals...)
}
sample = NewPartSampleH26x(int32(durationGoToMp4(TsToTime(msg.Cts()), 90000)), randomAccess, nalus, sample_duration, dts)
case base.RtmpCodecIdHevc:
var nals [][]byte
if msg.IsEnchanedHevcNalu() {
index := msg.GetEnchanedHevcNaluIndex()
nals, _ = avc.SplitNaluAvcc(msg.Payload[index:])
} else {
nals, _ = avc.SplitNaluAvcc(msg.Payload[5:])
}
if msg.IsHevcKeyNalu() {
randomAccess = true
nalus = append(nalus, m.vps)
nalus = append(nalus, m.sps)
nalus = append(nalus, m.pps)
nalus = append(nalus, nals...)
} else {
nalus = append(nalus, nals...)
}
sample = NewPartSampleH26x(int32(durationGoToMp4(TsToTime(msg.Cts()), 90000)), randomAccess, nalus, sample_duration, dts)
default:
return nil, fmt.Errorf("invalid video codec id: %d", msg.VideoCodecId())
}
m.lastVideoDts = dts
return sample, nil
}
func (m *Muxer) FeedAudio(msg base.RtmpMsg) (*PartSample, error) {
if m.AudioTrack == nil {
return nil, fmt.Errorf("no audio track")
}
if m.AudioDtsDecoder == nil {
m.AudioDtsDecoder = utils.NewDtsDecoder(0, time.Duration(m.auidoTimeScale), msg.Dts())
}
dts := m.AudioDtsDecoder.Decode(msg.Dts())
if !m.hasinitAudio {
m.lastAudioDts = dts
m.hasinitAudio = true
}
sample_duration := uint32(durationGoToMp4(dts-m.lastAudioDts, m.auidoTimeScale))
var payload []byte
switch msg.AudioCodecId() {
case base.RtmpSoundFormatAac:
payload = msg.Payload[2:]
case base.RtmpSoundFormatOpus:
payload = msg.Payload[5:]
default:
return nil, fmt.Errorf("invalid audio codec id: %d", msg.AudioCodecId())
}
sample := &PartSample{
Dts: dts,
Duration: sample_duration,
IsNonSyncSample: true,
PTSOffset: 0,
Payload: payload,
}
m.lastAudioDts = dts
return sample, nil
}
================================================
FILE: fmp4/muxer/muxer_part.go
================================================
package muxer
import "time"
func durationGoToMp4(v time.Duration, timeScale uint32) uint64 {
timeScale64 := uint64(timeScale)
secs := v / time.Second
dec := v % time.Second
return uint64(secs)*timeScale64 + uint64(dec)*timeScale64/uint64(time.Second)
}
func durationMp4ToGo(v uint64, timeScale uint32) time.Duration {
timeScale64 := uint64(timeScale)
secs := v / timeScale64
dec := v % timeScale64
return time.Duration(secs)*time.Second + time.Duration(dec)*time.Second/time.Duration(timeScale64)
}
type MuxerPart struct {
VideoSamples []*PartSample
AudioSamples []*PartSample
audioTimeScale uint32
videoStartDTSFilled bool
videoStartDTS time.Duration
audioStartDTSFilled bool
audioStartDTS time.Duration
buffer *Buffer
partId uint64
partDuration time.Duration
videoPartDuration time.Duration
audioPartDuration time.Duration
}
func NewMuxerPart(partId uint64, audioTimeScale uint32) *MuxerPart {
return &MuxerPart{
buffer: &Buffer{},
partId: partId,
audioTimeScale: audioTimeScale,
}
}
func (p *MuxerPart) Bytes() []byte {
return p.buffer.Bytes()
}
func (p *MuxerPart) Duration() time.Duration {
return p.partDuration
}
func (p *MuxerPart) AudioTimeScale() uint32 {
return p.audioTimeScale
}
func (p *MuxerPart) Encode(lastSampleDuration time.Duration, end bool) error {
part := Part{
SequenceNumber: uint32(p.partId),
}
if p.VideoSamples != nil {
part.Tracks = append(part.Tracks, &PartTrack{
ID: 1,
BaseTime: durationGoToMp4(p.videoStartDTS, 90000),
Samples: p.VideoSamples,
})
}
if p.AudioSamples != nil {
part.Tracks = append(part.Tracks, &PartTrack{
ID: 1 + len(part.Tracks),
BaseTime: durationGoToMp4(p.audioStartDTS, p.audioTimeScale),
Samples: p.AudioSamples,
})
}
err := part.Marshal(p.buffer)
if err != nil {
return err
}
if !end {
if p.VideoSamples != nil {
p.partDuration = lastSampleDuration - p.videoStartDTS
} else {
p.partDuration = lastSampleDuration - p.audioStartDTS
}
} else {
if p.VideoSamples != nil {
p.partDuration = p.videoPartDuration
} else {
p.partDuration = p.audioPartDuration
}
}
p.VideoSamples = nil
p.AudioSamples = nil
return nil
}
func (p *MuxerPart) WriteVideo(sample *PartSample) {
if !p.videoStartDTSFilled {
p.videoStartDTSFilled = true
p.videoStartDTS = sample.Dts
}
p.videoPartDuration = sample.Dts - p.videoStartDTS
p.VideoSamples = append(p.VideoSamples, sample)
}
func (p *MuxerPart) WriteAudio(sample *PartSample) {
if !p.audioStartDTSFilled {
p.audioStartDTSFilled = true
p.audioStartDTS = sample.Dts
}
p.audioPartDuration = sample.Dts - p.audioStartDTS
p.AudioSamples = append(p.AudioSamples, sample)
}
func (p *MuxerPart) StartVideoDts() time.Duration {
return p.videoStartDTS
}
func (p *MuxerPart) StartAudioDts() time.Duration {
return p.audioStartDTS
}
func (p *MuxerPart) ResetStartVideoDts() {
p.videoStartDTS = 0
}
func (p *MuxerPart) ResetStartAudioDts() {
p.audioStartDTS = 0
}
func (p *MuxerPart) Clone() *MuxerPart {
clone := *p
clone.buffer = &Buffer{}
return &clone
}
func (p *MuxerPart) SetPartId(partId uint64) {
p.partId = partId
}
func (p *MuxerPart) CalcDuration(newPartStartDts time.Duration, end bool) (partDuration time.Duration) {
if !end {
if p.VideoSamples != nil {
partDuration = newPartStartDts - p.videoStartDTS
} else {
partDuration = newPartStartDts - p.audioStartDTS
}
} else {
if p.VideoSamples != nil {
partDuration = p.videoPartDuration
} else {
partDuration = p.audioPartDuration
}
}
return partDuration
}
func (p *MuxerPart) SetVideoStartDts(videoStartDTS time.Duration) {
p.videoStartDTS = videoStartDTS
}
func (p *MuxerPart) SetAudioStartDts(audioStartDTS time.Duration) {
p.audioStartDTS = audioStartDTS
}
================================================
FILE: fmp4/muxer/part.go
================================================
package muxer
import (
"io"
"github.com/abema/go-mp4"
)
const (
trunFlagDataOffsetPreset = 0x01
trunFlagSampleDurationPresent = 0x100
trunFlagSampleSizePresent = 0x200
trunFlagSampleFlagsPresent = 0x400
trunFlagSampleCompositionTimeOffsetPresentOrV1 = 0x800
sampleFlagIsNonSyncSample = 1 << 16
)
// Part is a fMP4 part.
type Part struct {
SequenceNumber uint32
Tracks []*PartTrack
}
// Marshal encodes a fMP4 part.
func (p *Part) Marshal(w io.WriteSeeker) error {
/*
|moof|
| |mfhd|
| |traf|
| |traf|
| |....|
|mdat|
*/
mw := newMP4Writer(w)
moofOffset, err := mw.writeBoxStart(&mp4.Moof{}) // <moof>
if err != nil {
return err
}
_, err = mw.writeBox(&mp4.Mfhd{ // <mfhd/>
SequenceNumber: p.SequenceNumber,
})
if err != nil {
return err
}
trackLen := len(p.Tracks)
truns := make([]*mp4.Trun, trackLen)
trunOffsets := make([]int, trackLen)
dataOffsets := make([]int, trackLen)
dataSize := 0
for i, track := range p.Tracks {
var trun *mp4.Trun
var trunOffset int
trun, trunOffset, err = track.marshal(mw)
if err != nil {
return err
}
dataOffsets[i] = dataSize
for _, sample := range track.Samples {
dataSize += len(sample.Payload)
}
truns[i] = trun
trunOffsets[i] = trunOffset
}
err = mw.writeBoxEnd() // </moof>
if err != nil {
return err
}
mdat := &mp4.Mdat{} // <mdat/>
mdat.Data = make([]byte, dataSize)
pos := 0
for _, track := range p.Tracks {
for _, sample := range track.Samples {
pos += copy(mdat.Data[pos:], sample.Payload)
}
}
mdatOffset, err := mw.writeBox(mdat)
if err != nil {
return err
}
for i := range p.Tracks {
truns[i].DataOffset = int32(dataOffsets[i] + mdatOffset - moofOffset + 8)
err = mw.rewriteBox(trunOffsets[i], truns[i])
if err != nil {
return err
}
}
return nil
}
================================================
FILE: fmp4/muxer/part_sample.go
================================================
package muxer
import (
"time"
)
// PartSample is a sample of a PartTrack.
type PartSample struct {
Dts time.Duration
Duration uint32
PTSOffset int32
IsNonSyncSample bool
Payload []byte
}
func avccMarshalSize(au [][]byte) int {
n := 0
for _, nalu := range au {
n += 4 + len(nalu)
}
return n
}
// AVCCMarshal encodes an access unit into the AVCC stream format.
// Specification: ISO 14496-15, section 5.3.4.2.1
func AVCCMarshal(au [][]byte) ([]byte, error) {
buf := make([]byte, avccMarshalSize(au))
pos := 0
for _, nalu := range au {
naluLen := len(nalu)
buf[pos] = byte(naluLen >> 24)
buf[pos+1] = byte(naluLen >> 16)
buf[pos+2] = byte(naluLen >> 8)
buf[pos+3] = byte(naluLen)
pos += 4
pos += copy(buf[pos:], nalu)
}
return buf, nil
}
// NewPartSampleH26x creates a sample with H26x data.
func NewPartSampleH26x(ptsOffset int32, randomAccessPresent bool, au [][]byte, duration uint32, dts time.Duration) *PartSample {
avcc, err := AVCCMarshal(au)
if err != nil {
return nil
}
return &PartSample{
Dts: dts,
PTSOffset: ptsOffset,
IsNonSyncSample: !randomAccessPresent,
Payload: avcc,
Duration: duration,
}
}
================================================
FILE: fmp4/muxer/part_track.go
================================================
package muxer
import "github.com/abema/go-mp4"
// PartTrack is a track of Part.
type PartTrack struct {
ID int
BaseTime uint64
Samples []*PartSample
}
func (pt *PartTrack) marshal(w *mp4Writer) (*mp4.Trun, int, error) {
/*
|traf|
| |tfhd|
| |tfdt|
| |trun|
*/
_, err := w.writeBoxStart(&mp4.Traf{}) // <traf>
if err != nil {
return nil, 0, err
}
flags := 0
_, err = w.writeBox(&mp4.Tfhd{ // <tfhd/>
FullBox: mp4.FullBox{
Flags: [3]byte{2, byte(flags >> 8), byte(flags)},
},
TrackID: uint32(pt.ID),
})
if err != nil {
return nil, 0, err
}
_, err = w.writeBox(&mp4.Tfdt{ // <tfdt/>
FullBox: mp4.FullBox{
Version: 1,
},
// sum of decode durations of all earlier samples
BaseMediaDecodeTimeV1: pt.BaseTime,
})
if err != nil {
return nil, 0, err
}
flags = trunFlagDataOffsetPreset |
trunFlagSampleDurationPresent |
trunFlagSampleSizePresent
for _, sample := range pt.Samples {
if sample.IsNonSyncSample {
flags |= trunFlagSampleFlagsPresent
}
if sample.PTSOffset != 0 {
flags |= trunFlagSampleCompositionTimeOffsetPresentOrV1
}
}
trun := &mp4.Trun{ // <trun/>
FullBox: mp4.FullBox{
Version: 1,
Flags: [3]byte{0, byte(flags >> 8), byte(flags)},
},
SampleCount: uint32(len(pt.Samples)),
}
for _, sample := range pt.Samples {
var flags uint32
if sample.IsNonSyncSample {
flags |= sampleFlagIsNonSyncSample
}
trun.Entries = append(trun.Entries, mp4.TrunEntry{
SampleDuration: sample.Duration,
SampleSize: uint32(len(sample.Payload)),
SampleFlags: flags,
SampleCompositionTimeOffsetV1: sample.PTSOffset,
})
}
trunOffset, err := w.writeBox(trun)
if err != nil {
return nil, 0, err
}
err = w.writeBoxEnd() // </traf>
if err != nil {
return nil, 0, err
}
return trun, trunOffset, nil
}
================================================
FILE: fmp4/muxer/rtmp2fmp4.go
================================================
package muxer
import (
"bytes"
"time"
"github.com/q191201771/lal/pkg/aac"
"github.com/q191201771/lal/pkg/avc"
"github.com/q191201771/lal/pkg/base"
"github.com/q191201771/lal/pkg/hevc"
"github.com/q191201771/naza/pkg/nazalog"
)
type IRtmp2Fmp4muxerObserver interface {
OnInitFmp4(init []byte)
OnFmp4Packets(currentPart *MuxerPart, lastSampleDuration time.Duration, end bool, isVideo bool)
}
var waitHeaderQueueSize = 16
type Rtmp2Fmp4Remuxer struct {
data []base.RtmpMsg
done bool
maxMsgSize int
vCodec Codec
aCodec Codec
mux *Muxer
observer IRtmp2Fmp4muxerObserver
log nazalog.Logger
nextPartId uint64
currentPart *MuxerPart
}
func NewRtmp2Fmp4Remuxer(observer IRtmp2Fmp4muxerObserver) *Rtmp2Fmp4Remuxer {
m := &Rtmp2Fmp4Remuxer{
maxMsgSize: waitHeaderQueueSize,
data: make([]base.RtmpMsg, waitHeaderQueueSize)[0:0],
done: false,
observer: observer,
log: Log,
}
m.mux = NewMuxer()
m.mux.WithLog(m.log)
return m
}
func (m *Rtmp2Fmp4Remuxer) WithLog(log nazalog.Logger) *Rtmp2Fmp4Remuxer {
m.log = log
m.mux.WithLog(m.log)
return m
}
func (m *Rtmp2Fmp4Remuxer) FeedRtmpMessage(msg base.RtmpMsg) {
m.Push(msg)
}
func (m *Rtmp2Fmp4Remuxer) Push(msg base.RtmpMsg) {
if msg.Header.MsgTypeId == base.RtmpTypeIdMetadata {
return
}
if m.done {
m.pack(msg)
return
}
if msg.IsVideoKeySeqHeader() {
switch msg.VideoCodecId() {
case base.RtmpCodecIdAvc:
if sps, pps, err := avc.ParseSpsPpsFromSeqHeader(msg.Payload); err != nil {
m.log.Errorf("parse sps pps from seq header failed: %v", err)
return
} else {
m.vCodec = &CodecH264{
SPS: sps,
PPS: pps,
}
}
case base.RtmpCodecIdHevc:
var vps, sps, pps []byte
var err error
if msg.IsEnhanced() {
vps, sps, pps, err = hevc.ParseVpsSpsPpsFromEnhancedSeqHeader(msg.Payload)
if err != nil {
nazalog.Error("ParseVpsSpsPpsFromEnhancedSeqHeader failed, err:", err)
break
}
} else {
vps, sps, pps, err = hevc.ParseVpsSpsPpsFromSeqHeader(msg.Payload)
if err != nil {
nazalog.Error("ParseVpsSpsPpsFromSeqHeader failed, err:", err)
break
}
}
m.vCodec = &CodecH265{
VPS: vps,
SPS: sps,
PPS: pps,
}
default:
m.log.Errorf("unknown video codec id: %d", msg.VideoCodecId())
return
}
}
if msg.Header.MsgTypeId == base.RtmpTypeIdAudio {
switch msg.AudioCodecId() {
case base.RtmpSoundFormatAac:
if msg.IsAacSeqHeader() {
if ascCtx, err := aac.NewAscContext(msg.Payload[2:]); err != nil {
m.log.Errorf("new asc context failed: %v", err)
return
} else {
m.aCodec = &CodecAAC{
Ctx: ascCtx,
AscData: msg.Payload[2:],
}
}
}
default:
return
}
}
m.data = append(m.data, msg.Clone())
if m.vCodec != nil && m.aCodec != nil {
m.drain()
return
}
if len(m.data) >= m.maxMsgSize {
m.drain()
return
}
}
func (m *Rtmp2Fmp4Remuxer) drain() {
if m.vCodec != nil {
m.mux.AddVideoTrack(m.vCodec)
}
if m.aCodec != nil {
m.mux.AddAudioTrack(m.aCodec)
}
init := m.mux.GetInitMp4()
if m.observer != nil {
m.observer.OnInitFmp4(init)
}
for i := range m.data {
m.pack(m.data[i])
}
m.data = nil
m.done = true
}
func (m *Rtmp2Fmp4Remuxer) FlushLastSegment() {
if m.currentPart != nil {
if err := m.currentPart.Encode(0, true); err == nil && m.observer != nil {
m.observer.OnFmp4Packets(m.currentPart, 0, true, false)
}
}
}
func (m *Rtmp2Fmp4Remuxer) Dispose() {
}
func (m *Rtmp2Fmp4Remuxer) pack(msg base.RtmpMsg) {
paramsChanged := false
if m.done {
if msg.IsVideoKeySeqHeader() {
switch msg.VideoCodecId() {
case base.RtmpCodecIdAvc:
if sps, pps, err := avc.ParseSpsPpsFromSeqHeader(msg.Payload); err != nil {
m.log.Errorf("parse sps pps from seq header failed: %v", err)
return
} else {
codec, ok := m.vCodec.(*CodecH264)
if !ok || !bytes.Equal(codec.SPS, sps) || !bytes.Equal(codec.PPS, pps) {
old := m.vCodec
m.vCodec = &CodecH264{
SPS: sps,
PPS: pps,
}
paramsChanged = true
if old != nil && m.vCodec != nil {
m.log.Infof("video codec changed, old:%s, new:%s", old.String(), m.vCodec.String())
}
}
}
case base.RtmpCodecIdHevc:
var vps, sps, pps []byte
var err error
if msg.IsEnhanced() {
vps, sps, pps, err = hevc.ParseVpsSpsPpsFromEnhancedSeqHeader(msg.Payload)
if err != nil {
nazalog.Error("ParseVpsSpsPpsFromEnhancedSeqHeader failed, err:", err)
break
}
} else {
vps, sps, pps, err = hevc.ParseVpsSpsPpsFromSeqHeader(msg.Payload)
if err != nil {
nazalog.Error("ParseVpsSpsPpsFromSeqHeader failed, err:", err)
break
}
}
codec, ok := m.vCodec.(*CodecH265)
if !ok || !bytes.Equal(codec.VPS, vps) || !bytes.Equal(codec.SPS, sps) || !bytes.Equal(codec.PPS, pps) {
old := m.vCodec
m.vCodec = &CodecH265{
VPS: vps,
SPS: sps,
PPS: pps,
}
paramsChanged = true
if old != nil && m.vCodec != nil {
m.log.Infof("video codec changed, old:%s, new:%s", old.String(), m.vCodec.String())
}
}
}
} else if msg.Header.MsgTypeId == base.RtmpTypeIdAudio {
if msg.IsAacSeqHeader() {
if ascCtx, err := aac.NewAscContext(msg.Payload[2:]); err != nil {
m.log.Errorf("new asc context failed: %v", err)
return
} else {
codec, ok := m.aCodec.(*CodecAAC)
if !ok || !bytes.Equal(codec.AscData, msg.Payload[2:]) {
old := m.aCodec
m.aCodec = &CodecAAC{
Ctx: ascCtx,
AscData: msg.Payload[2:],
}
paramsChanged = true
if old != nil && m.aCodec != nil {
m.log.Infof("audio codec changed, old:%s, new:%s", old.String(), m.aCodec.String())
}
}
}
}
}
if paramsChanged {
// 编码格式发生变化,需要更新init和强制生成当前这个文件
if m.currentPart != nil {
if err := m.currentPart.Encode(0, true); err == nil && m.observer != nil {
m.observer.OnFmp4Packets(m.currentPart, 0, true, false)
}
}
m.mux = NewMuxer()
m.mux.WithLog(m.log)
if m.vCodec != nil {
m.mux.AddVideoTrack(m.vCodec)
}
if m.aCodec != nil {
m.mux.AddAudioTrack(m.aCodec)
}
init := m.mux.GetInitMp4()
if m.observer != nil {
m.observer.OnInitFmp4(init)
}
m.currentPart = nil
}
}
sample, err := m.mux.Pack(msg)
if err == nil {
if m.vCodec != nil {
// 视频存在的话,I帧作为分割点
if msg.IsVideoKeyNalu() {
if m.currentPart == nil {
m.currentPart = NewMuxerPart(m.partId(), m.mux.AudioTimeScale())
} else {
if len(m.currentPart.VideoSamples) >= 15 {
if m.observer != nil {
m.observer.OnFmp4Packets(m.currentPart, sample.Dts, false, true)
} else {
return
}
m.currentPart = NewMuxerPart(m.partId(), m.mux.AudioTimeScale())
}
}
}
if msg.Header.MsgTypeId == base.RtmpTypeIdVideo {
m.currentPart.WriteVideo(sample)
} else {
// 防止起始是音频
if m.currentPart == nil {
m.currentPart = NewMuxerPart(m.partId(), m.mux.AudioTimeScale())
}
m.currentPart.WriteAudio(sample)
}
} else {
if m.currentPart == nil {
m.currentPart = NewMuxerPart(m.partId(), m.mux.AudioTimeScale())
} else {
// 只有音频的话,2s分割
if m.currentPart.Duration() >= 2*time.Second {
if m.observer != nil {
m.observer.OnFmp4Packets(m.currentPart, sample.Dts, false, false)
}
m.currentPart = NewMuxerPart(m.partId(), m.mux.AudioTimeScale())
}
}
m.currentPart.WriteAudio(sample)
}
}
}
func (m *Rtmp2Fmp4Remuxer) partId() uint64 {
id := m.nextPartId
m.nextPartId++
return id
}
================================================
FILE: fmp4/muxer/seekablebuffer.go
================================================
package muxer
import (
"bytes"
"fmt"
"io"
)
// Buffer is a bytes.Buffer with an additional Seek() method.
type Buffer struct {
bytes.Buffer
pos int64
}
// Write implements io.Writer.
func (b *Buffer) Write(p []byte) (int, error) {
n := 0
if b.pos < int64(b.Len()) {
n = copy(b.Bytes()[b.pos:], p)
p = p[n:]
}
if len(p) > 0 {
// Buffer.Write can't return an error.
nn, _ := b.Buffer.Write(p) //nolint:errcheck
n += nn
}
b.pos += int64(n)
return n, nil
}
// Read implements io.Reader.
func (b *Buffer) Read(_ []byte) (int, error) {
return 0, fmt.Errorf("unimplemented")
}
// Seek implements io.Seeker.
func (b *Buffer) Seek(offset int64, whence int) (int64, error) {
pos2 := int64(0)
switch whence {
case io.SeekStart:
pos2 = offset
case io.SeekCurrent:
pos2 = b.pos + offset
case io.SeekEnd:
pos2 = int64(b.Len()) + offset
}
if pos2 < 0 {
return 0, fmt.Errorf("negative position")
}
b.pos = pos2
diff := b.pos - int64(b.Len())
if diff > 0 {
// Buffer.Write can't return an error.
b.Buffer.Write(make([]byte, diff)) //nolint:errcheck
}
return pos2, nil
}
// Reset resets the buffer state.
func (b *Buffer) Reset() {
b.Buffer.Reset()
b.pos = 0
}
================================================
FILE: fmp4/muxer/track.go
================================================
package muxer
type Track struct {
Codec
TrackId uint32
timeScale uint32
firstDTS int64
lastDTS int64
samples []PartSample
}
func NewTrack(codec Codec, trackId, timeSacle uint32) *Track {
return &Track{
Codec: codec,
TrackId: trackId,
timeScale: timeSacle,
firstDTS: -1,
}
}
================================================
FILE: fmp4/muxer/var.go
================================================
package muxer
import "github.com/q191201771/naza/pkg/nazalog"
var Log = nazalog.GetGlobalLogger()
================================================
FILE: gb28181/auth.go
================================================
package gb28181
import (
"crypto/md5"
"fmt"
"github.com/ghettovoice/gosip/sip"
"github.com/q191201771/naza/pkg/nazalog"
)
type Authorization struct {
*sip.Authorization
}
func (a *Authorization) Verify(username, passwd, realm, nonce string) bool {
//1、将 username,realm,password 依次组合获取 1 个字符串,并用算法加密的到密文 r1
s1 := fmt.Sprintf("%s:%s:%s", username, realm, passwd)
r1 := a.getDigest(s1)
//2、将 method,即REGISTER ,uri 依次组合获取 1 个字符串,并对这个字符串使用算法 加密得到密文 r2
s2 := fmt.Sprintf("REGISTER:%s", a.Uri())
r2 := a.getDigest(s2)
if r1 == "" || r2 == "" {
nazalog.Error("Authorization algorithm wrong")
return false
}
//3、将密文 1,nonce 和密文 2 依次组合获取 1 个字符串,并对这个字符串使用算法加密,获得密文 r3,即Response
s3 := fmt.Sprintf("%s:%s:%s", r1, nonce, r2)
r3 := a.getDigest(s3)
//4、计算服务端和客户端上报的是否相等
return r3 == a.Response()
}
func (a *Authorization) getDigest(raw string) string {
switch a.Algorithm() {
case "MD5":
return fmt.Sprintf("%x", md5.Sum([]byte(raw)))
default: //如果没有算法,默认使用MD5
return fmt.Sprintf("%x", md5.Sum([]byte(raw)))
}
}
================================================
FILE: gb28181/avail_conn_pool.go
================================================
// Copyright 2020, Chef. All rights reserved.
// https://github.com/q191201771/naza
//
// Use of this source code is governed by a MIT-style license
// that can be found in the License file.
//
// Author: Chef (191201771@qq.com)
//根据naza修改,新增tcp
package gb28181
import (
"errors"
"net"
"sync"
)
var ErrNazaNet = errors.New("gb28181: fxxk")
type OnListenWithPort func(port uint16) (net.Listener, error)
// 从指定的端口范围内,寻找可绑定监听的端口,绑定监听并返回
type AvailConnPool struct {
minPort uint16
maxPort uint16
m sync.Mutex
lastPort uint16
onListenWithPort OnListenWithPort
}
func NewAvailConnPool(minPort uint16, maxPort uint16) *AvailConnPool {
return &AvailConnPool{
minPort: minPort,
maxPort: maxPort,
lastPort: minPort,
}
}
func (a *AvailConnPool) WithListenWithPort(listenWithPort OnListenWithPort) {
a.onListenWithPort = listenWithPort
}
func (a *AvailConnPool) Acquire() (net.Listener, uint16, error) {
a.m.Lock()
defer a.m.Unlock()
loopFirstFlag := true
p := a.lastPort
for {
// 找了一轮也没有可用的,返回错误
if !loopFirstFlag && p == a.lastPort {
return nil, 0, ErrNazaNet
}
loopFirstFlag = false
if a.onListenWithPort == nil {
return nil, 0, ErrNazaNet
}
listener, err := a.onListenWithPort(p)
// 绑定失败,尝试下一个端口
if err != nil {
p = a.nextPort(p)
continue
}
// 绑定成功,更新last,返回结果
a.lastPort = a.nextPort(p)
return listener, p, nil
}
}
// 通过Acquire获取到可用net.UDPConn对象后,将对象关闭,只返回可用的端口
func (a *AvailConnPool) Peek() (uint16, error) {
conn, port, err := a.Acquire()
if err == nil {
err = conn.Close()
}
return port, err
}
func (a *AvailConnPool) ListenWithPort(port uint16) (net.Listener, error) {
if a.onListenWithPort == nil {
return nil, ErrNazaNet
}
return a.onListenWithPort(port)
}
func (a *AvailConnPool) nextPort(p uint16) uint16 {
if p == a.maxPort {
return a.minPort
}
return p + 1
}
================================================
FILE: gb28181/channel.go
================================================
package gb28181
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/q191201771/naza/pkg/nazaatomic"
config "github.com/q191201771/lalmax/config"
"github.com/q191201771/lalmax/gb28181/mediaserver"
"github.com/ghettovoice/gosip/sip"
"github.com/q191201771/naza/pkg/nazalog"
)
type Channel struct {
device *Device // 所属设备
//status atomic.Int32 // 通道状态,0:空闲,1:正在invite,2:正在播放
GpsTime time.Time // gps时间
number uint16
ackReq sip.Request
observer IMediaOpObserver
playInfo *PlayInfo
ChannelInfo
conf config.GB28181Config
}
// Channel 通道
type ChannelInfo struct {
ChannelId string `xml:"DeviceID"` // 设备id
ParentId string `xml:"ParentID"` //父目录Id
Name string `xml:"Name"` //设备名称
Manufacturer string `xml:"Manufacturer"` //制造厂商
Model string `xml:"Model"` //型号
Owner string `xml:"Owner"` //设备归属
CivilCode string `xml:"CivilCode"` //行政区划编码
Address string `xml:"Address"` //地址
Port int `xml:"Port"` //端口
Parental int `xml:"Parental"` //存在子设备,这里表明有子目录存在 1代表有子目录,0表示没有
SafetyWay int `xml:"SafetyWay"` //信令安全模式(可选)缺省为 0;0:不采用;2:S/MIME 签名方式;3:S/MIME 加密签名同时采用方式;4:数字摘要方式
RegisterWay int `xml:"RegisterWay"` //标准的认证注册模式
Secrecy int `xml:"Secrecy"` //0 表示不涉密
Status ChannelStatus `xml:"Status"` // 状态 on 在线 off离线
Longitude string `xml:"Longitude"` // 经度
Latitude string `xml:"Latitude"` // 纬度
StreamName string `xml:"-"`
serial string
mediaserver.MediaInfo
sn nazaatomic.Uint32
}
type ChannelStatus string
const (
ChannelOnStatus = "ON"
ChannelOffStatus = "OFF"
)
func (channel *Channel) WithMediaServer(observer IMediaOpObserver) {
channel.observer = observer
}
func (channel *Channel) TryAutoInvite(opt *InviteOptions, streamName string, playInfo *PlayInfo) {
if channel.CanInvite(streamName) {
go channel.Invite(opt, streamName, playInfo)
}
}
func (channel *Channel) CanInvite(streamName string) bool {
if len(channel.ChannelId) != 20 || channel.Status == ChannelOffStatus {
nazalog.Info("return false, channel.DeviceID:", len(channel.ChannelId), " channel.Status:", channel.Status)
return false
}
if channel.Parental != 0 {
return false
}
if channel.MediaInfo.IsInvite {
return false
}
// 11~13位是设备类型编码
typeID := channel.ChannelId[10:13]
if typeID == "132" || typeID == "131" {
return true
}
nazalog.Info("return false")
return false
}
// Invite 发送Invite报文 invites a channel to play
// 注意里面的锁保证不同时发送invite报文,该锁由channel持有
/***
f字段: f = v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率
各项具体含义:
v:后续参数为视频的参数;各参数间以 “/”分割;
编码格式:十进制整数字符串表示
1 –MPEG-4 2 –H.264 3 – SVAC 4 –3GP
分辨率:十进制整数字符串表示
1 – QCIF 2 – CIF 3 – 4CIF 4 – D1 5 –720P 6 –1080P/I
帧率:十进制整数字符串表示 0~99
码率类型:十进制整数字符串表示
1 – 固定码率(CBR) 2 – 可变码率(VBR)
码率大小:十进制整数字符串表示 0~100000(如 1表示1kbps)
a:后续参数为音频的参数;各参数间以 “/”分割;
编码格式:十进制整数字符串表示
1 – G.711 2 – G.723.1 3 – G.729 4 – G.722.1
码率大小:十进制整数字符串
音频编码码率: 1 — 5.3 kbps (注:G.723.1中使用)
2 — 6.3 kbps (注:G.723.1中使用)
3 — 8 kbps (注:G.729中使用)
4 — 16 kbps (注:G.722.1中使用)
5 — 24 kbps (注:G.722.1中使用)
6 — 32 kbps (注:G.722.1中使用)
7 — 48 kbps (注:G.722.1中使用)
8 — 64 kbps(注:G.711中使用)
采样率:十进制整数字符串表示
1 — 8 kHz(注:G.711/ G.723.1/ G.729中使用)
2—14 kHz(注:G.722.1中使用)
3—16 kHz(注:G.722.1中使用)
4—32 kHz(注:G.722.1中使用)
注1:字符串说明
本节中使用的“十进制整数字符串”的含义为“0”~“4294967296” 之间的十进制数字字符串。
注2:参数分割标识
各参数间以“/”分割,参数间的分割符“/”不能省略;
若两个分割符 “/”间的某参数为空时(即两个分割符 “/”直接将相连时)表示无该参数值;
注3:f字段说明
使用f字段时,应保证视频和音频参数的结构完整性,即在任何时候,f字段的结构都应是完整的结构:
f = v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率
若只有视频时,音频中的各参数项可以不填写,但应保持 “a///”的结构:
f = v/编码格式/分辨率/帧率/码率类型/码率大小a///
若只有音频时也类似处理,视频中的各参数项可以不填写,但应保持 “v/”的结构:
f = v/a/编码格式/码率大小/采样率
f字段中视、音频参数段之间不需空格分割。
可使用f字段中的分辨率参数标识同一设备不同分辨率的码流。
*/
func (channel *Channel) Invite(opt *InviteOptions, streamName string, playInfo *PlayInfo) (code int, err error) {
d := channel.device
s := "Play"
//然后按顺序生成,一个channel最大999 方便排查问题,也能保证唯一性
channel.number++
if channel.number > 999 {
channel.number = 1
}
if len(channel.serial) == 0 {
channel.serial = RandNumString(6)
}
opt.CreateSSRC(channel.serial, channel.number)
var mediaserver *mediaserver.GB28181MediaServer
if channel.observer != nil {
mediaserver = channel.observer.OnStartMediaServer(playInfo.NetWork, playInfo.SinglePort, channel.device.ID, channel.ChannelId)
}
if mediaserver == nil {
return http.StatusNotFound, err
}
protocol := ""
if playInfo.NetWork == "tcp" {
opt.MediaPort = mediaserver.GetListenerPort()
protocol = "TCP/"
} else {
opt.MediaPort = mediaserver.GetListenerPort()
}
sdpInfo := []string{
"v=0",
fmt.Sprintf("o=%s 0 0 IN IP4 %s", channel.ChannelId, d.mediaIP),
"s=" + s,
"c=IN IP4 " + d.mediaIP,
opt.String(),
fmt.Sprintf("m=video %d %sRTP/AVP 96", opt.MediaPort, protocol),
"a=recvonly",
"a=rtpmap:96 PS/90000",
"y=" + opt.ssrc,
}
if playInfo.NetWork == "tcp" {
sdpInfo = append(sdpInfo, "a=setup:passive", "a=connection:new")
}
invite := channel.CreateRequst(sip.INVITE, channel.conf)
contentType := sip.ContentType("application/sdp")
invite.AppendHeader(&contentType)
contentLength := sip.ContentLength(len(sdpInfo))
invite.AppendHeader(&contentLength)
invite.SetBody(strings.Join(sdpInfo, "\r\n")+"\r\n", true)
subject := sip.GenericHeader{
HeaderName: "Subject", Contents: fmt.Sprintf("%s:%s,%s:0", channel.ChannelId, opt.ssrc, channel.conf.Serial),
}
invite.AppendHeader(&subject)
inviteRes, err := d.SipRequestForResponse(invite)
if err != nil {
nazalog.Error("invite failed, err:", err, " invite msg:", invite.String())
//jay 在media端口监听成功后,但是sip发送失败时
if channel.observer != nil {
if err = channel.observer.OnStopMediaServer(playInfo.NetWork, playInfo.SinglePort, channel.device.ID, channel.ChannelId, ""); err != nil {
nazalog.Errorf("gb28181 MediaServer stop err:%s", err.Error())
}
}
return http.StatusInternalServerError, err
}
code = int(inviteRes.StatusCode())
if code == http.StatusOK {
ds := strings.Split(inviteRes.Body(), "\r\n")
for _, l := range ds {
if ls := strings.Split(l, "="); len(ls) > 1 {
if ls[0] == "y" && len(ls[1]) > 0 {
if _ssrc, err := strconv.ParseInt(ls[1], 10, 0); err == nil {
opt.SSRC = uint32(_ssrc)
} else {
nazalog.Error("parse invite response y failed, err:", err)
}
}
if ls[0] == "m" && len(ls[1]) > 0 {
netinfo := strings.Split(ls[1], " ")
if strings.ToUpper(netinfo[2]) == "TCP/RTP/AVP" {
nazalog.Info("Device support tcp")
} else {
nazalog.Info("Device not support tcp")
}
}
}
}
channel.MediaInfo.IsInvite = true
channel.MediaInfo.Ssrc = opt.SSRC
channel.MediaInfo.StreamName = streamName
channel.MediaInfo.MediaKey = fmt.Sprintf("%s%d", playInfo.NetWork, mediaserver.GetListenerPort())
ackReq := sip.NewAckRequest("", invite, inviteRes, "", nil)
//保存一下播放信息
channel.ackReq = ackReq
channel.playInfo = playInfo
err = channel.device.sipSvr.Send(ackReq)
} else {
if channel.observer != nil {
if err = channel.observer.OnStopMediaServer(playInfo.NetWork, playInfo.SinglePort, channel.device.ID, channel.ChannelId, ""); err != nil {
nazalog.Errorf("gb28181 MediaServer stop err:%s", err.Error())
}
}
}
return
}
func (channel *Channel) GetCallId() string {
if channel.ackReq != nil {
if callId, ok := channel.ackReq.CallID(); ok {
return callId.Value()
}
}
return ""
}
func (channel *Channel) stopMediaServer() (err error) {
if channel.playInfo != nil {
if channel.observer != nil {
if err = channel.observer.OnStopMediaServer(channel.playInfo.NetWork, channel.playInfo.SinglePort, channel.device.ID, channel.ChannelId, channel.playInfo.StreamName); err != nil {
nazalog.Errorf("gb28181 MediaServer stop err:%s", err.Error())
}
}
}
return
}
func (channel *Channel) byeClear() (err error) {
err = channel.stopMediaServer()
channel.ackReq = nil
channel.MediaInfo.Clear()
return
}
func (channel *Channel) Bye(streamName string) (err error) {
if channel.ackReq != nil {
byeReq := channel.ackReq
channel.ackReq = nil
byeReq.SetMethod(sip.BYE)
seq, _ := byeReq.CSeq()
seq.SeqNo += 1
channel.device.sipSvr.Send(byeReq)
} else {
err = errors.New("channel has been closed")
}
channel.stopMediaServer()
return err
}
func (channel *Channel) CreateRequst(Method sip.RequestMethod, conf config.GB28181Config) (req sip.Request) {
d := channel.device
d.sn++
callId := sip.CallID(RandNumString(10))
userAgent := sip.UserAgentHeader("LALMax")
maxForwards := sip.MaxForwards(70) //增加max-forwards为默认值 70
cseq := sip.CSeq{
SeqNo: uint32(d.sn),
MethodName: Method,
}
port := sip.Port(conf.SipPort)
serverAddr := sip.Address{
Uri: &sip.SipUri{
FUser: sip.String{Str: conf.Serial},
FHost: d.sipIP,
FPort: &port,
},
Params: sip.NewParams().Add("tag", sip.String{Str: RandNumString(9)}),
}
//非同一域的目标地址需要使用@host
host := conf.Realm
if channel.ChannelId[0:9] != host {
if channel.Port != 0 {
deviceIp := d.NetAddr
deviceIp = deviceIp[0:strings.LastIndex(deviceIp, ":")]
host = fmt.Sprintf("%s:%d", deviceIp, channel.Port)
} else {
host = d.NetAddr
}
}
channelAddr := sip.Address{
Uri: &sip.SipUri{FUser: sip.String{Str: channel.ChannelId}, FHost: host},
}
req = sip.NewRequest(
"",
Method,
channelAddr.Uri,
"SIP/2.0",
[]sip.Header{
serverAddr.AsFromHeader(),
channelAddr.AsToHeader(),
&callId,
&userAgent,
&cseq,
&maxForwards,
serverAddr.AsContactHeader(),
},
"",
nil,
)
req.SetTransport(channel.device.network)
req.SetDestination(d.NetAddr)
return req
}
func (channel *Channel) PtzDirection(direction *PtzDirection) error {
ptz := Ptz{
ZoomOut: false,
ZoomIn: false,
Up: direction.Up,
Down: direction.Down,
Left: direction.Left,
Right: direction.Right,
Speed: direction.Speed,
}
msgPtz := &MessagePtz{
CmdType: DeviceControl,
DeviceID: direction.ChannelId,
SN: int(channel.sn.Add(1)),
PTZCmd: ptz.Pack(),
}
xml, err := XmlEncode(msgPtz)
if err != nil {
return err
}
return channel.sipMessage(xml)
}
func (channel *Channel) PtzZoom(zoom *PtzZoom) error {
ptz := Ptz{
ZoomOut: zoom.ZoomOut,
ZoomIn: zoom.ZoomIn,
Speed: zoom.Speed,
}
msgPtz := &MessagePtz{
CmdType: DeviceControl,
DeviceID: zoom.ChannelId,
SN: int(channel.sn.Add(1)),
PTZCmd: ptz.Pack(),
}
xml, err := XmlEncode(msgPtz)
if err != nil {
return err
}
return channel.sipMessage(xml)
}
func (channel *Channel) PtzFi(fi *PtzFi) error {
ptzFi := Fi{
IrisIn: fi.IrisIn,
IrisOut: fi.IrisOut,
FocusNear: fi.FocusNear,
FocusFar: fi.FocusFar,
Speed: fi.Speed,
}
msgPtz := &MessagePtz{
CmdType: DeviceControl,
DeviceID: fi.ChannelId,
SN: int(channel.sn.Add(1)),
PTZCmd: ptzFi.Pack(),
}
xml, err := XmlEncode(msgPtz)
if err != nil {
return err
}
return channel.sipMessage(xml)
}
func (channel *Channel) PtzPreset(ptzPreset *PtzPreset) error {
cmd := byte(PresetSet)
switch ptzPreset.Cmd {
case PresetEditPoint:
cmd = PresetSet
case PresetDelPoint:
cmd = PresetDel
case PresetCallPoint:
cmd = PresetCall
default:
return errors.New(fmt.Sprintf("ptz preset cmd error:%d", ptzPreset.Cmd))
}
preset := Preset{
CMD: cmd,
Point: ptzPreset.Point,
}
msgPtz := &MessagePtz{
CmdType: DeviceControl,
DeviceID: ptzPreset.ChannelId,
SN: int(channel.sn.Add(1)),
PTZCmd: preset.Pack(),
}
xml, err := XmlEncode(msgPtz)
if err != nil {
return err
}
return channel.sipMessage(xml)
}
func (channel *Channel) PtzStop(stop *PtzStop) error {
ptz := Ptz{}
msgPtz := &MessagePtz{
CmdType: DeviceControl,
DeviceID: stop.ChannelId,
SN: int(channel.sn.Add(1)),
PTZCmd: ptz.Pack(),
}
xml, err := XmlEncode(msgPtz)
if err != nil {
return err
}
return channel.sipMessage(xml)
}
func (channel *Channel) sipMessage(xml string) error {
d := channel.device
msg := channel.CreateRequst(sip.MESSAGE, channel.conf)
msg.AppendHeader(&sip.GenericHeader{HeaderName: "Content-Type", Contents: "Application/MANSCDP+xml"})
msg.SetBody(xml, true)
msgRes, err := d.SipRequestForResponse(msg)
if err != nil {
return err
}
code := int(msgRes.StatusCode())
if code == http.StatusOK {
return nil
} else {
return errors.New(fmt.Sprintf("sip message ptz fail,code:%d", code))
}
}
================================================
FILE: gb28181/device.go
================================================
package gb28181
import (
"context"
"github.com/ghettovoice/gosip"
"net/http"
"strings"
"sync"
"time"
config "github.com/q191201771/lalmax/config"
"github.com/ghettovoice/gosip/sip"
"github.com/q191201771/naza/pkg/nazalog"
)
const TIME_LAYOUT = "2006-01-02T15:04:05"
var (
Devices sync.Map
DeviceNonce sync.Map //保存nonce防止设备伪造
DeviceRegisterCount sync.Map //设备注册次数
)
type DeviceStatus string
const (
DeviceRegisterStatus = "REGISTER"
DeviceRecoverStatus = "RECOVER"
DeviceOnlineStatus = "ONLINE"
DeviceOfflineStatus = "OFFLINE"
DeviceAlarmedStatus = "ALARMED"
)
type Device struct {
ID string
Name string
Manufacturer string
Model string
Owner string
RegisterTime time.Time
UpdateTime time.Time
LastKeepaliveAt time.Time
Status DeviceStatus
sn int
addr sip.Address
sipIP string //设备对应网卡的服务器ip
mediaIP string //设备对应网卡的服务器ip
NetAddr string
channelMap sync.Map
subscriber struct {
CallID string
Timeout time.Time
}
lastSyncTime time.Time
GpsTime time.Time //gps时间
Longitude string //经度
Latitude string //纬度
observer IMediaOpObserver
conf config.GB28181Config
network string
sipSvr gosip.Server
}
func (d *Device) WithMediaServer(observer IMediaOpObserver) {
d.observer = observer
}
func (d *Device) WithSipSvr(sipSvr gosip.Server) *Device {
d.sipSvr = sipSvr
return d
}
func (d *Device) syncChannels() {
if time.Since(d.lastSyncTime) > 2*time.Second {
d.lastSyncTime = time.Now()
d.Catalog(d.conf)
//d.Subscribe(conf)
//d.QueryDeviceInfo(conf)
}
}
func (d *Device) UpdateChannels(list ...ChannelInfo) {
for _, c := range list {
//当父设备非空且存在时、父设备节点增加通道
if c.ParentId != "" {
path := strings.Split(c.ParentId, "/")
parentId := path[len(path)-1]
//如果父ID并非本身所属设备,一般情况下这是因为下级设备上传了目录信息,该信息通常不需要处理。
// 暂时不考虑级联目录的实现
if d.ID != parentId {
if v, ok := Devices.Load(parentId); ok {
parent := v.(*Device)
parent.addOrUpdateChannel(c)
continue
}
}
}
c.ParentId = d.ID
//本设备增加通道
d.addOrUpdateChannel(c)
//channel.TryAutoInvite(&InviteOptions{}, conf)
}
}
func (d *Device) addOrUpdateChannel(info ChannelInfo) (c *Channel) {
if old, ok := d.channelMap.Load(info.ChannelId); ok {
c = old.(*Channel)
c.ChannelInfo = info
} else {
c = &Channel{
device: d,
ChannelInfo: info,
conf: d.conf,
}
c.WithMediaServer(d.observer)
d.channelMap.Store(info.ChannelId, c)
}
return
}
func (d *Device) Catalog(conf config.GB28181Config) int {
request := d.CreateRequest(sip.MESSAGE, conf)
expires := sip.Expires(3600)
d.subscriber.Timeout = time.Now().Add(time.Second * time.Duration(expires))
contentType := sip.ContentType("Application/MANSCDP+xml")
request.AppendHeader(&contentType)
request.AppendHeader(&expires)
request.SetBody(BuildCatalogXML(d.sn, d.ID), true)
// 输出Sip请求设备通道信息信令
nazalog.Info("SIP->Catalog request:", request.String())
resp, err := d.SipRequestForResponse(request)
if err == nil && resp != nil {
nazalog.Info("SIP->Catalog Response:", resp.String())
return int(resp.StatusCode())
} else if err != nil {
nazalog.Error("SIP<-Catalog error:", err)
}
return http.StatusRequestTimeout
}
func (d *Device) CreateRequest(Method sip.RequestMethod, conf config.GB28181Config) (req sip.Request) {
d.sn++
callId := sip.CallID(RandNumString(10))
userAgent := sip.UserAgentHeader("LALMax")
maxForwards := sip.MaxForwards(70) //增加max-forwards为默认值 70
cseq := sip.CSeq{
SeqNo: uint32(d.sn),
MethodName: Method,
}
port := sip.Port(conf.SipPort)
serverAddr := sip.Address{
Uri: &sip.SipUri{
FUser: sip.String{Str: conf.Serial},
FHost: d.sipIP,
FPort: &port,
},
Params: sip.NewParams().Add("tag", sip.String{Str: RandNumString(9)}),
}
req = sip.NewRequest(
"",
Method,
d.addr.Uri,
"SIP/2.0",
[]sip.Header{
serverAddr.AsFromHeader(),
d.addr.AsToHeader(),
&callId,
&userAgent,
&cseq,
&maxForwards,
serverAddr.AsContactHeader(),
},
"",
nil,
)
req.SetTransport(d.network)
req.SetDestination(d.NetAddr)
return
}
func (d *Device) Subscribe(conf config.GB28181Config) int {
request := d.CreateRequest(sip.SUBSCRIBE, conf)
if d.subscriber.CallID != "" {
callId := sip.CallID(RandNumString(10))
request.AppendHeader(&callId)
}
expires := sip.Expires(3600)
d.subscriber.Timeout = time.Now().Add(time.Second * time.Duration(expires))
contentType := sip.ContentType("Application/MANSCDP+xml")
request.AppendHeader(&contentType)
request.AppendHeader(&expires)
request.SetBody(BuildCatalogXML(d.sn, d.ID), true)
response, err := d.SipRequestForResponse(request)
if err == nil && response != nil {
if response.StatusCode() == http.StatusOK {
callId, _ := request.CallID()
d.subscriber.CallID = string(*callId)
} else {
d.subscriber.CallID = ""
}
return int(response.StatusCode())
}
return http.StatusRequestTimeout
}
func (d *Device) QueryDeviceInfo(conf config.GB28181Config) {
for i := time.Duration(5); i < 100; i++ {
time.Sleep(time.Second * i)
request := d.CreateRequest(sip.MESSAGE, conf)
contentType := sip.ContentType("Application/MANSCDP+xml")
request.AppendHeader(&contentType)
request.SetBody(BuildDeviceInfoXML(d.sn, d.ID), true)
response, _ := d.SipRequestForResponse(request)
if response != nil {
if response.StatusCode() == http.StatusOK {
break
}
}
}
}
// UpdateChannelStatus 目录订阅消息处理:新增/移除/更新通道或者更改通道状态
func (d *Device) UpdateChannelStatus(deviceList []*notifyMessage, conf config.GB28181Config) {
for _, v := range deviceList {
switch v.Event {
case "ON":
nazalog.Debug("receive channel online notify")
d.channelOnline(v.ChannelId)
case "OFF":
nazalog.Debug("receive channel offline notify")
d.channelOffline(v.ChannelId)
case "VLOST":
nazalog.Debug("receive channel video lost notify")
d.channelOffline(v.ChannelId)
case "DEFECT":
nazalog.Debug("receive channel video defect notify")
d.channelOffline(v.ChannelId)
case "ADD":
nazalog.Debug("receive channel add notify")
channel := ChannelInfo{
ChannelId: v.ChannelId,
ParentId: v.ParentId,
Name: v.Name,
Manufacturer: v.Manufacturer,
Model: v.Model,
Owner: v.Owner,
CivilCode: v.CivilCode,
Address: v.Address,
Port: v.Port,
Parental: v.Parental,
SafetyWay: v.SafetyWay,
RegisterWay: v.RegisterWay,
Secrecy: v.Secrecy,
Status: v.Status,
Longitude: v.Longitude,
Latitude: v.Latitude,
}
d.addOrUpdateChannel(channel)
case "DEL":
//删除
nazalog.Debug("receive channel delete notify")
d.deleteChannel(v.ChannelId)
case "UPDATE":
nazalog.Debug("receive channel update notify")
// 更新通道
channel := ChannelInfo{
ChannelId: v.ChannelId,
ParentId: v.ParentId,
Name: v.Name,
Manufacturer: v.Manufacturer,
Model: v.Model,
Owner: v.Owner,
CivilCode: v.CivilCode,
Address: v.Address,
Port: v.Port,
Parental: v.Parental,
SafetyWay: v.SafetyWay,
RegisterWay: v.RegisterWay,
Secrecy: v.Secrecy,
Status: v.Status,
Longitude: v.Longitude,
Latitude: v.Latitude,
}
d.UpdateChannels(channel)
}
}
}
func (d *Device) channelOnline(channelId string) {
if v, ok := d.channelMap.Load(channelId); ok {
c := v.(*Channel)
c.Status = ChannelOnStatus
nazalog.Debug("channel online, channelId: ", channelId)
} else {
nazalog.Debug("update channel status failed, not found, channelId: ", channelId)
}
}
func (d *Device) channelOffline(channelId string) {
if v, ok := d.channelMap.Load(channelId); ok {
c := v.(*Channel)
c.Status = ChannelOffStatus
nazalog.Debug("channel offline, channelId: ", channelId)
} else {
nazalog.Debug("update channel status failed, not found, channelId: ", channelId)
}
}
func (d *Device) deleteChannel(channelId string) {
d.channelMap.Delete(channelId)
}
// UpdateChannelPosition 更新通道GPS坐标
func (d *Device) UpdateChannelPosition(channelId string, gpsTime string, lng string, lat string) {
if v, ok := d.channelMap.Load(channelId); ok {
c := v.(*Channel)
c.GpsTime = time.Now() //时间取系统收到的时间,避免设备时间和格式问题
c.Longitude = lng
c.Latitude = lat
nazalog.Debug("update channel position success")
} else {
//如果未找到通道,则更新到设备上
d.GpsTime = time.Now() //时间取系统收到的时间,避免设备时间和格式问题
d.Longitude = lng
d.Latitude = lat
nazalog.Debug("update device position success, channelId:", channelId)
}
}
func (d *Device) SipRequestForResponse(request sip.Request) (sip.Response, error) {
return d.sipSvr.RequestWithContext(context.Background(), request)
}
================================================
FILE: gb28181/http_logic.go
================================================
package gb28181
import (
"sync"
"github.com/gin-gonic/gin"
)
type GbLogic struct {
s *GB28181Server
}
var gbLogic *GbLogic
var once sync.Once
func NewGbLogic(s *GB28181Server) *GbLogic {
once.Do(func() {
gbLogic = &GbLogic{
s: s,
}
})
return gbLogic
}
func (g *GbLogic) GetDeviceInfos(c *gin.Context) {
deviceInfos := g.s.getDeviceInfos()
ResponseSuccess(c, deviceInfos)
}
func (g *GbLogic) StartPlay(c *gin.Context) {
var reqPlay ReqPlay
if err := c.ShouldBindJSON(&reqPlay); err != nil {
ResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg())
} else {
ch := g.s.FindChannel(reqPlay.DeviceId, reqPlay.ChannelId)
if ch == nil {
ResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg())
} else {
streamName := reqPlay.StreamName
if len(streamName) == 0 {
streamName = reqPlay.ChannelId
}
if len(reqPlay.NetWork) == 0 || !(reqPlay.NetWork == "udp" || reqPlay.NetWork == "tcp") {
reqPlay.NetWork = "udp"
}
ch.TryAutoInvite(&InviteOptions{}, streamName, &reqPlay.PlayInfo)
respPlay := &RespPlay{
StreamName: streamName,
}
ResponseSuccess(c, respPlay)
}
}
}
func (g *GbLogic) StopPlay(c *gin.Context) {
var reqStop ReqStop
if err := c.ShouldBindJSON(&reqStop); err != nil {
ResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg())
} else {
ch := g.s.FindChannel(reqStop.DeviceId, reqStop.ChannelId)
if ch == nil {
ResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg())
} else {
streamName := reqStop.StreamName
if len(streamName) == 0 {
streamName = reqStop.ChannelId
}
if err = ch.Bye(streamName); err != nil {
ResponseErrorWithMsg(c, CodeDeviceStopError, err.Error())
} else {
ResponseSuccess(c, nil)
}
}
}
}
func (g *GbLogic) PtzDirection(c *gin.Context) {
var reqDirection PtzDirection
if err := c.ShouldBindJSON(&reqDirection); err != nil {
ResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg())
} else {
if !(reqDirection.Speed > 0 && reqDirection.Speed <= 8) {
ResponseErrorWithMsg(c, CodeInvalidParam, SpeedParamError)
}
ch := g.s.FindChannel(reqDirection.DeviceId, reqDirection.ChannelId)
if ch == nil {
ResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg())
} else {
reqDirection.Speed = reqDirection.Speed * 25
if err = ch.PtzDirection(&reqDirection); err != nil {
ResponseErrorWithMsg(c, CodeDeviceStopError, err.Error())
} else {
ResponseSuccess(c, nil)
}
}
}
}
func (g *GbLogic) PtzZoom(c *gin.Context) {
var reqZoom PtzZoom
if err := c.ShouldBindJSON(&reqZoom); err != nil {
ResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg())
} else {
if !(reqZoom.Speed > 0 && reqZoom.Speed <= 8) {
ResponseErrorWithMsg(c, CodeInvalidParam, SpeedParamError)
}
ch := g.s.FindChannel(reqZoom.DeviceId, reqZoom.ChannelId)
if ch == nil {
ResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg())
} else {
reqZoom.Speed = reqZoom.Speed * 25
if err = ch.PtzZoom(&reqZoom); err != nil {
ResponseErrorWithMsg(c, CodeDeviceStopError, err.Error())
} else {
ResponseSuccess(c, nil)
}
}
}
}
func (g *GbLogic) PtzFi(c *gin.Context) {
var reqFi PtzFi
if err := c.ShouldBindJSON(&reqFi); err != nil {
ResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg())
} else {
if !(reqFi.Speed > 0 && reqFi.Speed <= 8) {
ResponseErrorWithMsg(c, CodeInvalidParam, SpeedParamError)
}
ch := g.s.FindChannel(reqFi.DeviceId, reqFi.ChannelId)
if ch == nil {
ResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg())
} else {
reqFi.Speed = reqFi.Speed * 25
if err = ch.PtzFi(&reqFi); err != nil {
ResponseErrorWithMsg(c, CodeDeviceStopError, err.Error())
} else {
ResponseSuccess(c, nil)
}
}
}
}
func (g *GbLogic) PtzPreset(c *gin.Context) {
var reqPreset PtzPreset
if err := c.ShouldBindJSON(&reqPreset); err != nil {
ResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg())
} else {
if !(reqPreset.Point > 0 && reqPreset.Point <= 50) {
ResponseErrorWithMsg(c, CodeInvalidParam, PointParamError)
}
ch := g.s.FindChannel(reqPreset.DeviceId, reqPreset.ChannelId)
if ch == nil {
ResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg())
} else {
if err = ch.PtzPreset(&reqPreset); err != nil {
ResponseErrorWithMsg(c, CodeDeviceStopError, err.Error())
} else {
ResponseSuccess(c, nil)
}
}
}
}
func (g *GbLogic) PtzStop(c *gin.Context) {
var reqStop PtzStop
if err := c.ShouldBindJSON(&reqStop); err != nil {
ResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg())
} else {
ch := g.s.FindChannel(reqStop.DeviceId, reqStop.ChannelId)
if ch == nil {
ResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg())
} else {
if err = ch.PtzStop(&reqStop); err != nil {
ResponseErrorWithMsg(c, CodeDeviceStopError, err.Error())
} else {
ResponseSuccess(c, nil)
}
}
}
}
func (g *GbLogic) UpdateAllNotify(c *gin.Context) {
g.s.GetAllSyncChannels()
ResponseSuccess(c, nil)
}
func (g *GbLogic) UpdateNotify(c *gin.Context) {
var reqUpdateNotify ReqUpdateNotify
if err := c.ShouldBindJSON(&reqUpdateNotify); err != nil {
ResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg())
} else {
if g.s.GetSyncChannels(reqUpdateNotify.DeviceId) {
ResponseSuccess(c, nil)
} else {
ResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg())
}
}
}
================================================
FILE: gb28181/inviteoption.go
================================================
package gb28181
import (
"fmt"
"strconv"
)
type InviteOptions struct {
Start int
End int
ssrc string
SSRC uint32
MediaPort uint16
}
func (o InviteOptions) IsLive() bool {
return o.Start == 0 || o.End == 0
}
func (o InviteOptions) String() string {
return fmt.Sprintf("t=%d %d", o.Start, o.End)
}
func (o *InviteOptions) CreateSSRC(serial string, number uint16) {
//不按gb生成标准,取ID最后六位,然后按顺序生成,一个channel最大999
o.ssrc = fmt.Sprintf("%d%s%03d", 0, serial, number)
_ssrc, _ := strconv.ParseInt(o.ssrc, 10, 0)
o.SSRC = uint32(_ssrc)
}
================================================
FILE: gb28181/mediaserver/conn.go
================================================
package mediaserver
import (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"sync"
"time"
"github.com/q191201771/lalmax/gb28181/mpegps"
"github.com/pion/rtp"
"github.com/q191201771/lal/pkg/base"
"github.com/q191201771/lal/pkg/logic"
"github.com/q191201771/naza/pkg/nazalog"
)
var (
ErrInvalidPsData = errors.New("invalid mpegps data")
)
type Frame struct {
buffer *bytes.Buffer
pts uint64
dts uint64
initPts uint64
initDts uint64
}
type Conn struct {
conn net.Conn
r io.Reader
check bool
demuxer *mpegps.PsDemuxer
streamName string
lalServer logic.ILalServer
lalSession logic.ICustomizePubSessionContext
videoFrame Frame
audioFrame Frame
observer IGbObserver
rtpPts uint64
psPtsZeroTimes int64
psDumpFile *base.DumpFile
buffer *bytes.Buffer
key string
mediaServer *GB28181MediaServer
preferMediaKeyLookup bool
readTimeout time.Duration
one sync.Once
oneSaveConn sync.Once
}
func NewConn(conn net.Conn, observer IGbObserver, lal logic.ILalServer) *Conn {
c := &Conn{
conn: conn,
r: conn,
demuxer: mpegps.NewPsDemuxer(),
observer: observer,
lalServer: lal,
buffer: bytes.NewBuffer(nil),
}
c.demuxer.OnFrame = c.OnFrame
return c
}
func (c *Conn) SetMediaServer(mediaServer *GB28181MediaServer) {
c.mediaServer = mediaServer
}
func (c *Conn) SetKey(key string) {
c.key = key
}
func (c *Conn) SetPreferMediaKeyLookup(prefer bool) {
c.preferMediaKeyLookup = prefer
}
func (c *Conn) SetReadTimeout(timeout time.Duration) {
c.readTimeout = timeout
}
func (c *Conn) Serve() (err error) {
defer func() {
nazalog.Info("conn close, err:", err)
c.Close()
if c.observer != nil && c.streamName != "" {
c.observer.NotifyClose(c.streamName)
}
if c.psDumpFile != nil {
c.psDumpFile.Close()
}
if c.lalSession != nil {
c.lalServer.DelCustomizePubSession(c.lalSession)
}
}()
nazalog.Info("gb28181 conn, remoteaddr:", c.conn.RemoteAddr().String(), " localaddr:", c.conn.LocalAddr().String())
for {
if c.readTimeout > 0 {
c.conn.SetReadDeadline(time.Now().Add(c.readTimeout))
}
pkt := &rtp.Packet{}
if c.conn.RemoteAddr().Network() == "udp" {
buf := make([]byte, 1472*4)
n, err := c.conn.Read(buf)
if err != nil {
nazalog.Error("conn read failed, err:", err)
return err
}
err = pkt.Unmarshal(buf[:n])
if err != nil {
return err
}
} else {
len := make([]byte, 2)
_, err := io.ReadFull(c.r, len)
if err != nil {
return err
}
size := binary.BigEndian.Uint16(len)
buf := make([]byte, size)
_, err = io.ReadFull(c.r, buf)
if err != nil {
return err
}
err = pkt.Unmarshal(buf)
if err != nil {
return err
}
}
if !c.check && c.observer != nil {
var mediaInfo *MediaInfo
var ok bool
if c.preferMediaKeyLookup {
mediaInfo, ok = c.observer.GetMediaInfoByKey(c.key)
if !ok {
nazalog.Error("get mediaInfo :", c.key)
return fmt.Errorf("get mediaInfo:%s", c.key)
}
} else if pkt.SSRC != 0 {
mediaInfo, ok = c.observer.CheckSsrc(pkt.SSRC)
if !ok {
nazalog.Error("invalid ssrc:", pkt.SSRC)
return fmt.Errorf("invalid ssrc:%d", pkt.SSRC)
}
} else {
mediaInfo, ok = c.observer.GetMediaInfoByKey(c.key)
if !ok {
nazalog.Error("get mediaInfo :", c.key)
return fmt.Errorf("get mediaInfo:%s", c.key)
}
}
c.check = true
c.streamName = mediaInfo.StreamName
c.oneSaveConn.Do(func() {
if c.mediaServer != nil {
c.mediaServer.conns.Store(c.streamName, c)
}
})
if len(mediaInfo.DumpFileName) > 0 {
c.psDumpFile = base.NewDumpFile()
if err = c.psDumpFile.OpenToWrite(mediaInfo.DumpFileName); err != nil {
nazalog.Errorf("gb con dump file:%s", err.Error())
}
}
nazalog.Info("gb28181 ssrc check success, streamName:", c.streamName)
if c.observer != nil {
c.observer.OnRtpPacket(c.streamName, c.key)
}
session, err := c.lalServer.AddCustomizePubSession(mediaInfo.StreamName)
if err != nil {
nazalog.Error("lal server AddCustomizePubSession failed, err:", err)
return err
}
session.WithOption(func(option *base.AvPacketStreamOption) {
option.VideoFormat = base.AvPacketStreamVideoFormatAnnexb
option.AudioFormat = base.AvPacketStreamAudioFormatAdtsAac
})
c.lalSession = session
}
c.rtpPts = uint64(pkt.Header.Timestamp)
if c.observer != nil && c.streamName != "" {
c.observer.OnRtpPacket(c.streamName, c.key)
}
if c.demuxer != nil {
if c.psDumpFile != nil {
c.psDumpFile.WriteWithType(pkt.Payload, base.DumpTypePsRtpData)
}
c.demuxer.Input(pkt.Payload)
}
}
return
}
func (c *Conn) Demuxer(data []byte) error {
c.buffer.Write(data)
buf := c.buffer.Bytes()
if len(buf) < 4 {
return nil
}
if buf[0] != 0x00 && buf[1] != 0x00 && buf[2] != 0x01 && buf[3] != 0xBA {
return ErrInvalidPsData
}
packets := splitPsPackets(buf)
if len(packets) <= 1 {
return nil
}
for i, packet := range packets {
if i == len(packets)-1 {
c.buffer = bytes.NewBuffer(packet)
return nil
}
if c.demuxer != nil {
c.demuxer.Input(packet)
}
}
return nil
}
func (c *Conn) OnFrame(frame []byte, cid mpegps.PsStreamType, pts uint64, dts uint64) {
payloadType := getPayloadType(cid)
if payloadType == base.AvPacketPtUnknown {
return
}
//当ps流解析出pts为0时,计数超过10则用rtp的时间戳
if pts == 0 {
if c.psPtsZeroTimes >= 0 {
c.psPtsZeroTimes++
}
if c.psPtsZeroTimes > 10 {
pts = c.rtpPts
dts = c.rtpPts
}
} else {
c.psPtsZeroTimes = -1
}
if payloadType == base.AvPacketPtAac || payloadType == base.AvPacketPtG711A || payloadType == base.AvPacketPtG711U {
if c.audioFrame.initDts == 0 {
c.audioFrame.initDts = dts
}
if c.audioFrame.initPts == 0 {
c.audioFrame.initPts = pts
}
var pkt base.AvPacket
pkt.PayloadType = payloadType
pkt.Timestamp = int64(dts - c.audioFrame.initDts)
pkt.Pts = int64(pts - c.audioFrame.initPts)
pkt.Payload = append(pkt.Payload, frame...)
c.lalSession.FeedAvPacket(pkt)
} else {
if c.videoFrame.initPts == 0 {
c.videoFrame.initPts = pts
}
if c.videoFrame.initDts == 0 {
c.videoFrame.initDts = dts
}
// 塞入lal中
c.videoFrame.pts = pts - c.videoFrame.initPts
c.videoFrame.dts = dts - c.videoFrame.initDts
var pkt base.AvPacket
pkt.PayloadType = payloadType
pkt.Timestamp = int64(c.videoFrame.dts)
pkt.Pts = int64(c.videoFrame.pts)
pkt.Payload = frame
c.lalSession.FeedAvPacket(pkt)
}
}
func (c *Conn) Close() {
c.one.Do(func() {
c.conn.Close()
})
}
func getPayloadType(cid mpegps.PsStreamType) base.AvPacketPt {
switch cid {
case mpegps.PsStreamAac:
return base.AvPacketPtAac
case mpegps.PsStreamG711A:
return base.AvPacketPtG711A
case mpegps.PsStreamG711U:
return base.AvPacketPtG711U
case mpegps.PsStreamH264:
return base.AvPacketPtAvc
case mpegps.PsStreamH265:
return base.AvPacketPtHevc
}
return base.AvPacketPtUnknown
}
func splitPsPackets(data []byte) [][]byte {
startCode := []byte{0x00, 0x00, 0x01, 0xBA}
start := 0
var packets [][]byte
for i := 0; i < len(data); i++ {
if i+len(startCode) <= len(data) && bytes.Equal(data[i:i+len(startCode)], startCode) {
if i == 0 {
continue
}
packets = append(packets, data[start:i])
start = i
}
}
packets = append(packets, data[start:])
return packets
}
================================================
FILE: gb28181/mediaserver/mediaserver_t.go
================================================
package mediaserver
type MediaInfo struct {
IsInvite bool
Ssrc uint32
StreamName string
SinglePort bool
DumpFileName string
MediaKey string
}
func (m *MediaInfo) Clear() (err error) {
m.IsInvite = false
m.Ssrc = 0
m.StreamName = ""
m.SinglePort = false
m.DumpFileName = ""
return
}
================================================
FILE: gb28181/mediaserver/server.go
================================================
package mediaserver
import (
"errors"
"net"
"sync"
"sync/atomic"
"time"
"github.com/q191201771/lal/pkg/logic"
"github.com/q191201771/naza/pkg/nazalog"
)
const defaultReadTimeout = 10 * time.Second
type IGbObserver interface {
CheckSsrc(ssrc uint32) (*MediaInfo, bool)
GetMediaInfoByKey(key string) (*MediaInfo, bool)
NotifyClose(streamName string)
OnRtpPacket(streamName string, mediaKey string)
}
type GB28181MediaServer struct {
listenPort int
lalServer logic.ILalServer
listener net.Listener
disposeOnce sync.Once
disposed atomic.Bool
observer IGbObserver
mediaKey string
preferMediaKeyLookup bool
readTimeout time.Duration
conns sync.Map //增加链接对象,目前只适用于多端口
}
func NewGB28181MediaServer(listenPort int, mediaKey string, observer IGbObserver, lal logic.ILalServer) *GB28181MediaServer {
return &GB28181MediaServer{
listenPort: listenPort,
lalServer: lal,
observer: observer,
mediaKey: mediaKey,
readTimeout: defaultReadTimeout,
}
}
func (s *GB28181MediaServer) WithPreferMediaKeyLookup(prefer bool) *GB28181MediaServer {
s.preferMediaKeyLookup = prefer
return s
}
func (s *GB28181MediaServer) WithReadTimeout(timeout time.Duration) *GB28181MediaServer {
s.readTimeout = timeout
return s
}
func (s *GB28181MediaServer) GetListenerPort() uint16 {
return uint16(s.listenPort)
}
func (s *GB28181MediaServer) Start(listener net.Listener) (err error) {
s.listener = listener
if listener != nil {
go func(listener net.Listener) {
for {
if s.disposed.Load() {
return
}
conn, err := listener.Accept()
if err != nil {
var ne net.Error
if ok := errors.As(err, &ne); ok && ne.Timeout() {
nazalog.Error("Accept failed: timeout error, retrying...")
time.Sleep(time.Second / 20)
continue
} else {
break
}
}
if conn == nil {
continue
}
if s.disposed.Load() {
conn.Close()
return
}
c := NewConn(conn, s.observer, s.lalServer)
c.SetKey(s.mediaKey)
c.SetMediaServer(s)
c.SetPreferMediaKeyLookup(s.preferMediaKeyLookup)
c.SetReadTimeout(s.readTimeout)
s.conns.Store(c, c)
go func() {
c.Serve()
s.conns.Delete(c)
s.conns.Delete(c.streamName)
}()
}
}(listener)
}
return
}
func (s *GB28181MediaServer) CloseConn(streamName string) {
if v, ok := s.conns.Load(streamName); ok {
conn := v.(*Conn)
conn.Close()
}
}
func (s *GB28181MediaServer) Dispose() {
s.disposeOnce.Do(func() {
s.disposed.Store(true)
s.conns.Range(func(_, value any) bool {
conn := value.(*Conn)
conn.Close()
return true
})
if s.listener != nil {
s.listener.Close()
s.listener = nil
}
})
}
================================================
FILE: gb28181/mpegps/bitstream.go
================================================
package mpegps
import (
"encoding/binary"
)
var BitMask [8]byte = [8]byte{0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF}
type BitStream struct {
bits []byte
bytesOffset int
bitsOffset int
bitsmark int
bytemark int
}
func NewBitStream(buf []byte) *BitStream {
return &BitStream{
bits: buf,
bytesOffset: 0,
bitsOffset: 0,
bitsmark: 0,
bytemark: 0,
}
}
func (bs *BitStream) Uint8(n int) uint8 {
return uint8(bs.GetBits(n))
}
func (bs *BitStream) Uint16(n int) uint16 {
return uint16(bs.GetBits(n))
}
func (bs *BitStream) Uint32(n int) uint32 {
return uint32(bs.GetBits(n))
}
func (bs *BitStream) GetBytes(n int) []byte {
if bs.bytesOffset+n > len(bs.bits) {
panic("OUT OF RANGE")
}
if bs.bitsOffset != 0 {
panic("invaild operation")
}
data := make([]byte, n)
copy(data, bs.bits[bs.bytesOffset:bs.bytesOffset+n])
bs.bytesOffset += n
return data
}
// n <= 64
func (bs *BitStream) GetBits(n int) uint64 {
if bs.bytesOffset >= len(bs.bits) {
panic("OUT OF RANGE")
}
var ret uint64 = 0
if 8-bs.bitsOffset >= n {
ret = uint64((bs.bits[bs.bytesOffset] >> (8 - bs.bitsOffset - n)) & BitMask[n-1])
bs.bitsOffset += n
if bs.bitsOffset == 8 {
bs.bytesOffset++
bs.bitsOffset = 0
}
} else {
ret = uint64(bs.bits[bs.bytesOffset] & BitMask[8-bs.bitsOffset-1])
bs.bytesOffset++
n -= 8 - bs.bitsOffset
bs.bitsOffset = 0
for n > 0 {
if bs.bytesOffset >= len(bs.bits) {
panic("OUT OF RANGE")
}
if n >= 8 {
ret = ret<<8 | uint64(bs.bits[bs.bytesOffset])
bs.bytesOffset++
n -= 8
} else {
ret = (ret << n) | uint64((bs.bits[bs.bytesOffset]>>(8-n))&BitMask[n-1])
bs.bitsOffset = n
break
}
}
}
return ret
}
func (bs *BitStream) GetBit() uint8 {
if bs.bytesOffset >= len(bs.bits) {
panic("OUT OF RANGE")
}
ret := bs.bits[bs.bytesOffset] >> (7 - bs.bitsOffset) & 0x01
bs.bitsOffset++
if bs.bitsOffset >= 8 {
bs.bytesOffset++
bs.bitsOffset = 0
}
return ret
}
func (bs *BitStream) SkipBits(n int) {
bytecount := n / 8
bitscount := n % 8
bs.bytesOffset += bytecount
if bs.bitsOffset+bitscount < 8 {
bs.bitsOffset += bitscount
} else {
bs.bytesOffset += 1
bs.bitsOffset += bitscount - 8
}
}
func (bs *BitStream) Markdot() {
bs.bitsmark = bs.bitsOffset
bs.bytemark = bs.bytesOffset
}
func (bs *BitStream) DistanceFromMarkDot() int {
bytecount := bs.bytesOffset - bs.bytemark - 1
bitscount := bs.bitsOffset + (8 - bs.bitsmark)
return bytecount*8 + bitscount
}
func (bs *BitStream) RemainBytes() int {
if bs.bitsOffset > 0 {
return len(bs.bits) - bs.bytesOffset - 1
} else {
return len(bs.bits) - bs.bytesOffset
}
}
func (bs *BitStream) RemainBits() int {
if bs.bitsOffset > 0 {
return bs.RemainBytes()*8 + 8 - bs.bitsOffset
} else {
return bs.RemainBytes() * 8
}
}
func (bs *BitStream) Bits() []byte {
return bs.bits
}
func (bs *BitStream) RemainData() []byte {
return bs.bits[bs.bytesOffset:]
}
// 无符号哥伦布熵编码
func (bs *BitStream) ReadUE() uint64 {
leadingZeroBits := 0
for bs.GetBit() == 0 {
leadingZeroBits++
}
if leadingZeroBits == 0 {
return 0
}
info := bs.GetBits(leadingZeroBits)
return uint64(1)<<leadingZeroBits - 1 + info
}
// 有符号哥伦布熵编码
func (bs *BitStream) ReadSE() int64 {
v := bs.ReadUE()
if v%2 == 0 {
return -1 * int64(v/2)
} else {
return int64(v+1) / 2
}
}
func (bs *BitStream) ByteOffset() int {
return bs.bytesOffset
}
func (bs *BitStream) UnRead(n int) {
if n-bs.bitsOffset <= 0 {
bs.bitsOffset -= n
} else {
least := n - bs.bitsOffset
for least >= 8 {
bs.bytesOffset--
least -= 8
}
if least > 0 {
bs.bytesOffset--
bs.bitsOffset = 8 - least
}
}
}
func (bs *BitStream) NextBits(n int) uint64 {
r := bs.GetBits(n)
bs.UnRead(n)
return r
}
func (bs *BitStream) EOS() bool {
return bs.bytesOffset == len(bs.bits) && bs.bitsOffset == 0
}
type BitStreamWriter struct {
bits []byte
byteoffset int
bitsoffset int
bitsmark int
bytemark int
}
func NewBitStreamWriter(n int) *BitStreamWriter {
return &BitStreamWriter{
bits: make([]byte, n),
byteoffset: 0,
bitsoffset: 0,
bitsmark: 0,
bytemark: 0,
}
}
func (bsw *BitStreamWriter) expandSpace(n int) {
if (len(bsw.bits)-bsw.byteoffset-1)*8+8-bsw.bitsoffset < n {
newlen := 0
if len(bsw.bits)*8 < n {
newlen = len(bsw.bits) + n/8 + 1
} else {
newlen = len(bsw.bits) * 2
}
tmp := make([]byte, newlen)
copy(tmp, bsw.bits)
bsw.bits = tmp
}
}
func (bsw *BitStreamWriter) ByteOffset() int {
return bsw.byteoffset
}
func (bsw *BitStreamWriter) BitOffset() int {
return bsw.bitsoffset
}
func (bsw *BitStreamWriter) Markdot() {
bsw.bitsmark = bsw.bitsoffset
bsw.bytemark = bsw.byteoffset
}
func (bsw *BitStreamWriter) DistanceFromMarkDot() int {
bytecount := bsw.byteoffset - bsw.bytemark - 1
bitscount := bsw.bitsoffset + (8 - bsw.bitsmark)
return bytecount*8 + bitscount
}
func (bsw *BitStreamWriter) PutByte(v byte) {
bsw.expandSpace(8)
if bsw.bitsoffset == 0 {
bsw.bits[bsw.byteoffset] = v
bsw.byteoffset++
} else {
bsw.bits[bsw.byteoffset] |= v >> byte(bsw.bitsoffset)
bsw.byteoffset++
bsw.bits[bsw.byteoffset] = v & BitMask[bsw.bitsoffset-1]
}
}
func (bsw *BitStreamWriter) PutBytes(v []byte) {
if bsw.bitsoffset != 0 {
panic("bsw.bitsoffset > 0")
}
bsw.expandSpace(8 * len(v))
copy(bsw.bits[bsw.byteoffset:], v)
bsw.byteoffset += len(v)
}
func (bsw *BitStreamWriter) PutRepetValue(v byte, n int) {
if bsw.bitsoffset != 0 {
panic("bsw.bitsoffset > 0")
}
bsw.expandSpace(8 * n)
for i := 0; i < n; i++ {
bsw.bits[bsw.byteoffset] = v
bsw.byteoffset++
}
}
func (bsw *BitStreamWriter) PutUint8(v uint8, n int) {
bsw.PutUint64(uint64(v), n)
}
func (bsw *BitStreamWriter) PutUint16(v uint16, n int) {
bsw.PutUint64(uint64(v), n)
}
func (bsw *BitStreamWriter) PutUint32(v uint32, n int) {
bsw.PutUint64(uint64(v), n)
}
func (bsw *BitStreamWriter) PutUint64(v uint64, n int) {
bsw.expandSpace(n)
if 8-bsw.bitsoffset >= n {
bsw.bits[bsw.byteoffset] |= uint8(v) & BitMask[n-1] << (8 - bsw.bitsoffset - n)
bsw.bitsoffset += n
if bsw.bitsoffset == 8 {
bsw.bitsoffset = 0
bsw.byteoffset++
}
} else {
bsw.bits[bsw.byteoffset] |= uint8(v>>(n-int(8-bsw.bitsoffset))) & BitMask[8-bsw.bitsoffset-1]
bsw.byteoffset++
n -= 8 - bsw.bitsoffset
for n-8 >= 0 {
bsw.bits[bsw.byteoffset] = uint8(v>>(n-8)) & 0xFF
bsw.byteoffset++
n -= 8
}
bsw.bitsoffset = n
if n > 0 {
bsw.bits[bsw.byteoffset] |= (uint8(v) & BitMask[n-1]) << (8 - n)
}
}
}
func (bsw *BitStreamWriter) SetByte(v byte, where int) {
bsw.bits[where] = v
}
func (bsw *BitStreamWriter) SetUint16(v uint16, where int) {
binary.BigEndian.PutUint16(bsw.bits[where:where+2], v)
}
func (bsw *BitStreamWriter) Bits() []byte {
if bsw.byteoffset == len(bsw.bits) {
return bsw.bits
}
if bsw.bitsoffset > 0 {
return bsw.bits[0 : bsw.byteoffset+1]
} else {
return bsw.bits[0:bsw.byteoffset]
}
}
// 用v 填充剩余字节
func (bsw *BitStreamWriter) FillRemainData(v byte) {
for i := bsw.byteoffset; i < len(bsw.bits); i++ {
bsw.bits[i] = v
}
bsw.byteoffset = len(bsw.bits)
bsw.bitsoffset = 0
}
func (bsw *BitStreamWriter) Reset() {
for i := 0; i < len(bsw.bits); i++ {
bsw.bits[i] = 0
}
bsw.bitsmark = 0
bsw.bytemark = 0
bsw.bitsoffset = 0
bsw.byteoffset = 0
}
================================================
FILE: gb28181/mpegps/pes_proto.
gitextract_sbsxbvrx/
├── .github/
│ └── workflows/
│ ├── go.yml
│ └── release.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── build.sh
├── conf/
│ ├── cert.pem
│ ├── key.pem
│ └── lalmax.conf.json
├── config/
│ ├── config.go
│ └── config_test.go
├── document/
│ ├── api.md
│ ├── api_gateway.md
│ ├── config.md
│ ├── gb28181.md
│ ├── hook_api.md
│ ├── hook_plugin_architecture.md
│ ├── lal_api.md
│ ├── lal_config.md
│ ├── rtc.md
│ ├── srt.md
│ └── stream_url.md
├── fmp4/
│ ├── hls/
│ │ ├── server.go
│ │ └── session.go
│ ├── http-fmp4/
│ │ ├── server.go
│ │ └── session.go
│ └── muxer/
│ ├── codec.go
│ ├── file_writer.go
│ ├── flac_box.go
│ ├── init.go
│ ├── init_track.go
│ ├── mp4_writer.go
│ ├── muxer.go
│ ├── muxer_part.go
│ ├── part.go
│ ├── part_sample.go
│ ├── part_track.go
│ ├── rtmp2fmp4.go
│ ├── seekablebuffer.go
│ ├── track.go
│ └── var.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
│ ├── rtppub/
│ │ ├── manager.go
│ │ └── manager_test.go
│ ├── rtppush/
│ │ ├── lower_push_session.go
│ │ └── lower_push_session_test.go
│ ├── server.go
│ ├── t_http_api.go
│ ├── util.go
│ └── xml.go
├── go.mod
├── go.sum
├── logic/
│ ├── gop_cache.go
│ ├── group.go
│ ├── group_manager.go
│ ├── group_test.go
│ ├── stat_aggregator.go
│ ├── stream_key.go
│ └── subscriber_stat.go
├── main.go
├── rtc/
│ ├── jessibucasession.go
│ ├── packer.go
│ ├── peerConnection.go
│ ├── server.go
│ ├── subscriber_stat.go
│ ├── unpacker.go
│ ├── whepsession.go
│ └── whipsession.go
├── run.sh
├── server/
│ ├── hook_builtin_http_plugin.go
│ ├── hook_filter.go
│ ├── hook_plugin.go
│ ├── http_notify.go
│ ├── middle.go
│ ├── router.go
│ ├── router_ctrl.go
│ ├── router_flv_proxy.go
│ ├── router_fmp4.go
│ ├── router_helper.go
│ ├── router_hook.go
│ ├── router_rtc.go
│ ├── router_stat.go
│ ├── router_test.go
│ ├── router_zlm_compat.go
│ ├── server.go
│ ├── stat_view.go
│ ├── zlm_compat_config.go
│ ├── zlm_compat_ffmpeg.go
│ ├── zlm_compat_test.go
│ └── zlm_compat_types.go
├── srt/
│ ├── pub.go
│ ├── server.go
│ ├── stream_id.go
│ └── sub.go
├── utils/
│ └── adjustdts.go
└── version/
├── README.md
├── v0.1.0.md
└── v0.2.0.md
SYMBOL INDEX (1109 symbols across 87 files)
FILE: config/config.go
type Config (line 12) | type Config struct
method SaveToFile (line 230) | func (c *Config) SaveToFile() error {
type SrtConfig (line 26) | type SrtConfig struct
type RtcConfig (line 31) | type RtcConfig struct
type HttpConfig (line 39) | type HttpConfig struct
type CtrlAuthWhitelist (line 49) | type CtrlAuthWhitelist struct
type Fmp4Config (line 54) | type Fmp4Config struct
type Fmp4HttpConfig (line 59) | type Fmp4HttpConfig struct
type Fmp4HlsConfig (line 63) | type Fmp4HlsConfig struct
type GB28181Config (line 71) | type GB28181Config struct
type GB28181MediaConfig (line 85) | type GB28181MediaConfig struct
type ZlmCompatHookConfig (line 93) | type ZlmCompatHookConfig struct
method HasZlmHooks (line 107) | func (c ZlmCompatHookConfig) HasZlmHooks() bool {
type HttpNotifyConfig (line 118) | type HttpNotifyConfig struct
type LogicConfig (line 141) | type LogicConfig struct
function Open (line 146) | func Open(filepath string) error {
function Unmarshal (line 159) | func Unmarshal(data []byte) error {
function unmarshalConfig (line 184) | func unmarshalConfig(data []byte, cfg *Config) error {
function GetConfig (line 224) | func GetConfig() *Config {
FILE: config/config_test.go
function TestUnmarshalStructuredConfig (line 8) | func TestUnmarshalStructuredConfig(t *testing.T) {
function TestUnmarshalLegacyConfig (line 41) | func TestUnmarshalLegacyConfig(t *testing.T) {
function TestUnmarshalStructuredFmp4ConfigKeepsExplicitZero (line 89) | func TestUnmarshalStructuredFmp4ConfigKeepsExplicitZero(t *testing.T) {
function TestUnmarshalStructuredLogicConfigKeepsExplicitZero (line 130) | func TestUnmarshalStructuredLogicConfigKeepsExplicitZero(t *testing.T) {
FILE: fmp4/hls/server.go
type HlsServer (line 14) | type HlsServer struct
method NewHlsSession (line 30) | func (s *HlsServer) NewHlsSession(streamName string) {
method NewHlsSessionWithAppName (line 34) | func (s *HlsServer) NewHlsSessionWithAppName(appName, streamName strin...
method OnMsg (line 40) | func (s *HlsServer) OnMsg(streamName string, msg base.RtmpMsg) {
method OnMsgWithAppName (line 44) | func (s *HlsServer) OnMsgWithAppName(appName, streamName string, msg b...
method OnStop (line 52) | func (s *HlsServer) OnStop(streamName string) {
method OnStopWithAppName (line 56) | func (s *HlsServer) OnStopWithAppName(appName, streamName string) {
method HandleRequest (line 66) | func (s *HlsServer) HandleRequest(ctx *gin.Context) {
method getSession (line 74) | func (s *HlsServer) getSession(appName, streamName string) (*HlsSessio...
method cleanInvalidSession (line 113) | func (s *HlsServer) cleanInvalidSession() {
function NewHlsServer (line 20) | func NewHlsServer(conf config.Fmp4HlsConfig) *HlsServer {
type sessionKey (line 101) | type sessionKey struct
function hlsSessionKey (line 106) | func hlsSessionKey(appName, streamName string) sessionKey {
FILE: fmp4/hls/session.go
type HlsSession (line 19) | type HlsSession struct
method OnMsg (line 84) | func (session *HlsSession) OnMsg(msg base.RtmpMsg) {
method drain (line 244) | func (session *HlsSession) drain() {
method OnStop (line 322) | func (session *HlsSession) OnStop() {
method HandleRequest (line 328) | func (session *HlsSession) HandleRequest(ctx *gin.Context) {
function NewHlsSession (line 39) | func NewHlsSession(streamName string, conf config.Fmp4HlsConfig) *HlsSes...
function NewHlsSessionWithAppName (line 43) | func NewHlsSessionWithAppName(appName, streamName string, conf config.Fm...
type Frame (line 333) | type Frame struct
FILE: fmp4/http-fmp4/server.go
type HttpFmp4Server (line 7) | type HttpFmp4Server struct
method HandleRequest (line 16) | func (s *HttpFmp4Server) HandleRequest(c *gin.Context) {
function NewHttpFmp4Server (line 10) | func NewHttpFmp4Server() *HttpFmp4Server {
FILE: fmp4/http-fmp4/session.go
type HttpFmp4Session (line 28) | type HttpFmp4Session struct
method OnInitFmp4 (line 59) | func (session *HttpFmp4Session) OnInitFmp4(init []byte) {
method OnFmp4Packets (line 63) | func (session *HttpFmp4Session) OnFmp4Packets(currentPart *muxer.Muxer...
method Dispose (line 71) | func (session *HttpFmp4Session) Dispose() error {
method dispose (line 74) | func (session *HttpFmp4Session) dispose() error {
method handleSession (line 86) | func (session *HttpFmp4Session) handleSession(c *gin.Context) {
method writeHttpHeader (line 135) | func (session *HttpFmp4Session) writeHttpHeader(header http.Header) er...
method write (line 157) | func (session *HttpFmp4Session) write(buf []byte) (err error) {
method OnMsg (line 163) | func (session *HttpFmp4Session) OnMsg(msg base.RtmpMsg) {
method OnStop (line 169) | func (session *HttpFmp4Session) OnStop() {
method GetSubscriberStat (line 175) | func (session *HttpFmp4Session) GetSubscriberStat() maxlogic.Subscribe...
function NewHttpFmp4Session (line 41) | func NewHttpFmp4Session(appName, streamid string) *HttpFmp4Session {
FILE: fmp4/muxer/codec.go
type Codec (line 9) | type Codec interface
type CodecH264 (line 15) | type CodecH264 struct
method IsVideo (line 20) | func (c *CodecH264) IsVideo() bool {
method Equal (line 24) | func (c *CodecH264) Equal(other Codec) bool {
method String (line 32) | func (c *CodecH264) String() string {
type CodecH265 (line 36) | type CodecH265 struct
method IsVideo (line 42) | func (c *CodecH265) IsVideo() bool {
method Equal (line 46) | func (c *CodecH265) Equal(other Codec) bool {
method String (line 54) | func (c *CodecH265) String() string {
type CodecAAC (line 58) | type CodecAAC struct
method IsVideo (line 63) | func (c *CodecAAC) IsVideo() bool {
method Equal (line 67) | func (c *CodecAAC) Equal(other Codec) bool {
method String (line 75) | func (c *CodecAAC) String() string {
type CodecOpus (line 79) | type CodecOpus struct
method IsVideo (line 83) | func (c *CodecOpus) IsVideo() bool {
method Equal (line 87) | func (c *CodecOpus) Equal(other Codec) bool {
method String (line 91) | func (c *CodecOpus) String() string {
FILE: fmp4/muxer/file_writer.go
type Fmp4Record (line 12) | type Fmp4Record struct
method createFile (line 45) | func (r *Fmp4Record) createFile() (err error) {
method WriteInitFmp4 (line 58) | func (r *Fmp4Record) WriteInitFmp4(init []byte) {
method WriteFmp4Segment (line 62) | func (r *Fmp4Record) WriteFmp4Segment(part *MuxerPart, lastSampleDurat...
method WriteMultiFile (line 71) | func (r *Fmp4Record) WriteMultiFile(part *MuxerPart, lastSampleDuratio...
method writeSingleFile (line 130) | func (r *Fmp4Record) writeSingleFile(part *MuxerPart, lastSampleDurati...
method Dispose (line 150) | func (r *Fmp4Record) Dispose() error {
function NewFmp4Record (line 27) | func NewFmp4Record(recordInterval int, enableRecordByInterval bool, stre...
type FileWriter (line 158) | type FileWriter struct
method Create (line 162) | func (fw *FileWriter) Create(filename string) (err error) {
method Write (line 167) | func (fw *FileWriter) Write(b []byte) (err error) {
method Dispose (line 175) | func (fw *FileWriter) Dispose() error {
method Name (line 182) | func (fw *FileWriter) Name() string {
FILE: fmp4/muxer/flac_box.go
function BoxTypeFlac (line 7) | func BoxTypeFlac() mp4.BoxType { return mp4.StrToBoxType("fLaC") }
function init (line 9) | func init() {
function BoxTypeDfla (line 31) | func BoxTypeDfla() mp4.BoxType {
function init (line 35) | func init() {
type DflaBox (line 39) | type DflaBox struct
method GetType (line 44) | func (d *DflaBox) GetType() mp4.BoxType {
method AddFlag (line 59) | func (d *DflaBox) AddFlag(uint32) {}
method CheckFlag (line 61) | func (d *DflaBox) CheckFlag(uint32) bool {
method GetFlags (line 65) | func (d *DflaBox) GetFlags() uint32 {
method GetVersion (line 69) | func (d *DflaBox) GetVersion() uint8 {
method RemoveFlag (line 73) | func (d *DflaBox) RemoveFlag(uint32) {
method SetFlags (line 76) | func (d *DflaBox) SetFlags(uint32) {
method SetVersion (line 79) | func (d *DflaBox) SetVersion(uint8) {
FILE: fmp4/muxer/init.go
constant objectTypeIndicationVisualISO14496part2 (line 14) | objectTypeIndicationVisualISO14496part2 = 0x20
constant objectTypeIndicationAudioISO14496part3 (line 15) | objectTypeIndicationAudioISO14496part3 = 0x40
constant objectTypeIndicationVisualISO1318part2Main (line 16) | objectTypeIndicationVisualISO1318part2Main = 0x61
constant objectTypeIndicationAudioISO11172part3 (line 17) | objectTypeIndicationAudioISO11172part3 = 0x6B
constant objectTypeIndicationVisualISO10918part1 (line 18) | objectTypeIndicationVisualISO10918part1 = 0x6C
constant streamTypeVisualStream (line 23) | streamTypeVisualStream = 0x04
constant streamTypeAudioStream (line 24) | streamTypeAudioStream = 0x05
function h265FindParams (line 27) | func h265FindParams(params []mp4.HEVCNaluArray) ([]byte, []byte, []byte,...
function h264FindParams (line 65) | func h264FindParams(avcc *mp4.AVCDecoderConfiguration) ([]byte, []byte, ...
function esdsFindDecoderConf (line 87) | func esdsFindDecoderConf(descriptors []mp4.Descriptor) *mp4.DecoderConfi...
function esdsFindDecoderSpecificInfo (line 96) | func esdsFindDecoderSpecificInfo(descriptors []mp4.Descriptor) []byte {
type Init (line 106) | type Init struct
method Marshal (line 111) | func (i *Init) Marshal(w io.WriteSeeker) error {
method Unmarshal (line 193) | func (i *Init) Unmarshal(r io.ReadSeeker) error {
FILE: fmp4/muxer/init_track.go
function boolToUint8 (line 12) | func boolToUint8(v bool) uint8 {
type InitTrack (line 20) | type InitTrack struct
method marshal (line 39) | func (it *InitTrack) marshal(w *mp4Writer) error {
function Uint32ToBoolSlice (line 548) | func Uint32ToBoolSlice(num uint32) [32]bool {
FILE: fmp4/muxer/mp4_writer.go
type mp4Writer (line 9) | type mp4Writer struct
method writeBoxStart (line 19) | func (w *mp4Writer) writeBoxStart(box mp4.IImmutableBox) (int, error) {
method writeBoxEnd (line 37) | func (w *mp4Writer) writeBoxEnd() error {
method writeBox (line 42) | func (w *mp4Writer) writeBox(box mp4.IImmutableBox) (int, error) {
method rewriteBox (line 56) | func (w *mp4Writer) rewriteBox(off int, box mp4.IImmutableBox) error {
function newMP4Writer (line 13) | func newMP4Writer(w io.WriteSeeker) *mp4Writer {
FILE: fmp4/muxer/muxer.go
function AudioTimeScale (line 13) | func AudioTimeScale(c Codec) uint32 {
function TsToTime (line 25) | func TsToTime(ts uint32) time.Duration {
type Muxer (line 29) | type Muxer struct
method WithLog (line 52) | func (m *Muxer) WithLog(log nazalog.Logger) {
method AddVideoTrack (line 56) | func (m *Muxer) AddVideoTrack(c Codec) {
method AddAudioTrack (line 75) | func (m *Muxer) AddAudioTrack(c Codec) {
method AudioTimeScale (line 81) | func (m *Muxer) AudioTimeScale() uint32 {
method GetInitMp4 (line 85) | func (m *Muxer) GetInitMp4() []byte {
method Pack (line 117) | func (m *Muxer) Pack(msg base.RtmpMsg) (*PartSample, error) {
method FeedVideo (line 127) | func (m *Muxer) FeedVideo(msg base.RtmpMsg) (*PartSample, error) {
method FeedAudio (line 201) | func (m *Muxer) FeedAudio(msg base.RtmpMsg) (*PartSample, error) {
function NewMuxer (line 45) | func NewMuxer() *Muxer {
FILE: fmp4/muxer/muxer_part.go
function durationGoToMp4 (line 5) | func durationGoToMp4(v time.Duration, timeScale uint32) uint64 {
function durationMp4ToGo (line 12) | func durationMp4ToGo(v uint64, timeScale uint32) time.Duration {
type MuxerPart (line 19) | type MuxerPart struct
method Bytes (line 44) | func (p *MuxerPart) Bytes() []byte {
method Duration (line 48) | func (p *MuxerPart) Duration() time.Duration {
method AudioTimeScale (line 52) | func (p *MuxerPart) AudioTimeScale() uint32 {
method Encode (line 56) | func (p *MuxerPart) Encode(lastSampleDuration time.Duration, end bool)...
method WriteVideo (line 102) | func (p *MuxerPart) WriteVideo(sample *PartSample) {
method WriteAudio (line 112) | func (p *MuxerPart) WriteAudio(sample *PartSample) {
method StartVideoDts (line 122) | func (p *MuxerPart) StartVideoDts() time.Duration {
method StartAudioDts (line 126) | func (p *MuxerPart) StartAudioDts() time.Duration {
method ResetStartVideoDts (line 130) | func (p *MuxerPart) ResetStartVideoDts() {
method ResetStartAudioDts (line 134) | func (p *MuxerPart) ResetStartAudioDts() {
method Clone (line 138) | func (p *MuxerPart) Clone() *MuxerPart {
method SetPartId (line 144) | func (p *MuxerPart) SetPartId(partId uint64) {
method CalcDuration (line 148) | func (p *MuxerPart) CalcDuration(newPartStartDts time.Duration, end bo...
method SetVideoStartDts (line 166) | func (p *MuxerPart) SetVideoStartDts(videoStartDTS time.Duration) {
method SetAudioStartDts (line 170) | func (p *MuxerPart) SetAudioStartDts(audioStartDTS time.Duration) {
function NewMuxerPart (line 36) | func NewMuxerPart(partId uint64, audioTimeScale uint32) *MuxerPart {
FILE: fmp4/muxer/part.go
constant trunFlagDataOffsetPreset (line 10) | trunFlagDataOffsetPreset = 0x01
constant trunFlagSampleDurationPresent (line 11) | trunFlagSampleDurationPresent = 0x100
constant trunFlagSampleSizePresent (line 12) | trunFlagSampleSizePresent = 0x200
constant trunFlagSampleFlagsPresent (line 13) | trunFlagSampleFlagsPresent = 0x400
constant trunFlagSampleCompositionTimeOffsetPresentOrV1 (line 14) | trunFlagSampleCompositionTimeOffsetPresentOrV1 = 0x800
constant sampleFlagIsNonSyncSample (line 16) | sampleFlagIsNonSyncSample = 1 << 16
type Part (line 20) | type Part struct
method Marshal (line 26) | func (p *Part) Marshal(w io.WriteSeeker) error {
FILE: fmp4/muxer/part_sample.go
type PartSample (line 8) | type PartSample struct
function avccMarshalSize (line 16) | func avccMarshalSize(au [][]byte) int {
function AVCCMarshal (line 26) | func AVCCMarshal(au [][]byte) ([]byte, error) {
function NewPartSampleH26x (line 45) | func NewPartSampleH26x(ptsOffset int32, randomAccessPresent bool, au [][...
FILE: fmp4/muxer/part_track.go
type PartTrack (line 6) | type PartTrack struct
method marshal (line 12) | func (pt *PartTrack) marshal(w *mp4Writer) (*mp4.Trun, int, error) {
FILE: fmp4/muxer/rtmp2fmp4.go
type IRtmp2Fmp4muxerObserver (line 14) | type IRtmp2Fmp4muxerObserver interface
type Rtmp2Fmp4Remuxer (line 21) | type Rtmp2Fmp4Remuxer struct
method WithLog (line 49) | func (m *Rtmp2Fmp4Remuxer) WithLog(log nazalog.Logger) *Rtmp2Fmp4Remux...
method FeedRtmpMessage (line 55) | func (m *Rtmp2Fmp4Remuxer) FeedRtmpMessage(msg base.RtmpMsg) {
method Push (line 59) | func (m *Rtmp2Fmp4Remuxer) Push(msg base.RtmpMsg) {
method drain (line 146) | func (m *Rtmp2Fmp4Remuxer) drain() {
method FlushLastSegment (line 168) | func (m *Rtmp2Fmp4Remuxer) FlushLastSegment() {
method Dispose (line 176) | func (m *Rtmp2Fmp4Remuxer) Dispose() {
method pack (line 179) | func (m *Rtmp2Fmp4Remuxer) pack(msg base.RtmpMsg) {
method partId (line 339) | func (m *Rtmp2Fmp4Remuxer) partId() uint64 {
function NewRtmp2Fmp4Remuxer (line 34) | func NewRtmp2Fmp4Remuxer(observer IRtmp2Fmp4muxerObserver) *Rtmp2Fmp4Rem...
FILE: fmp4/muxer/seekablebuffer.go
type Buffer (line 10) | type Buffer struct
method Write (line 16) | func (b *Buffer) Write(p []byte) (int, error) {
method Read (line 35) | func (b *Buffer) Read(_ []byte) (int, error) {
method Seek (line 40) | func (b *Buffer) Seek(offset int64, whence int) (int64, error) {
method Reset (line 70) | func (b *Buffer) Reset() {
FILE: fmp4/muxer/track.go
type Track (line 3) | type Track struct
function NewTrack (line 12) | func NewTrack(codec Codec, trackId, timeSacle uint32) *Track {
FILE: gb28181/auth.go
type Authorization (line 11) | type Authorization struct
method Verify (line 15) | func (a *Authorization) Verify(username, passwd, realm, nonce string) ...
method getDigest (line 36) | func (a *Authorization) getDigest(raw string) string {
FILE: gb28181/avail_conn_pool.go
type OnListenWithPort (line 20) | type OnListenWithPort
type AvailConnPool (line 23) | type AvailConnPool struct
method WithListenWithPort (line 39) | func (a *AvailConnPool) WithListenWithPort(listenWithPort OnListenWith...
method Acquire (line 42) | func (a *AvailConnPool) Acquire() (net.Listener, uint16, error) {
method Peek (line 72) | func (a *AvailConnPool) Peek() (uint16, error) {
method ListenWithPort (line 79) | func (a *AvailConnPool) ListenWithPort(port uint16) (net.Listener, err...
method nextPort (line 85) | func (a *AvailConnPool) nextPort(p uint16) uint16 {
function NewAvailConnPool (line 32) | func NewAvailConnPool(minPort uint16, maxPort uint16) *AvailConnPool {
FILE: gb28181/channel.go
type Channel (line 20) | type Channel struct
method WithMediaServer (line 65) | func (channel *Channel) WithMediaServer(observer IMediaOpObserver) {
method TryAutoInvite (line 69) | func (channel *Channel) TryAutoInvite(opt *InviteOptions, streamName s...
method CanInvite (line 75) | func (channel *Channel) CanInvite(streamName string) bool {
method Invite (line 146) | func (channel *Channel) Invite(opt *InviteOptions, streamName string, ...
method GetCallId (line 262) | func (channel *Channel) GetCallId() string {
method stopMediaServer (line 270) | func (channel *Channel) stopMediaServer() (err error) {
method byeClear (line 280) | func (channel *Channel) byeClear() (err error) {
method Bye (line 286) | func (channel *Channel) Bye(streamName string) (err error) {
method CreateRequst (line 300) | func (channel *Channel) CreateRequst(Method sip.RequestMethod, conf co...
method PtzDirection (line 357) | func (channel *Channel) PtzDirection(direction *PtzDirection) error {
method PtzZoom (line 379) | func (channel *Channel) PtzZoom(zoom *PtzZoom) error {
method PtzFi (line 397) | func (channel *Channel) PtzFi(fi *PtzFi) error {
method PtzPreset (line 417) | func (channel *Channel) PtzPreset(ptzPreset *PtzPreset) error {
method PtzStop (line 445) | func (channel *Channel) PtzStop(stop *PtzStop) error {
method sipMessage (line 459) | func (channel *Channel) sipMessage(xml string) error {
type ChannelInfo (line 35) | type ChannelInfo struct
type ChannelStatus (line 58) | type ChannelStatus
constant ChannelOnStatus (line 61) | ChannelOnStatus = "ON"
constant ChannelOffStatus (line 62) | ChannelOffStatus = "OFF"
FILE: gb28181/device.go
constant TIME_LAYOUT (line 17) | TIME_LAYOUT = "2006-01-02T15:04:05"
type DeviceStatus (line 25) | type DeviceStatus
constant DeviceRegisterStatus (line 28) | DeviceRegisterStatus = "REGISTER"
constant DeviceRecoverStatus (line 29) | DeviceRecoverStatus = "RECOVER"
constant DeviceOnlineStatus (line 30) | DeviceOnlineStatus = "ONLINE"
constant DeviceOfflineStatus (line 31) | DeviceOfflineStatus = "OFFLINE"
constant DeviceAlarmedStatus (line 32) | DeviceAlarmedStatus = "ALARMED"
type Device (line 35) | type Device struct
method WithMediaServer (line 67) | func (d *Device) WithMediaServer(observer IMediaOpObserver) {
method WithSipSvr (line 71) | func (d *Device) WithSipSvr(sipSvr gosip.Server) *Device {
method syncChannels (line 76) | func (d *Device) syncChannels() {
method UpdateChannels (line 85) | func (d *Device) UpdateChannels(list ...ChannelInfo) {
method addOrUpdateChannel (line 108) | func (d *Device) addOrUpdateChannel(info ChannelInfo) (c *Channel) {
method Catalog (line 124) | func (d *Device) Catalog(conf config.GB28181Config) int {
method CreateRequest (line 146) | func (d *Device) CreateRequest(Method sip.RequestMethod, conf config.G...
method Subscribe (line 188) | func (d *Device) Subscribe(conf config.GB28181Config) int {
method QueryDeviceInfo (line 215) | func (d *Device) QueryDeviceInfo(conf config.GB28181Config) {
method UpdateChannelStatus (line 234) | func (d *Device) UpdateChannelStatus(deviceList []*notifyMessage, conf...
method channelOnline (line 300) | func (d *Device) channelOnline(channelId string) {
method channelOffline (line 310) | func (d *Device) channelOffline(channelId string) {
method deleteChannel (line 320) | func (d *Device) deleteChannel(channelId string) {
method UpdateChannelPosition (line 325) | func (d *Device) UpdateChannelPosition(channelId string, gpsTime strin...
method SipRequestForResponse (line 341) | func (d *Device) SipRequestForResponse(request sip.Request) (sip.Respo...
FILE: gb28181/http_logic.go
type GbLogic (line 9) | type GbLogic struct
method GetDeviceInfos (line 25) | func (g *GbLogic) GetDeviceInfos(c *gin.Context) {
method StartPlay (line 30) | func (g *GbLogic) StartPlay(c *gin.Context) {
method StopPlay (line 56) | func (g *GbLogic) StopPlay(c *gin.Context) {
method PtzDirection (line 77) | func (g *GbLogic) PtzDirection(c *gin.Context) {
method PtzZoom (line 98) | func (g *GbLogic) PtzZoom(c *gin.Context) {
method PtzFi (line 119) | func (g *GbLogic) PtzFi(c *gin.Context) {
method PtzPreset (line 140) | func (g *GbLogic) PtzPreset(c *gin.Context) {
method PtzStop (line 160) | func (g *GbLogic) PtzStop(c *gin.Context) {
method UpdateAllNotify (line 177) | func (g *GbLogic) UpdateAllNotify(c *gin.Context) {
method UpdateNotify (line 181) | func (g *GbLogic) UpdateNotify(c *gin.Context) {
function NewGbLogic (line 16) | func NewGbLogic(s *GB28181Server) *GbLogic {
FILE: gb28181/inviteoption.go
type InviteOptions (line 8) | type InviteOptions struct
method IsLive (line 16) | func (o InviteOptions) IsLive() bool {
method String (line 20) | func (o InviteOptions) String() string {
method CreateSSRC (line 24) | func (o *InviteOptions) CreateSSRC(serial string, number uint16) {
FILE: gb28181/mediaserver/conn.go
type Frame (line 25) | type Frame struct
type Conn (line 33) | type Conn struct
method SetMediaServer (line 75) | func (c *Conn) SetMediaServer(mediaServer *GB28181MediaServer) {
method SetKey (line 78) | func (c *Conn) SetKey(key string) {
method SetPreferMediaKeyLookup (line 81) | func (c *Conn) SetPreferMediaKeyLookup(prefer bool) {
method SetReadTimeout (line 84) | func (c *Conn) SetReadTimeout(timeout time.Duration) {
method Serve (line 87) | func (c *Conn) Serve() (err error) {
method Demuxer (line 209) | func (c *Conn) Demuxer(data []byte) error {
method OnFrame (line 240) | func (c *Conn) OnFrame(frame []byte, cid mpegps.PsStreamType, pts uint...
method Close (line 293) | func (c *Conn) Close() {
function NewConn (line 61) | func NewConn(conn net.Conn, observer IGbObserver, lal logic.ILalServer) ...
function getPayloadType (line 298) | func getPayloadType(cid mpegps.PsStreamType) base.AvPacketPt {
function splitPsPackets (line 315) | func splitPsPackets(data []byte) [][]byte {
FILE: gb28181/mediaserver/mediaserver_t.go
type MediaInfo (line 3) | type MediaInfo struct
method Clear (line 12) | func (m *MediaInfo) Clear() (err error) {
FILE: gb28181/mediaserver/server.go
constant defaultReadTimeout (line 14) | defaultReadTimeout = 10 * time.Second
type IGbObserver (line 16) | type IGbObserver interface
type GB28181MediaServer (line 23) | type GB28181MediaServer struct
method WithPreferMediaKeyLookup (line 49) | func (s *GB28181MediaServer) WithPreferMediaKeyLookup(prefer bool) *GB...
method WithReadTimeout (line 54) | func (s *GB28181MediaServer) WithReadTimeout(timeout time.Duration) *G...
method GetListenerPort (line 59) | func (s *GB28181MediaServer) GetListenerPort() uint16 {
method Start (line 62) | func (s *GB28181MediaServer) Start(listener net.Listener) (err error) {
method CloseConn (line 105) | func (s *GB28181MediaServer) CloseConn(streamName string) {
method Dispose (line 111) | func (s *GB28181MediaServer) Dispose() {
function NewGB28181MediaServer (line 39) | func NewGB28181MediaServer(listenPort int, mediaKey string, observer IGb...
FILE: gb28181/mpegps/bitstream.go
type BitStream (line 9) | type BitStream struct
method Uint8 (line 27) | func (bs *BitStream) Uint8(n int) uint8 {
method Uint16 (line 31) | func (bs *BitStream) Uint16(n int) uint16 {
method Uint32 (line 35) | func (bs *BitStream) Uint32(n int) uint32 {
method GetBytes (line 39) | func (bs *BitStream) GetBytes(n int) []byte {
method GetBits (line 53) | func (bs *BitStream) GetBits(n int) uint64 {
method GetBit (line 88) | func (bs *BitStream) GetBit() uint8 {
method SkipBits (line 101) | func (bs *BitStream) SkipBits(n int) {
method Markdot (line 113) | func (bs *BitStream) Markdot() {
method DistanceFromMarkDot (line 118) | func (bs *BitStream) DistanceFromMarkDot() int {
method RemainBytes (line 124) | func (bs *BitStream) RemainBytes() int {
method RemainBits (line 132) | func (bs *BitStream) RemainBits() int {
method Bits (line 141) | func (bs *BitStream) Bits() []byte {
method RemainData (line 145) | func (bs *BitStream) RemainData() []byte {
method ReadUE (line 150) | func (bs *BitStream) ReadUE() uint64 {
method ReadSE (line 163) | func (bs *BitStream) ReadSE() int64 {
method ByteOffset (line 172) | func (bs *BitStream) ByteOffset() int {
method UnRead (line 176) | func (bs *BitStream) UnRead(n int) {
method NextBits (line 192) | func (bs *BitStream) NextBits(n int) uint64 {
method EOS (line 198) | func (bs *BitStream) EOS() bool {
function NewBitStream (line 17) | func NewBitStream(buf []byte) *BitStream {
type BitStreamWriter (line 202) | type BitStreamWriter struct
method expandSpace (line 220) | func (bsw *BitStreamWriter) expandSpace(n int) {
method ByteOffset (line 234) | func (bsw *BitStreamWriter) ByteOffset() int {
method BitOffset (line 238) | func (bsw *BitStreamWriter) BitOffset() int {
method Markdot (line 242) | func (bsw *BitStreamWriter) Markdot() {
method DistanceFromMarkDot (line 247) | func (bsw *BitStreamWriter) DistanceFromMarkDot() int {
method PutByte (line 253) | func (bsw *BitStreamWriter) PutByte(v byte) {
method PutBytes (line 265) | func (bsw *BitStreamWriter) PutBytes(v []byte) {
method PutRepetValue (line 274) | func (bsw *BitStreamWriter) PutRepetValue(v byte, n int) {
method PutUint8 (line 285) | func (bsw *BitStreamWriter) PutUint8(v uint8, n int) {
method PutUint16 (line 289) | func (bsw *BitStreamWriter) PutUint16(v uint16, n int) {
method PutUint32 (line 293) | func (bsw *BitStreamWriter) PutUint32(v uint32, n int) {
method PutUint64 (line 297) | func (bsw *BitStreamWriter) PutUint64(v uint64, n int) {
method SetByte (line 322) | func (bsw *BitStreamWriter) SetByte(v byte, where int) {
method SetUint16 (line 326) | func (bsw *BitStreamWriter) SetUint16(v uint16, where int) {
method Bits (line 330) | func (bsw *BitStreamWriter) Bits() []byte {
method FillRemainData (line 342) | func (bsw *BitStreamWriter) FillRemainData(v byte) {
method Reset (line 350) | func (bsw *BitStreamWriter) Reset() {
function NewBitStreamWriter (line 210) | func NewBitStreamWriter(n int) *BitStreamWriter {
FILE: gb28181/mpegps/pes_proto.go
type TsStreamType (line 8) | type TsStreamType
constant TsStreamAudioMpeg1 (line 11) | TsStreamAudioMpeg1 TsStreamType = 0x03
constant TsStreamAudioMpeg2 (line 12) | TsStreamAudioMpeg2 TsStreamType = 0x04
constant TsStreamAac (line 13) | TsStreamAac TsStreamType = 0x0F
constant TsStreamH264 (line 14) | TsStreamH264 TsStreamType = 0x1B
constant TsStreamH265 (line 15) | TsStreamH265 TsStreamType = 0x24
type PesStreamId (line 21) | type PesStreamId
constant PesStreamEnd (line 24) | PesStreamEnd PesStreamId = 0xB9
constant PesStreamStart (line 25) | PesStreamStart PesStreamId = 0xBA
constant PesStreamSystemHead (line 26) | PesStreamSystemHead PesStreamId = 0xBB
constant PesStreamMap (line 27) | PesStreamMap PesStreamId = 0xBC
constant PesStreamPrivate (line 28) | PesStreamPrivate PesStreamId = 0xBD
constant PesStreamAudio (line 29) | PesStreamAudio PesStreamId = 0xC0
constant PesStreamVideo (line 30) | PesStreamVideo PesStreamId = 0xE0
type Display (line 33) | type Display interface
function findPesIdByStreamType (line 37) | func findPesIdByStreamType(cid TsStreamType) PesStreamId {
type PesPacket (line 48) | type PesPacket struct
method PrettyPrint (line 88) | func (pkg *PesPacket) PrettyPrint(file *os.File) {
method Decode (line 144) | func (pkg *PesPacket) Decode(bs *BitStream) error {
method DecodeMpeg1 (line 247) | func (pkg *PesPacket) DecodeMpeg1(bs *BitStream) error {
method Encode (line 307) | func (pkg *PesPacket) Encode(bsw *BitStreamWriter) {
function NewPesPacket (line 84) | func NewPesPacket() *PesPacket {
FILE: gb28181/mpegps/ps_demuxer.go
type psStream (line 11) | type psStream struct
method setCid (line 26) | func (p *psStream) setCid(cid PsStreamType) {
method clearBuf (line 30) | func (p *psStream) clearBuf() {
function newPsStream (line 19) | func newPsStream(sid uint8, cid PsStreamType) *psStream {
type PsDemuxer (line 34) | type PsDemuxer struct
method Input (line 61) | func (psDemuxer *PsDemuxer) Input(data []byte) error {
method Flush (line 210) | func (psDemuxer *PsDemuxer) Flush() {
method guessCodecid (line 221) | func (psDemuxer *PsDemuxer) guessCodecid(stream *psStream) {
method demuxPespacket (line 238) | func (psDemuxer *PsDemuxer) demuxPespacket(stream *psStream, pes *PesP...
method demuxAudio (line 255) | func (psDemuxer *PsDemuxer) demuxAudio(stream *psStream, pes *PesPacke...
method demuxH26x (line 262) | func (psDemuxer *PsDemuxer) demuxH26x(stream *psStream, pes *PesPacket...
function NewPsDemuxer (line 50) | func NewPsDemuxer() *PsDemuxer {
FILE: gb28181/mpegps/ps_demuxer_test.go
function TestPSDemuxer_Input (line 24) | func TestPSDemuxer_Input(t *testing.T) {
function TestPSDemuxer (line 89) | func TestPSDemuxer(t *testing.T) {
function fileExists (line 181) | func fileExists(fileName string) (bool, error) {
function writeFile (line 191) | func writeFile(filename string, buffer []byte) (err error) {
FILE: gb28181/mpegps/ps_muxer.go
type PsMuxer (line 9) | type PsMuxer struct
method AddStream (line 28) | func (muxer *PsMuxer) AddStream(cid PsStreamType) uint8 {
method Write (line 50) | func (muxer *PsMuxer) Write(sid uint8, frame []byte, pts uint64, dts u...
function NewPsMuxer (line 16) | func NewPsMuxer() *PsMuxer {
FILE: gb28181/mpegps/ps_proto.go
type Error (line 10) | type Error interface
type needmoreError (line 18) | type needmoreError struct
method Error (line 20) | func (e *needmoreError) Error() string { return "need more by...
method NeedMore (line 21) | func (e *needmoreError) NeedMore() bool { return true }
method ParserError (line 22) | func (e *needmoreError) ParserError() bool { return false }
method StreamIdNotFound (line 23) | func (e *needmoreError) StreamIdNotFound() bool { return false }
type parserError (line 27) | type parserError struct
method Error (line 29) | func (e *parserError) Error() string { return "parser packet ...
method NeedMore (line 30) | func (e *parserError) NeedMore() bool { return false }
method ParserError (line 31) | func (e *parserError) ParserError() bool { return true }
method StreamIdNotFound (line 32) | func (e *parserError) StreamIdNotFound() bool { return false }
type sidNotFoundError (line 36) | type sidNotFoundError struct
method Error (line 38) | func (e *sidNotFoundError) Error() string { return "stream id...
method NeedMore (line 39) | func (e *sidNotFoundError) NeedMore() bool { return false }
method ParserError (line 40) | func (e *sidNotFoundError) ParserError() bool { return false }
method StreamIdNotFound (line 41) | func (e *sidNotFoundError) StreamIdNotFound() bool { return true }
type PsStreamType (line 43) | type PsStreamType
constant PsStreamUnknow (line 46) | PsStreamUnknow PsStreamType = 0xFF
constant PsStreamAac (line 47) | PsStreamAac PsStreamType = 0x0F
constant PsStreamH264 (line 48) | PsStreamH264 PsStreamType = 0x1B
constant PsStreamH265 (line 49) | PsStreamH265 PsStreamType = 0x24
constant PsStreamG711A (line 50) | PsStreamG711A PsStreamType = 0x90
constant PsStreamG711U (line 51) | PsStreamG711U PsStreamType = 0x91
type PsPackHeader (line 79) | type PsPackHeader struct
method PrettyPrint (line 87) | func (psPackHeader *PsPackHeader) PrettyPrint(file *os.File) {
method Decode (line 95) | func (psPackHeader *PsPackHeader) Decode(bs *BitStream) error {
method decodeMpeg2 (line 119) | func (psPackHeader *PsPackHeader) decodeMpeg2(bs *BitStream) error {
method decodeMpeg1 (line 142) | func (psPackHeader *PsPackHeader) decodeMpeg1(bs *BitStream) error {
method Encode (line 158) | func (psPackHeader *PsPackHeader) Encode(bsw *BitStreamWriter) {
type ElementaryStream (line 177) | type ElementaryStream struct
function NewElementaryStream (line 183) | func NewElementaryStream(sid uint8) *ElementaryStream {
type SystemHeader (line 212) | type SystemHeader struct
method PrettyPrint (line 225) | func (sh *SystemHeader) PrettyPrint(file *os.File) {
method Encode (line 243) | func (sh *SystemHeader) Encode(bsw *BitStreamWriter) {
method Decode (line 270) | func (sh *SystemHeader) Decode(bs *BitStream) error {
type ElementaryStreamElem (line 314) | type ElementaryStreamElem struct
function NewElementaryStreamElem (line 320) | func NewElementaryStreamElem(stype uint8, esid uint8) *ElementaryStreamE...
type ProgramStreamMap (line 352) | type ProgramStreamMap struct
method PrettyPrint (line 362) | func (psm *ProgramStreamMap) PrettyPrint(file *os.File) {
method Encode (line 387) | func (psm *ProgramStreamMap) Encode(bsw *BitStreamWriter) {
method Decode (line 413) | func (psm *ProgramStreamMap) Decode(bs *BitStream) error {
type ProgramStreamDirectory (line 472) | type ProgramStreamDirectory struct
method Decode (line 476) | func (psd *ProgramStreamDirectory) Decode(bs *BitStream) error {
type CommonPesPacket (line 493) | type CommonPesPacket struct
method Decode (line 498) | func (compes *CommonPesPacket) Decode(bs *BitStream) error {
type PsPacket (line 513) | type PsPacket struct
FILE: gb28181/mpegps/util.go
constant CodecUnknown (line 9) | CodecUnknown = iota
constant CodecH264 (line 10) | CodecH264
constant CodecH265 (line 11) | CodecH265
constant CodecH266 (line 12) | CodecH266
constant CodecMpeg4 (line 13) | CodecMpeg4
function CalcCrc32 (line 62) | func CalcCrc32(crc uint32, buffer []byte) uint32 {
type StartCodeType (line 70) | type StartCodeType
constant StartCode3 (line 73) | StartCode3 StartCodeType = 3
constant STartCode4 (line 74) | STartCode4 StartCodeType = 4
function FindStartCode (line 77) | func FindStartCode(nalu []byte, offset int) (int, StartCodeType) {
function SplitFrame (line 91) | func SplitFrame(frames []byte, onFrame func(nalu []byte) bool) {
function H264NaluType (line 108) | func H264NaluType(h264 []byte) uint8 {
function H265NaluType (line 112) | func H265NaluType(h265 []byte) uint8 {
function mpegH264FindNALU (line 117) | func mpegH264FindNALU(data []byte) (int, int, error) {
function mpegH26xVerify (line 136) | func mpegH26xVerify(data []byte) (int, error) {
function audioVerify (line 187) | func audioVerify(data []byte) PsStreamType {
FILE: gb28181/ptz.go
type MessagePtz (line 8) | type MessagePtz struct
constant DeviceControl (line 16) | DeviceControl = "DeviceControl"
constant PTZFirstByte (line 17) | PTZFirstByte = 0xA5
constant PresetSet (line 19) | PresetSet = 0x81
constant PresetCall (line 20) | PresetCall = 0x82
constant PresetDel (line 21) | PresetDel = 0x83
constant CruiseAdd (line 25) | CruiseAdd = 0x84
constant CruiseDel (line 26) | CruiseDel = 0x85
constant CruiseSetSpeed (line 27) | CruiseSetSpeed = 0x86
constant CruiseStopTime (line 28) | CruiseStopTime = 0x87
constant CruiseStart (line 29) | CruiseStart = 0x88
constant ScanningStart (line 32) | ScanningStart = 0x89
constant ScanningSpeed (line 33) | ScanningSpeed = 0x8A
type PtzHead (line 55) | type PtzHead struct
function getAssembleCode (line 62) | func getAssembleCode() uint8 {
function getVerificationCode (line 65) | func getVerificationCode(ptz []byte) {
type Ptz (line 81) | type Ptz struct
method Pack (line 91) | func (p *Ptz) Pack() string {
method Stop (line 128) | func (p *Ptz) Stop() string {
type Fi (line 147) | type Fi struct
method Pack (line 155) | func (f *Fi) Pack() string {
type Preset (line 185) | type Preset struct
method Pack (line 190) | func (p *Preset) Pack() string {
type Cruise (line 212) | type Cruise struct
method Pack (line 218) | func (c *Cruise) Pack() string {
type Scanning (line 238) | type Scanning struct
FILE: gb28181/rtppub/manager.go
constant defaultPortMin (line 19) | defaultPortMin = 30000
constant defaultPortMaxIncrement (line 20) | defaultPortMaxIncrement = 3000
type Manager (line 28) | type Manager struct
method Start (line 79) | func (m *Manager) Start(req base.ApiCtrlStartRtpPubReq) (ret base.ApiC...
method Stop (line 160) | func (m *Manager) Stop(streamName, sessionID string) (*Session, error) {
method GetMediaInfoByKey (line 178) | func (m *Manager) GetMediaInfoByKey(key string) (*mediaserver.MediaInf...
method CheckSsrc (line 189) | func (m *Manager) CheckSsrc(ssrc uint32) (*mediaserver.MediaInfo, bool) {
method NotifyClose (line 193) | func (m *Manager) NotifyClose(streamName string) {
method UpdatePortRange (line 198) | func (m *Manager) UpdatePortRange(portMin, portMax int) {
method OnRtpPacket (line 206) | func (m *Manager) OnRtpPacket(streamName string, mediaKey string) {
method stopSession (line 215) | func (m *Manager) stopSession(session *Session) {
method watchTimeout (line 232) | func (m *Manager) watchTimeout(session *Session, timeout time.Duration) {
method listen (line 259) | func (m *Manager) listen(port int, network string) (net.Listener, int,...
type Session (line 40) | type Session struct
function NewManager (line 54) | func NewManager(lalServer logic.ILalServer, mediaConfig config.GB28181Me...
function listenPort (line 279) | func listenPort(port int, network string) (net.Listener, error) {
FILE: gb28181/rtppub/manager_test.go
function freeTCPPort (line 13) | func freeTCPPort(t *testing.T) uint16 {
function newTestManager (line 25) | func newTestManager(t *testing.T) *Manager {
function TestManagerStartStopBySessionID (line 31) | func TestManagerStartStopBySessionID(t *testing.T) {
function TestManagerRejectsDuplicateStream (line 60) | func TestManagerRejectsDuplicateStream(t *testing.T) {
function TestManagerTimeoutRemovesIdleSession (line 85) | func TestManagerTimeoutRemovesIdleSession(t *testing.T) {
function TestNewManagerUsesConfiguredPortRangeAfterListenPort (line 118) | func TestNewManagerUsesConfiguredPortRangeAfterListenPort(t *testing.T) {
FILE: gb28181/rtppush/lower_push_session.go
constant lowerPushNetworkUDP (line 20) | lowerPushNetworkUDP = "udp"
constant lowerPushNetworkTCP (line 21) | lowerPushNetworkTCP = "tcp"
constant lowerPushRtpPacketMax (line 24) | lowerPushRtpPacketMax = 1400
constant lowerPushQueueMax (line 26) | lowerPushQueueMax = 256
type LowerPushSession (line 34) | type LowerPushSession struct
method WithStreamName (line 94) | func (s *LowerPushSession) WithStreamName(streamName string) *LowerPus...
method WithLogPrefix (line 100) | func (s *LowerPushSession) WithLogPrefix(prefix string) *LowerPushSess...
method SetLocalIP (line 106) | func (s *LowerPushSession) SetLocalIP(localIP string) {
method SetLocalPort (line 111) | func (s *LowerPushSession) SetLocalPort(localPort int) {
method SetPeerIP (line 116) | func (s *LowerPushSession) SetPeerIP(peerIP string) {
method SetPeerPort (line 121) | func (s *LowerPushSession) SetPeerPort(peerPort int) {
method SetSsrc (line 126) | func (s *LowerPushSession) SetSsrc(ssrc uint32) {
method Start (line 131) | func (s *LowerPushSession) Start(network string) error {
method startUDP (line 143) | func (s *LowerPushSession) startUDP() error {
method startTCP (line 167) | func (s *LowerPushSession) startTCP() error {
method OnMsg (line 187) | func (s *LowerPushSession) OnMsg(msg lalbase.RtmpMsg) {
method OnStop (line 209) | func (s *LowerPushSession) OnStop() {
method WriteRtpPacket (line 214) | func (s *LowerPushSession) WriteRtpPacket(pkt rtprtcp.RtpPacket) error {
method WriteRtpPsPacket (line 231) | func (s *LowerPushSession) WriteRtpPsPacket(buf []byte) error {
method writeUDP (line 250) | func (s *LowerPushSession) writeUDP(payload []byte) error {
method writeTCP (line 260) | func (s *LowerPushSession) writeTCP(payload []byte) error {
method Dispose (line 271) | func (s *LowerPushSession) Dispose() error {
method UniqueKey (line 289) | func (s *LowerPushSession) UniqueKey() string {
method StreamName (line 294) | func (s *LowerPushSession) StreamName() string {
method LocalAddr (line 302) | func (s *LowerPushSession) LocalAddr() net.Addr {
method RemoteAddr (line 313) | func (s *LowerPushSession) RemoteAddr() net.Addr {
method consumeControlMsg (line 324) | func (s *LowerPushSession) consumeControlMsg(msg lalbase.RtmpMsg) bool {
method updateVideoHeader (line 380) | func (s *LowerPushSession) updateVideoHeader(msg lalbase.RtmpMsg) error {
method updateAacHeader (line 415) | func (s *LowerPushSession) updateAacHeader(msg lalbase.RtmpMsg) error {
method shouldDrain (line 430) | func (s *LowerPushSession) shouldDrain(msg lalbase.RtmpMsg) bool {
method drain (line 450) | func (s *LowerPushSession) drain() {
method feedRtmpMsg (line 467) | func (s *LowerPushSession) feedRtmpMsg(msg lalbase.RtmpMsg) error {
method feedVideo (line 482) | func (s *LowerPushSession) feedVideo(msg lalbase.RtmpMsg) error {
method feedAudio (line 593) | func (s *LowerPushSession) feedAudio(msg lalbase.RtmpMsg) error {
method nextSeq (line 620) | func (s *LowerPushSession) nextSeq() uint16 {
method packRtp (line 626) | func (s *LowerPushSession) packRtp(buf []byte, timestamp uint32) []rtp...
function NewLowerPushSession (line 74) | func NewLowerPushSession() *LowerPushSession {
FILE: gb28181/rtppush/lower_push_session_test.go
function TestLowerPushSessionUDPWriteRtpPacket (line 15) | func TestLowerPushSessionUDPWriteRtpPacket(t *testing.T) {
function TestLowerPushSessionTCPWriteRtpPsPacket (line 61) | func TestLowerPushSessionTCPWriteRtpPsPacket(t *testing.T) {
function TestLowerPushSessionWriteBeforeStart (line 114) | func TestLowerPushSessionWriteBeforeStart(t *testing.T) {
function TestLowerPushSessionOnMsgVideoUDP (line 122) | func TestLowerPushSessionOnMsgVideoUDP(t *testing.T) {
function TestLowerPushSessionOnMsgAudioTCP (line 194) | func TestLowerPushSessionOnMsgAudioTCP(t *testing.T) {
function makeTestRtpPacket (line 265) | func makeTestRtpPacket(payload []byte) rtprtcp.RtpPacket {
function makeAvcSeqHeaderMsg (line 274) | func makeAvcSeqHeaderMsg() lalbase.RtmpMsg {
function makeAvcKeyFrameMsg (line 293) | func makeAvcKeyFrameMsg() lalbase.RtmpMsg {
function makeAacSeqHeaderMsg (line 316) | func makeAacSeqHeaderMsg() lalbase.RtmpMsg {
function makeAacRawMsg (line 328) | func makeAacRawMsg() lalbase.RtmpMsg {
function makeG711AMsg (line 341) | func makeG711AMsg() lalbase.RtmpMsg {
function min (line 353) | func min(a, b int) int {
function TestFixturesAreValid (line 360) | func TestFixturesAreValid(t *testing.T) {
FILE: gb28181/server.go
type IMediaOpObserver (line 26) | type IMediaOpObserver interface
type GB28181Server (line 30) | type GB28181Server struct
method Start (line 114) | func (s *GB28181Server) Start() {
method newSipServer (line 119) | func (s *GB28181Server) newSipServer(network string) gosip.Server {
method Dispose (line 140) | func (s *GB28181Server) Dispose() {
method OnStartMediaServer (line 152) | func (s *GB28181Server) OnStartMediaServer(netWork string, singlePort ...
method OnStopMediaServer (line 222) | func (s *GB28181Server) OnStopMediaServer(netWork string, singlePort b...
method CheckSsrc (line 261) | func (s *GB28181Server) CheckSsrc(ssrc uint32) (*mediaserver.MediaInfo...
method GetMediaInfoByKey (line 288) | func (s *GB28181Server) GetMediaInfoByKey(key string) (*mediaserver.Me...
method NotifyClose (line 316) | func (s *GB28181Server) NotifyClose(streamName string) {
method OnRtpPacket (line 339) | func (s *GB28181Server) OnRtpPacket(streamName string, mediaKey string) {
method startJob (line 342) | func (s *GB28181Server) startJob() {
method removeBanDevice (line 357) | func (s *GB28181Server) removeBanDevice() {
method statusCheck (line 370) | func (s *GB28181Server) statusCheck() {
method getDeviceInfos (line 388) | func (s *GB28181Server) getDeviceInfos() (deviceInfos *DeviceInfos) {
method GetAllSyncChannels (line 421) | func (s *GB28181Server) GetAllSyncChannels() {
method GetSyncChannels (line 428) | func (s *GB28181Server) GetSyncChannels(deviceId string) bool {
method FindChannel (line 437) | func (s *GB28181Server) FindChannel(deviceId string, channelId string)...
method OnRegister (line 450) | func (s *GB28181Server) OnRegister(req sip.Request, tx sip.ServerTrans...
method OnMessage (line 572) | func (s *GB28181Server) OnMessage(req sip.Request, tx sip.ServerTransa...
method OnNotify (line 651) | func (s *GB28181Server) OnNotify(req sip.Request, tx sip.ServerTransac...
method OnBye (line 696) | func (s *GB28181Server) OnBye(req sip.Request, tx sip.ServerTransactio...
method StoreDevice (line 717) | func (s *GB28181Server) StoreDevice(id string, req sip.Request) (d *De...
method RecoverDevice (line 766) | func (s *GB28181Server) RecoverDevice(d *Device, req sip.Request) {
constant MaxRegisterCount (line 49) | MaxRegisterCount = 3
function init (line 56) | func init() {
function NewGB28181Server (line 60) | func NewGB28181Server(conf config.GB28181Config, lal logic.ILalServer) *...
type notifyMessage (line 791) | type notifyMessage struct
FILE: gb28181/t_http_api.go
type DeviceInfos (line 9) | type DeviceInfos struct
type DeviceItem (line 12) | type DeviceItem struct
type ChannelItem (line 16) | type ChannelItem struct
type PlayInfo (line 28) | type PlayInfo struct
type ReqPlay (line 36) | type ReqPlay struct
type RespPlay (line 39) | type RespPlay struct
type ReqStop (line 42) | type ReqStop struct
type PtzDirection (line 46) | type PtzDirection struct
type PtzZoom (line 55) | type PtzZoom struct
type PtzFi (line 62) | type PtzFi struct
type PresetCmd (line 71) | type PresetCmd
constant PresetEditPoint (line 74) | PresetEditPoint PresetCmd = iota
constant PresetDelPoint (line 75) | PresetDelPoint
constant PresetCallPoint (line 76) | PresetCallPoint
type PtzPreset (line 79) | type PtzPreset struct
type PtzStop (line 85) | type PtzStop struct
type ReqUpdateNotify (line 89) | type ReqUpdateNotify struct
function ResponseErrorWithMsg (line 93) | func ResponseErrorWithMsg(c *gin.Context, code ResCode, msg interface{}) {
function ResponseSuccess (line 101) | func ResponseSuccess(c *gin.Context, data interface{}) {
type ResCode (line 109) | type ResCode
method Msg (line 132) | func (c ResCode) Msg() string {
constant CodeSuccess (line 112) | CodeSuccess ResCode = 1000 + iota
constant CodeInvalidParam (line 113) | CodeInvalidParam
constant CodeServerBusy (line 114) | CodeServerBusy
constant CodeDeviceNotRegister (line 115) | CodeDeviceNotRegister
constant CodeDeviceStopError (line 116) | CodeDeviceStopError
constant SpeedParamError (line 128) | SpeedParamError = "speed 范围(0,8]"
constant PointParamError (line 129) | PointParamError = "point 范围(0,50]"
type ResponseData (line 140) | type ResponseData struct
FILE: gb28181/util.go
function RandNumString (line 14) | func RandNumString(n int) string {
function RandString (line 19) | func RandString(n int) string {
function randStringBySoure (line 25) | func randStringBySoure(src string, n int) string {
function DecodeGbk (line 47) | func DecodeGbk(v interface{}, body []byte) error {
function GbkToUtf8 (line 58) | func GbkToUtf8(s []byte) ([]byte, error) {
FILE: gb28181/xml.go
function BuildCatalogXML (line 46) | func BuildCatalogXML(sn int, id string) string {
function BuildAlarmResponseXML (line 62) | func BuildAlarmResponseXML(id string) string {
function BuildDeviceInfoXML (line 66) | func BuildDeviceInfoXML(sn int, id string) string {
function XmlEncode (line 70) | func XmlEncode(v interface{}) (string, error) {
FILE: logic/gop_cache.go
type GopCache (line 9) | type GopCache struct
method Feed (line 37) | func (c *GopCache) Feed(msg base.RtmpMsg) {
method feedNewGop (line 76) | func (c *GopCache) feedNewGop(msg base.RtmpMsg) {
method feedLastGop (line 85) | func (c *GopCache) feedLastGop(msg base.RtmpMsg) {
method isGopRingFull (line 96) | func (c *GopCache) isGopRingFull() bool {
method isGopRingEmpty (line 100) | func (c *GopCache) isGopRingEmpty() bool {
method Clear (line 104) | func (c *GopCache) Clear() {
method GetGopCount (line 112) | func (c *GopCache) GetGopCount() int {
method GetGopDataAt (line 116) | func (c *GopCache) GetGopDataAt(pos int) []base.RtmpMsg {
function NewGopCache (line 22) | func NewGopCache(gopSize, singleGopMaxFrameNum int) *GopCache {
type Gop (line 124) | type Gop struct
method feed (line 128) | func (g *Gop) feed(msg base.RtmpMsg) {
method clear (line 132) | func (g *Gop) clear() {
method release (line 142) | func (g *Gop) release() {
method size (line 146) | func (g *Gop) size() int {
FILE: logic/group.go
constant SubscriberProtocolLalmax (line 17) | SubscriberProtocolLalmax = "LALMAX"
constant SubscriberProtocolWHEP (line 18) | SubscriberProtocolWHEP = "WHEP"
constant SubscriberProtocolJessibuca (line 19) | SubscriberProtocolJessibuca = "JESSIBUCA"
constant SubscriberProtocolHTTPFMP4 (line 20) | SubscriberProtocolHTTPFMP4 = "HTTP-FMP4"
constant SubscriberProtocolSRT (line 21) | SubscriberProtocolSRT = "SRT"
type Subscriber (line 24) | type Subscriber interface
type ReplaySubscriber (line 30) | type ReplaySubscriber interface
type SubscriberInfo (line 35) | type SubscriberInfo struct
type Group (line 42) | type Group struct
method initHlsSession (line 132) | func (group *Group) initHlsSession() {
method waitLifecycleIdle (line 138) | func (group *Group) waitLifecycleIdle() {
method Key (line 147) | func (group *Group) Key() StreamKey {
method UniqueKey (line 151) | func (group *Group) UniqueKey() string {
method BindStopHook (line 155) | func (group *Group) BindStopHook(key StreamKey, onStop func(StreamKey)) {
method BindActiveHook (line 166) | func (group *Group) BindActiveHook(key StreamKey, onActive func(Stream...
method OnMsg (line 177) | func (group *Group) OnMsg(msg base.RtmpMsg) {
method OnStop (line 239) | func (group *Group) OnStop() {
method AddSubscriber (line 278) | func (group *Group) AddSubscriber(info SubscriberInfo, subscriber Subs...
method AddSubscriberWithReplay (line 282) | func (group *Group) AddSubscriberWithReplay(info SubscriberInfo, subsc...
method AddConsumer (line 345) | func (group *Group) AddConsumer(consumerID string, subscriber Subscrib...
method AddConsumerWithReplay (line 349) | func (group *Group) AddConsumerWithReplay(consumerID string, subscribe...
method StatSubscribers (line 353) | func (group *Group) StatSubscribers() []base.StatSub {
method GetAllConsumer (line 365) | func (group *Group) GetAllConsumer() []base.StatSub {
method RemoveSubscriber (line 369) | func (group *Group) RemoveSubscriber(subscriberID string) {
method RemoveConsumer (line 379) | func (group *Group) RemoveConsumer(consumerID string) {
method GetVideoSeqHeaderMsg (line 383) | func (group *Group) GetVideoSeqHeaderMsg() *base.RtmpMsg {
method GetAudioSeqHeaderMsg (line 393) | func (group *Group) GetAudioSeqHeaderMsg() *base.RtmpMsg {
method handleSubscriberMsg (line 403) | func (group *Group) handleSubscriberMsg(c *subscriberState, msg base.R...
method replayGopMessagesLocked (line 441) | func (group *Group) replayGopMessagesLocked(c *subscriberState, msgs [...
method getGopReplayMessages (line 561) | func (group *Group) getGopReplayMessages() []base.RtmpMsg {
type subscriberState (line 63) | type subscriberState struct
method AppName (line 80) | func (s *subscriberState) AppName() string {
method GetStat (line 84) | func (s *subscriberState) GetStat() base.StatSession {
method IsAlive (line 91) | func (s *subscriberState) IsAlive() (readAlive bool, writeAlive bool) {
method RawQuery (line 95) | func (s *subscriberState) RawQuery() string {
method StreamName (line 99) | func (s *subscriberState) StreamName() string {
method UniqueKey (line 103) | func (s *subscriberState) UniqueKey() string {
method UpdateStat (line 107) | func (s *subscriberState) UpdateStat(intervalSec uint32) {
method Url (line 114) | func (s *subscriberState) Url() string {
method deliverMsg (line 463) | func (s *subscriberState) deliverMsg(msg base.RtmpMsg) bool {
method refreshStat (line 472) | func (s *subscriberState) refreshStat(intervalSec float64) base.StatSe...
method refreshStatSnapshotLocked (line 494) | func (s *subscriberState) refreshStatSnapshotLocked() {
method updateBitrateLocked (line 507) | func (s *subscriberState) updateBitrateLocked(intervalSec float64) {
method stopWithNotify (line 534) | func (s *subscriberState) stopWithNotify() {
method stopWithoutNotify (line 551) | func (s *subscriberState) stopWithoutNotify() {
function newGroup (line 118) | func newGroup(manager *ComplexGroupManager, uniqueKey string, key Stream...
function isActiveMediaMsg (line 228) | func isActiveMediaMsg(msg base.RtmpMsg) bool {
function bitrateFromBytes (line 523) | func bitrateFromBytes(bytes uint64, intervalSec float64) int {
function diffUint64 (line 527) | func diffUint64(curr, prev uint64) uint64 {
FILE: logic/group_manager.go
type IGroupManager (line 11) | type IGroupManager interface
type ComplexGroupManager (line 20) | type ComplexGroupManager struct
method GetOrCreateGroup (line 47) | func (m *ComplexGroupManager) GetOrCreateGroup(key StreamKey, uniqueKe...
method GetOrCreateGroupByStreamName (line 90) | func (m *ComplexGroupManager) GetOrCreateGroupByStreamName(uniqueKey, ...
method setGroup (line 94) | func (m *ComplexGroupManager) setGroup(key StreamKey, group *Group) {
method setGroupLocked (line 105) | func (m *ComplexGroupManager) setGroupLocked(key StreamKey, group *Gro...
method setGroupByStreamName (line 122) | func (m *ComplexGroupManager) setGroupByStreamName(streamName string, ...
method RemoveGroup (line 126) | func (m *ComplexGroupManager) RemoveGroup(key StreamKey) {
method RemoveGroupIfMatch (line 131) | func (m *ComplexGroupManager) RemoveGroupIfMatch(key StreamKey, group ...
method removeGroup (line 135) | func (m *ComplexGroupManager) removeGroup(key StreamKey, group *Group,...
method RemoveGroupByStreamName (line 175) | func (m *ComplexGroupManager) RemoveGroupByStreamName(streamName strin...
method GetGroup (line 179) | func (m *ComplexGroupManager) GetGroup(key StreamKey) (bool, *Group) {
method getGroupLocked (line 190) | func (m *ComplexGroupManager) getGroupLocked(key StreamKey) (bool, *Gr...
method GetGroupByStreamName (line 211) | func (m *ComplexGroupManager) GetGroupByStreamName(streamName string) ...
method WaitGroup (line 217) | func (m *ComplexGroupManager) WaitGroup(key StreamKey, interval, timeo...
method getGroupByOnlyStreamNameLocked (line 231) | func (m *ComplexGroupManager) getGroupByOnlyStreamNameLocked(streamNam...
method Iterate (line 247) | func (m *ComplexGroupManager) Iterate(onIterateGroup func(key StreamKe...
method Len (line 276) | func (m *ComplexGroupManager) Len() int {
function NewComplexGroupManager (line 28) | func NewComplexGroupManager() *ComplexGroupManager {
function GetGroupManagerInstance (line 40) | func GetGroupManagerInstance() *ComplexGroupManager {
FILE: logic/group_test.go
type recordSubscriber (line 11) | type recordSubscriber struct
method OnMsg (line 17) | func (s *recordSubscriber) OnMsg(msg base.RtmpMsg) {
method OnStop (line 23) | func (s *recordSubscriber) OnStop() {
method len (line 29) | func (s *recordSubscriber) len() int {
method markerAt (line 35) | func (s *recordSubscriber) markerAt(idx int) byte {
method stopCountValue (line 41) | func (s *recordSubscriber) stopCountValue() int {
type blockingSubscriber (line 47) | type blockingSubscriber struct
method OnMsg (line 63) | func (s *blockingSubscriber) OnMsg(msg base.RtmpMsg) {
method OnStop (line 77) | func (s *blockingSubscriber) OnStop() {}
method OnReplayStart (line 79) | func (s *blockingSubscriber) OnReplayStart() {
method OnReplayStop (line 85) | func (s *blockingSubscriber) OnReplayStop() {
method markers (line 91) | func (s *blockingSubscriber) markers() []byte {
function newBlockingSubscriber (line 56) | func newBlockingSubscriber() *blockingSubscriber {
type selfRemovingSubscriber (line 102) | type selfRemovingSubscriber struct
method OnMsg (line 110) | func (s *selfRemovingSubscriber) OnMsg(msg base.RtmpMsg) {
method OnStop (line 121) | func (s *selfRemovingSubscriber) OnStop() {}
method len (line 123) | func (s *selfRemovingSubscriber) len() int {
method markerAt (line 129) | func (s *selfRemovingSubscriber) markerAt(idx int) byte {
type statSubscriber (line 135) | type statSubscriber struct
method OnMsg (line 140) | func (s *statSubscriber) OnMsg(msg base.RtmpMsg) {}
method OnStop (line 142) | func (s *statSubscriber) OnStop() {}
method GetSubscriberStat (line 144) | func (s *statSubscriber) GetSubscriberStat() SubscriberStat {
method setStat (line 150) | func (s *statSubscriber) setStat(stat SubscriberStat) {
function videoSeqHeader (line 156) | func videoSeqHeader(marker byte) base.RtmpMsg {
function videoKeyNalu (line 168) | func videoKeyNalu(marker byte) base.RtmpMsg {
function videoInterNalu (line 180) | func videoInterNalu(marker byte) base.RtmpMsg {
function aacSeqHeader (line 192) | func aacSeqHeader(marker byte) base.RtmpMsg {
function aacRaw (line 203) | func aacRaw(marker byte) base.RtmpMsg {
function g711aAudio (line 214) | func g711aAudio(marker byte) base.RtmpMsg {
function payloadMarker (line 221) | func payloadMarker(msg base.RtmpMsg) byte {
function newTestGroup (line 225) | func newTestGroup(streamName string) *Group {
function testSubscriberState (line 230) | func testSubscriberState(t *testing.T, group *Group, subscriberID string...
function TestAddConsumerReplaysCachedGopImmediately (line 246) | func TestAddConsumerReplaysCachedGopImmediately(t *testing.T) {
function TestVideoSeqHeaderChangeClearsStaleGop (line 271) | func TestVideoSeqHeaderChangeClearsStaleGop(t *testing.T) {
function TestNonAacAudioIsNotReplayedAsHeader (line 298) | func TestNonAacAudioIsNotReplayedAsHeader(t *testing.T) {
function TestAddConsumerWithReplayDisabledDoesNotReplayCachedGop (line 321) | func TestAddConsumerWithReplayDisabledDoesNotReplayCachedGop(t *testing....
function TestAddConsumerReplayDoesNotInterleaveWithLiveKeyFrame (line 353) | func TestAddConsumerReplayDoesNotInterleaveWithLiveKeyFrame(t *testing.T) {
function TestSubscriberRemovingItselfStopsReplayDelivery (line 398) | func TestSubscriberRemovingItselfStopsReplayDelivery(t *testing.T) {
function TestSubscriberRemovingItselfStopsHeaderAndLiveDelivery (line 422) | func TestSubscriberRemovingItselfStopsHeaderAndLiveDelivery(t *testing.T) {
function TestGroupManagerSupportsAppNameAndStreamName (line 441) | func TestGroupManagerSupportsAppNameAndStreamName(t *testing.T) {
function TestGroupManagerStreamNameFallbackRejectsAmbiguousAppName (line 458) | func TestGroupManagerStreamNameFallbackRejectsAmbiguousAppName(t *testin...
function TestGroupManagerGetOrCreateGroupReturnsExisting (line 469) | func TestGroupManagerGetOrCreateGroupReturnsExisting(t *testing.T) {
function TestGroupManagerGetOrCreateWaitsForClosedGroupCleanup (line 487) | func TestGroupManagerGetOrCreateWaitsForClosedGroupCleanup(t *testing.T) {
function TestGroupManagerGetOrCreateReturnsReplacementAfterWaitingClosedGroup (line 525) | func TestGroupManagerGetOrCreateReturnsReplacementAfterWaitingClosedGrou...
function TestGroupManagerRemoveGroupIfMatchDoesNotRemoveNewGroup (line 560) | func TestGroupManagerRemoveGroupIfMatchDoesNotRemoveNewGroup(t *testing....
function TestGroupManagerIterateRemoveDoesNotRemoveReplacement (line 576) | func TestGroupManagerIterateRemoveDoesNotRemoveReplacement(t *testing.T) {
function TestGopCacheClearReleasesStaleGopPayloads (line 597) | func TestGopCacheClearReleasesStaleGopPayloads(t *testing.T) {
function TestGopCacheNegativeFrameLimitMeansUnlimited (line 614) | func TestGopCacheNegativeFrameLimitMeansUnlimited(t *testing.T) {
function TestOnStopIsIdempotentAndClosesSubscribers (line 626) | func TestOnStopIsIdempotentAndClosesSubscribers(t *testing.T) {
function TestOnMsgTriggersActiveHookOnceOnFirstMediaPacket (line 646) | func TestOnMsgTriggersActiveHookOnceOnFirstMediaPacket(t *testing.T) {
function TestAddSubscriberAfterStopIsIgnored (line 672) | func TestAddSubscriberAfterStopIsIgnored(t *testing.T) {
function TestDuplicateSubscriberIDIsIgnored (line 690) | func TestDuplicateSubscriberIDIsIgnored(t *testing.T) {
function TestStatSubscribersRefreshRuntimeStats (line 709) | func TestStatSubscribersRefreshRuntimeStats(t *testing.T) {
FILE: logic/stat_aggregator.go
type StatAggregator (line 6) | type StatAggregator struct
method ExtSubscribers (line 22) | func (a *StatAggregator) ExtSubscribers(key StreamKey) []base.StatSub {
method BuildGroupView (line 42) | func (a *StatAggregator) BuildGroupView(group base.StatGroup) StatGrou...
method BuildGroupsView (line 56) | func (a *StatAggregator) BuildGroupsView(groups []base.StatGroup) []St...
method MergeGroup (line 68) | func (a *StatAggregator) MergeGroup(group base.StatGroup) base.StatGro...
method MergeGroups (line 72) | func (a *StatAggregator) MergeGroups(groups []base.StatGroup) []base.S...
method FindGroupView (line 84) | func (a *StatAggregator) FindGroupView(groups []base.StatGroup, key St...
method FindGroup (line 114) | func (a *StatAggregator) FindGroup(groups []base.StatGroup, key Stream...
type StatGroupView (line 10) | type StatGroupView struct
function NewStatAggregator (line 15) | func NewStatAggregator(groupManager IGroupManager) *StatAggregator {
FILE: logic/stream_key.go
type StreamKey (line 3) | type StreamKey struct
method Valid (line 20) | func (key StreamKey) Valid() bool {
method String (line 24) | func (key StreamKey) String() string {
function NewStreamKey (line 9) | func NewStreamKey(appName, streamName string) StreamKey {
function StreamKeyFromStreamName (line 16) | func StreamKeyFromStreamName(streamName string) StreamKey {
FILE: logic/subscriber_stat.go
type SubscriberStat (line 4) | type SubscriberStat struct
type SubscriberStatProvider (line 11) | type SubscriberStatProvider interface
FILE: main.go
function main (line 20) | func main() {
function parseFlag (line 43) | func parseFlag() string {
FILE: rtc/jessibucasession.go
type jessibucaSession (line 19) | type jessibucaSession struct
method createDataChannel (line 59) | func (conn *jessibucaSession) createDataChannel() (err error) {
method GetAnswerSDP (line 66) | func (conn *jessibucaSession) GetAnswerSDP(offer string) (sdp string) {
method Run (line 99) | func (conn *jessibucaSession) Run() {
method OnMsg (line 179) | func (conn *jessibucaSession) OnMsg(msg base.RtmpMsg) {
method OnStop (line 194) | func (conn *jessibucaSession) OnStop() {
method Close (line 200) | func (conn *jessibucaSession) Close() {
method GetSubscriberStat (line 209) | func (conn *jessibucaSession) GetSubscriberStat() maxlogic.SubscriberS...
method refreshRemoteAddr (line 217) | func (conn *jessibucaSession) refreshRemoteAddr() {
method currentRemoteAddr (line 223) | func (conn *jessibucaSession) currentRemoteAddr() string {
method loadRemoteAddr (line 237) | func (conn *jessibucaSession) loadRemoteAddr() string {
function NewJessibucaSession (line 39) | func NewJessibucaSession(appName, streamid string, writeChanSize int, pc...
function chunkSlice (line 163) | func chunkSlice(slice []byte, size int) [][]byte {
FILE: rtc/packer.go
constant PacketH264 (line 15) | PacketH264 = "H264"
constant PacketHEVC (line 16) | PacketHEVC = "HEVC"
constant PacketPCMA (line 17) | PacketPCMA = "PCMA"
constant PacketPCMU (line 18) | PacketPCMU = "PCMU"
constant PacketOPUS (line 19) | PacketOPUS = "OPUS"
type Packer (line 22) | type Packer struct
method Encode (line 44) | func (p *Packer) Encode(msg base.RtmpMsg) ([]*rtp.Packet, error) {
method UpdateVideoCodec (line 51) | func (p *Packer) UpdateVideoCodec(vps, sps, pps []byte) {
function NewPacker (line 26) | func NewPacker(mimeType string, codec []byte) *Packer {
type IRtpEncoder (line 66) | type IRtpEncoder interface
type H264RtpEncoder (line 70) | type H264RtpEncoder struct
method Encode (line 95) | func (enc *H264RtpEncoder) Encode(msg base.RtmpMsg) ([]*rtp.Packet, er...
method UpdateVideoCodec (line 147) | func (enc *H264RtpEncoder) UpdateVideoCodec(_ []byte, sps, pps []byte) {
function NewH264RtpEncoder (line 77) | func NewH264RtpEncoder(codec []byte) *H264RtpEncoder {
type G711RtpEncoder (line 152) | type G711RtpEncoder struct
method Encode (line 166) | func (enc *G711RtpEncoder) Encode(msg base.RtmpMsg) ([]*rtp.Packet, er...
function NewG711RtpEncoder (line 157) | func NewG711RtpEncoder(pt uint8) *G711RtpEncoder {
type HevcRtpEncoder (line 192) | type HevcRtpEncoder struct
method Encode (line 219) | func (enc *HevcRtpEncoder) Encode(msg base.RtmpMsg) ([]*rtp.Packet, er...
method UpdateVideoCodec (line 273) | func (enc *HevcRtpEncoder) UpdateVideoCodec(vps, sps, pps []byte) {
function NewHevcRtpEncoder (line 200) | func NewHevcRtpEncoder(codec []byte) *HevcRtpEncoder {
type OpusRtpEncoder (line 279) | type OpusRtpEncoder struct
method Encode (line 292) | func (enc *OpusRtpEncoder) Encode(msg base.RtmpMsg) ([]*rtp.Packet, er...
function NewOpusRtpEncoder (line 284) | func NewOpusRtpEncoder(pt uint8) *OpusRtpEncoder {
FILE: rtc/peerConnection.go
type peerConnection (line 10) | type peerConnection struct
function newPeerConnection (line 14) | func newPeerConnection(ips []string, iceUDPMux ice.UDPMux, iceTCPMux ice...
FILE: rtc/server.go
type StreamNotFoundFn (line 20) | type StreamNotFoundFn
type RtcServer (line 22) | type RtcServer struct
method SetStreamNotFoundFn (line 31) | func (s *RtcServer) SetStreamNotFoundFn(fn StreamNotFoundFn) {
method waitStreamReady (line 37) | func (s *RtcServer) waitStreamReady(appName, streamid, schema string) ...
method HandleWHIP (line 100) | func (s *RtcServer) HandleWHIP(c *gin.Context) {
method ServeWHIPPublishPage (line 148) | func (s *RtcServer) ServeWHIPPublishPage(c *gin.Context) {
method HandleJessibuca (line 167) | func (s *RtcServer) HandleJessibuca(c *gin.Context) {
method ServeWHEPPlayPage (line 222) | func (s *RtcServer) ServeWHEPPlayPage(c *gin.Context) {
method HandleWHEP (line 243) | func (s *RtcServer) HandleWHEP(c *gin.Context) {
method HandleZlmWebrtcPlay (line 299) | func (s *RtcServer) HandleZlmWebrtcPlay(app, stream, offer string) (st...
function NewRtcServer (line 52) | func NewRtcServer(config config.RtcConfig, lal logic.ILalServer) (*RtcSe...
function buildWHIPPublishHTML (line 163) | func buildWHIPPublishHTML() string {
function buildWHEPPlayHTML (line 239) | func buildWHEPPlayHTML() string {
FILE: rtc/subscriber_stat.go
function remoteAddrFromDTLSTransport (line 9) | func remoteAddrFromDTLSTransport(dtls *webrtc.DTLSTransport) string {
function remoteAddrFromICETransport (line 16) | func remoteAddrFromICETransport(iceTransport *webrtc.ICETransport) string {
FILE: rtc/unpacker.go
type UnPacker (line 24) | type UnPacker struct
method UnPack (line 70) | func (un *UnPacker) UnPack(pkt *rtp.Packet) (err error) {
function NewUnPacker (line 34) | func NewUnPacker(mimeType string, clockRate uint32, pktChan chan<- base....
type IRtpDecoder (line 105) | type IRtpDecoder interface
type H264RtpDecoder (line 109) | type H264RtpDecoder struct
method Decode (line 121) | func (r *H264RtpDecoder) Decode(pkt *rtp.Packet) ([]byte, error) {
function NewH264RtpDecoder (line 114) | func NewH264RtpDecoder(f format.Format) *H264RtpDecoder {
type G711RtpDecoder (line 141) | type G711RtpDecoder struct
method Decode (line 153) | func (r *G711RtpDecoder) Decode(pkt *rtp.Packet) ([]byte, error) {
function NewG711RtpDecoder (line 146) | func NewG711RtpDecoder(f format.Format) *G711RtpDecoder {
type OpusRtpDecoder (line 163) | type OpusRtpDecoder struct
method Decode (line 175) | func (r *OpusRtpDecoder) Decode(pkt *rtp.Packet) ([]byte, error) {
function NewOpusRtpDecoder (line 168) | func NewOpusRtpDecoder(f format.Format) *OpusRtpDecoder {
type H265RtpDecoder (line 185) | type H265RtpDecoder struct
method Decode (line 197) | func (r *H265RtpDecoder) Decode(pkt *rtp.Packet) ([]byte, error) {
function NewH265RtpDecoder (line 190) | func NewH265RtpDecoder(f format.Format) *H265RtpDecoder {
FILE: rtc/whepsession.go
constant whepMaxReplayPaceDelay (line 23) | whepMaxReplayPaceDelay = 5 * time.Millisecond
type whepSession (line 25) | type whepSession struct
method GetAnswerSDP (line 67) | func (conn *whepSession) GetAnswerSDP(offer string) (sdp string) {
method Run (line 172) | func (conn *whepSession) Run() {
method signalConnected (line 233) | func (conn *whepSession) signalConnected() {
method OnReplayStart (line 240) | func (conn *whepSession) OnReplayStart() {
method OnReplayStop (line 247) | func (conn *whepSession) OnReplayStop() {
method paceReplayMsg (line 251) | func (conn *whepSession) paceReplayMsg(msg base.RtmpMsg) {
method OnMsg (line 283) | func (conn *whepSession) OnMsg(msg base.RtmpMsg) {
method OnStop (line 302) | func (conn *whepSession) OnStop() {
method sendAudio (line 306) | func (conn *whepSession) sendAudio(msg base.RtmpMsg) {
method sendVideo (line 323) | func (conn *whepSession) sendVideo(msg base.RtmpMsg) {
method updateVideoCodec (line 341) | func (conn *whepSession) updateVideoCodec(msg base.RtmpMsg) {
method Close (line 380) | func (conn *whepSession) Close() {
method GetSubscriberStat (line 386) | func (conn *whepSession) GetSubscriberStat() maxlogic.SubscriberStat {
method recordSentRTP (line 394) | func (conn *whepSession) recordSentRTP(pkt *rtp.Packet) {
method refreshRemoteAddr (line 401) | func (conn *whepSession) refreshRemoteAddr() {
method currentRemoteAddr (line 407) | func (conn *whepSession) currentRemoteAddr() string {
method loadRemoteAddr (line 424) | func (conn *whepSession) loadRemoteAddr() string {
function NewWhepSession (line 48) | func NewWhepSession(appName, streamid string, writeChanSize int, pc *pee...
FILE: rtc/whipsession.go
type whipSession (line 11) | type whipSession struct
method GetAnswerSDP (line 47) | func (conn *whipSession) GetAnswerSDP(offer string) (sdp string) {
method Run (line 73) | func (conn *whipSession) Run() {
method Close (line 135) | func (conn *whipSession) Close() {
function NewWhipSession (line 23) | func NewWhipSession(streamid string, pc *peerConnection, lalServer logic...
FILE: server/hook_builtin_http_plugin.go
type hookBuiltinHTTPPlugin (line 5) | type hookBuiltinHTTPPlugin struct
method Name (line 10) | func (p *hookBuiltinHTTPPlugin) Name() string {
method OnHookEvent (line 14) | func (p *hookBuiltinHTTPPlugin) OnHookEvent(event HookEvent) error {
method mustRegisterBuiltinHTTPPlugin (line 115) | func (h *HttpNotify) mustRegisterBuiltinHTTPPlugin() {
FILE: server/hook_filter.go
type HookEventFilter (line 9) | type HookEventFilter struct
method Match (line 53) | func (f HookEventFilter) Match(event HookEvent) bool {
function NewHookEventFilter (line 16) | func NewHookEventFilter(appName, streamName, sessionID string, eventName...
function ParseHookEventNames (line 36) | func ParseHookEventNames(raw string) []string {
function matchStreamKey (line 81) | func matchStreamKey(key maxlogic.StreamKey, appName, streamName string) ...
FILE: server/hook_plugin.go
constant defaultHookPluginBufferSize (line 8) | defaultHookPluginBufferSize = 64
type HookPlugin (line 10) | type HookPlugin interface
type HookPluginOptions (line 15) | type HookPluginOptions struct
type hookPluginEntry (line 20) | type hookPluginEntry struct
method RegisterPlugin (line 26) | func (h *HttpNotify) RegisterPlugin(plugin HookPlugin, options HookPlugi...
method runPlugin (line 68) | func (h *HttpNotify) runPlugin(entry *hookPluginEntry) {
method dispatchPlugins (line 76) | func (h *HttpNotify) dispatchPlugins(event HookEvent) {
method unregisterPlugin (line 93) | func (h *HttpNotify) unregisterPlugin(name string) {
FILE: server/http_notify.go
type hookHTTPPostTask (line 42) | type hookHTTPPostTask struct
type hookHTTPPostWorker (line 49) | type hookHTTPPostWorker struct
type HookGroupInfo (line 53) | type HookGroupInfo struct
type HookEvent (line 59) | type HookEvent struct
constant HookEventServerStart (line 72) | HookEventServerStart = "on_server_start"
constant HookEventUpdate (line 73) | HookEventUpdate = "on_update"
constant HookEventGroupStart (line 74) | HookEventGroupStart = "on_group_start"
constant HookEventGroupStop (line 75) | HookEventGroupStop = "on_group_stop"
constant HookEventStreamActive (line 76) | HookEventStreamActive = "on_stream_active"
constant HookEventPubStart (line 77) | HookEventPubStart = "on_pub_start"
constant HookEventPubStop (line 78) | HookEventPubStop = "on_pub_stop"
constant HookEventSubStart (line 79) | HookEventSubStart = "on_sub_start"
constant HookEventSubStop (line 80) | HookEventSubStop = "on_sub_stop"
constant HookEventRelayPullStart (line 81) | HookEventRelayPullStart = "on_relay_pull_start"
constant HookEventRelayPullStop (line 82) | HookEventRelayPullStop = "on_relay_pull_stop"
constant HookEventRtmpConnect (line 83) | HookEventRtmpConnect = "on_rtmp_connect"
constant HookEventHlsMakeTs (line 84) | HookEventHlsMakeTs = "on_hls_make_ts"
constant HookEventStreamChanged (line 85) | HookEventStreamChanged = "on_stream_changed"
constant HookEventServerKeepalive (line 86) | HookEventServerKeepalive = "on_server_keepalive"
constant HookEventStreamNoneReader (line 87) | HookEventStreamNoneReader = "on_stream_none_reader"
constant HookEventRtpServerTimeout (line 88) | HookEventRtpServerTimeout = "on_rtp_server_timeout"
constant HookEventRecordMp4 (line 89) | HookEventRecordMp4 = "on_record_mp4"
constant HookEventPublish (line 90) | HookEventPublish = "on_publish"
constant HookEventPlay (line 91) | HookEventPlay = "on_play"
constant HookEventStreamNotFound (line 92) | HookEventStreamNotFound = "on_stream_not_found"
type SubCountFn (line 97) | type SubCountFn
type HttpNotify (line 99) | type HttpNotify struct
method SetSubCountFn (line 122) | func (h *HttpNotify) SetSubCountFn(fn SubCountFn) {
method UpdateZlmHookConfig (line 129) | func (h *HttpNotify) UpdateZlmHookConfig(zlmCfg config.ZlmCompatHookCo...
method NotifyServerStart (line 181) | func (h *HttpNotify) NotifyServerStart(info base.LalInfo) {
method NotifyUpdate (line 186) | func (h *HttpNotify) NotifyUpdate(info base.UpdateInfo) {
method NotifyGroupStart (line 192) | func (h *HttpNotify) NotifyGroupStart(info HookGroupInfo) {
method NotifyGroupStop (line 197) | func (h *HttpNotify) NotifyGroupStop(info HookGroupInfo) {
method NotifyStreamActive (line 202) | func (h *HttpNotify) NotifyStreamActive(info HookGroupInfo) {
method NotifyPubStart (line 207) | func (h *HttpNotify) NotifyPubStart(info base.PubStartInfo) {
method NotifyPubStop (line 234) | func (h *HttpNotify) NotifyPubStop(info base.PubStopInfo) {
method NotifySubStart (line 254) | func (h *HttpNotify) NotifySubStart(info base.SubStartInfo) {
method NotifySubStop (line 271) | func (h *HttpNotify) NotifySubStop(info base.SubStopInfo) {
method NotifyPullStart (line 289) | func (h *HttpNotify) NotifyPullStart(info base.PullStartInfo) {
method NotifyPullStop (line 294) | func (h *HttpNotify) NotifyPullStop(info base.PullStopInfo) {
method NotifyRtmpConnect (line 299) | func (h *HttpNotify) NotifyRtmpConnect(info base.RtmpConnectInfo) {
method NotifyOnHlsMakeTs (line 304) | func (h *HttpNotify) NotifyOnHlsMakeTs(info base.HlsMakeTsInfo) {
method NotifyStreamChanged (line 309) | func (h *HttpNotify) NotifyStreamChanged(info ZlmOnStreamChangedPayloa...
method NotifyServerKeepalive (line 316) | func (h *HttpNotify) NotifyServerKeepalive() {
method NotifyStreamNoneReader (line 322) | func (h *HttpNotify) NotifyStreamNoneReader(info ZlmOnStreamNoneReader...
method NotifyRtpServerTimeout (line 329) | func (h *HttpNotify) NotifyRtpServerTimeout(info ZlmOnRtpServerTimeout...
method NotifyRecordMp4 (line 336) | func (h *HttpNotify) NotifyRecordMp4(info ZlmOnRecordMp4Payload) {
method NotifyPublish (line 343) | func (h *HttpNotify) NotifyPublish(info ZlmOnPublishPayload) {
method NotifyPlay (line 350) | func (h *HttpNotify) NotifyPlay(info ZlmOnPlayPayload) {
method NotifyStreamNotFound (line 357) | func (h *HttpNotify) NotifyStreamNotFound(info ZlmOnStreamNotFoundPayl...
method OnServerStart (line 366) | func (h *HttpNotify) OnServerStart(info base.LalInfo) {
method OnUpdate (line 370) | func (h *HttpNotify) OnUpdate(info base.UpdateInfo) {
method OnGroupStart (line 374) | func (h *HttpNotify) OnGroupStart(info HookGroupInfo) {
method OnGroupStop (line 378) | func (h *HttpNotify) OnGroupStop(info HookGroupInfo) {
method OnStreamActive (line 382) | func (h *HttpNotify) OnStreamActive(info HookGroupInfo) {
method OnPubStart (line 386) | func (h *HttpNotify) OnPubStart(info base.PubStartInfo) {
method OnPubStop (line 390) | func (h *HttpNotify) OnPubStop(info base.PubStopInfo) {
method OnSubStart (line 394) | func (h *HttpNotify) OnSubStart(info base.SubStartInfo) {
method OnSubStop (line 398) | func (h *HttpNotify) OnSubStop(info base.SubStopInfo) {
method OnRelayPullStart (line 402) | func (h *HttpNotify) OnRelayPullStart(info base.PullStartInfo) {
method OnRelayPullStop (line 406) | func (h *HttpNotify) OnRelayPullStop(info base.PullStopInfo) {
method OnRtmpConnect (line 410) | func (h *HttpNotify) OnRtmpConnect(info base.RtmpConnectInfo) {
method OnHlsMakeTs (line 414) | func (h *HttpNotify) OnHlsMakeTs(info base.HlsMakeTsInfo) {
method asyncPostEvent (line 418) | func (h *HttpNotify) asyncPostEvent(url string, event HookEvent) {
method newHookHTTPPostTask (line 426) | func (h *HttpNotify) newHookHTTPPostTask(url string, event HookEvent) ...
method dispatchHTTPPost (line 451) | func (h *HttpNotify) dispatchHTTPPost(task hookHTTPPostTask) {
method runHTTPPostWorker (line 470) | func (h *HttpNotify) runHTTPPostWorker(orderKey string, worker *hookHT...
method postRaw (line 503) | func (h *HttpNotify) postRaw(url string, payload []byte) {
method Recent (line 524) | func (h *HttpNotify) Recent(limit int) []HookEvent {
method RecentFiltered (line 538) | func (h *HttpNotify) RecentFiltered(limit int, filter HookEventFilter)...
method Subscribe (line 560) | func (h *HttpNotify) Subscribe(buffer int) (int64, <-chan HookEvent, f...
method publish (line 584) | func (h *HttpNotify) publish(event string, info interface{}) {
function NewHttpNotify (line 155) | func NewHttpNotify(cfg config.HttpNotifyConfig, serverId string) *HttpNo...
function buildHookHTTPOrderKey (line 435) | func buildHookHTTPOrderKey(url string, event HookEvent) string {
function populateHookEventMeta (line 637) | func populateHookEventMeta(event *HookEvent, info interface{}) {
function populateHookSessionMeta (line 703) | func populateHookSessionMeta(event *HookEvent, info base.SessionEventCom...
FILE: server/middle.go
method Cors (line 10) | func (s *LalMaxServer) Cors() gin.HandlerFunc {
function Authentication (line 36) | func Authentication(secrets, ips []string) gin.HandlerFunc {
function authentication (line 51) | func authentication(reqToken, clientIP string, secrets, ips []string) bo...
function containFn (line 63) | func containFn[T comparable](ts []T, t T) bool {
FILE: server/router.go
method InitRouter (line 5) | func (s *LalMaxServer) InitRouter(router *gin.Engine) {
FILE: server/router_ctrl.go
method initCtrlRouter (line 11) | func (s *LalMaxServer) initCtrlRouter(router *gin.Engine, handlers ...gi...
method ctrlStartRelayPullHandler (line 21) | func (s *LalMaxServer) ctrlStartRelayPullHandler(c *gin.Context) {
method ctrlStopRelayPullHandler (line 52) | func (s *LalMaxServer) ctrlStopRelayPullHandler(c *gin.Context) {
method ctrlKickSessionHandler (line 68) | func (s *LalMaxServer) ctrlKickSessionHandler(c *gin.Context) {
method ctrlStartRtpPubHandler (line 87) | func (s *LalMaxServer) ctrlStartRtpPubHandler(c *gin.Context) {
method ctrlStopRtpPubHandler (line 110) | func (s *LalMaxServer) ctrlStopRtpPubHandler(c *gin.Context) {
FILE: server/router_flv_proxy.go
method initFlvProxy (line 16) | func (s *LalMaxServer) initFlvProxy(router *gin.Engine) {
method getLalHttpflvAddr (line 75) | func (s *LalMaxServer) getLalHttpflvAddr() string {
FILE: server/router_fmp4.go
method initFmp4Router (line 10) | func (s *LalMaxServer) initFmp4Router(router *gin.Engine) {
method HandleHls (line 15) | func (s *LalMaxServer) HandleHls(c *gin.Context) {
method HandleHttpFmp4 (line 24) | func (s *LalMaxServer) HandleHttpFmp4(c *gin.Context) {
FILE: server/router_helper.go
function unmarshalRequestJSONBody (line 12) | func unmarshalRequestJSONBody(r *http.Request, info interface{}, keyFiel...
FILE: server/router_hook.go
method initHookRouter (line 12) | func (s *LalMaxServer) initHookRouter(router *gin.Engine, handlers ...gi...
method hookRecentHandler (line 18) | func (s *LalMaxServer) hookRecentHandler(c *gin.Context) {
method hookStreamHandler (line 44) | func (s *LalMaxServer) hookStreamHandler(c *gin.Context) {
function writeHookEventSSE (line 111) | func writeHookEventSSE(w http.ResponseWriter, event HookEvent) error {
FILE: server/router_rtc.go
method initRtcRouter (line 9) | func (s *LalMaxServer) initRtcRouter(router *gin.Engine) {
method HandleWHIP (line 25) | func (s *LalMaxServer) HandleWHIP(c *gin.Context) {
method HandleWHEP (line 54) | func (s *LalMaxServer) HandleWHEP(c *gin.Context) {
method HandleJessibuca (line 83) | func (s *LalMaxServer) HandleJessibuca(c *gin.Context) {
FILE: server/router_stat.go
method initStatRouter (line 11) | func (s *LalMaxServer) initStatRouter(router *gin.Engine, handlers ...gi...
method statGroupHandler (line 18) | func (s *LalMaxServer) statGroupHandler(c *gin.Context) {
method statAllGroupHandler (line 42) | func (s *LalMaxServer) statAllGroupHandler(c *gin.Context) {
method statLalInfoHandler (line 50) | func (s *LalMaxServer) statLalInfoHandler(c *gin.Context) {
FILE: server/router_test.go
type testHookPlugin (line 24) | type testHookPlugin struct
method Name (line 47) | func (p *testHookPlugin) Name() string {
method OnHookEvent (line 51) | func (p *testHookPlugin) OnHookEvent(event HookEvent) error {
type maxlogicTestSubscriber (line 29) | type maxlogicTestSubscriber struct
method OnMsg (line 39) | func (s *maxlogicTestSubscriber) OnMsg(msg base.RtmpMsg) {}
method OnStop (line 41) | func (s *maxlogicTestSubscriber) OnStop() {}
method GetSubscriberStat (line 43) | func (s *maxlogicTestSubscriber) GetSubscriberStat() maxlogic.Subscrib...
type hookHTTPPayload (line 33) | type hookHTTPPayload struct
constant httpNotifyAddr (line 61) | httpNotifyAddr = ":55559"
function uniqueTestName (line 63) | func uniqueTestName(prefix string) string {
function findTestGroup (line 67) | func findTestGroup(groups []LalmaxStatGroup, streamName string) *LalmaxS...
function TestMain (line 76) | func TestMain(m *testing.M) {
function TestAllGroup (line 127) | func TestAllGroup(t *testing.T) {
function TestNotifyUpdate (line 191) | func TestNotifyUpdate(t *testing.T) {
function TestRtpPubStartStop (line 231) | func TestRtpPubStartStop(t *testing.T) {
function TestStatGroupWithAppName (line 272) | func TestStatGroupWithAppName(t *testing.T) {
function TestStatGroupIncludesLalmaxExtSubs (line 290) | func TestStatGroupIncludesLalmaxExtSubs(t *testing.T) {
function TestStatGroupIncludesLalmaxExtSubsRuntimeFields (line 330) | func TestStatGroupIncludesLalmaxExtSubsRuntimeFields(t *testing.T) {
function TestStopRelayPullAllowsGet (line 382) | func TestStopRelayPullAllowsGet(t *testing.T) {
function TestHookHubRecentAndSubscribe (line 400) | func TestHookHubRecentAndSubscribe(t *testing.T) {
function TestHookGroupEventsFromDirectLifecycle (line 430) | func TestHookGroupEventsFromDirectLifecycle(t *testing.T) {
function TestHookHubStreamActiveEvent (line 495) | func TestHookHubStreamActiveEvent(t *testing.T) {
function TestBuiltinHTTPPluginRespectsEnableFlag (line 521) | func TestBuiltinHTTPPluginRespectsEnableFlag(t *testing.T) {
function TestBuiltinHTTPPluginPreservesOrderPerStream (line 542) | func TestBuiltinHTTPPluginPreservesOrderPerStream(t *testing.T) {
function TestBuiltinHTTPPluginAllowsParallelAcrossStreams (line 611) | func TestBuiltinHTTPPluginAllowsParallelAcrossStreams(t *testing.T) {
function TestBuiltinHTTPPluginPreservesOrderAcrossDifferentURLsForSameStream (line 673) | func TestBuiltinHTTPPluginPreservesOrderAcrossDifferentURLsForSameStream...
function TestHookRecentEndpoint (line 749) | func TestHookRecentEndpoint(t *testing.T) {
function TestHookRecentEndpointFilterByEventAndStream (line 791) | func TestHookRecentEndpointFilterByEventAndStream(t *testing.T) {
function TestHookEventFilterBySessionID (line 842) | func TestHookEventFilterBySessionID(t *testing.T) {
function TestHookEventFilterByUpdateGroup (line 856) | func TestHookEventFilterByUpdateGroup(t *testing.T) {
function TestHookEventFilterByGroupLifecycle (line 871) | func TestHookEventFilterByGroupLifecycle(t *testing.T) {
function TestHookPluginReceivesFilteredEvents (line 884) | func TestHookPluginReceivesFilteredEvents(t *testing.T) {
function TestRegisterHookPluginFromServer (line 930) | func TestRegisterHookPluginFromServer(t *testing.T) {
function TestAuthentication (line 965) | func TestAuthentication(t *testing.T) {
function TestWHIPGETNot404 (line 999) | func TestWHIPGETNot404(t *testing.T) {
function TestWHEPGETCanonicalPath (line 1019) | func TestWHEPGETCanonicalPath(t *testing.T) {
FILE: server/router_zlm_compat.go
method initZlmCompatRouter (line 16) | func (s *LalMaxServer) initZlmCompatRouter(router *gin.Engine, handlers ...
method zlmOpenRtpServerHandler (line 33) | func (s *LalMaxServer) zlmOpenRtpServerHandler(c *gin.Context) {
method zlmCloseRtpServerHandler (line 62) | func (s *LalMaxServer) zlmCloseRtpServerHandler(c *gin.Context) {
method zlmCloseStreamsHandler (line 82) | func (s *LalMaxServer) zlmCloseStreamsHandler(c *gin.Context) {
method zlmGetServerConfigHandler (line 130) | func (s *LalMaxServer) zlmGetServerConfigHandler(c *gin.Context) {
method zlmSetServerConfigHandler (line 137) | func (s *LalMaxServer) zlmSetServerConfigHandler(c *gin.Context) {
method zlmRestartServerHandler (line 226) | func (s *LalMaxServer) zlmRestartServerHandler(c *gin.Context) {
method zlmAddStreamProxyHandler (line 234) | func (s *LalMaxServer) zlmAddStreamProxyHandler(c *gin.Context) {
method zlmStartRecordHandler (line 277) | func (s *LalMaxServer) zlmStartRecordHandler(c *gin.Context) {
method zlmStopRecordHandler (line 303) | func (s *LalMaxServer) zlmStopRecordHandler(c *gin.Context) {
method zlmGetSnapHandler (line 323) | func (s *LalMaxServer) zlmGetSnapHandler(c *gin.Context) {
method zlmWebrtcHandler (line 350) | func (s *LalMaxServer) zlmWebrtcHandler(c *gin.Context) {
function extractHostPort (line 390) | func extractHostPort(conf *config.Config, protocol string) string {
FILE: server/server.go
type LalMaxServer (line 30) | type LalMaxServer struct
method Run (line 118) | func (s *LalMaxServer) Run() (err error) {
method runPeriodicUpdate (line 179) | func (s *LalMaxServer) runPeriodicUpdate(ctx context.Context) {
method runPeriodicKeepalive (line 205) | func (s *LalMaxServer) runPeriodicKeepalive(ctx context.Context) {
method HookHub (line 228) | func (s *LalMaxServer) HookHub() *HttpNotify {
method RegisterHookPlugin (line 232) | func (s *LalMaxServer) RegisterHookPlugin(plugin HookPlugin, options H...
function NewLalMaxServer (line 45) | func NewLalMaxServer(conf *config.Config) (*LalMaxServer, error) {
FILE: server/stat_view.go
type LalmaxGroupStat (line 8) | type LalmaxGroupStat struct
type LalmaxStatGroup (line 12) | type LalmaxStatGroup struct
type ApiStatGroupResp (line 26) | type ApiStatGroupResp struct
type ApiStatAllGroupResp (line 31) | type ApiStatAllGroupResp struct
function newLalmaxStatGroup (line 38) | func newLalmaxStatGroup(view maxlogic.StatGroupView) LalmaxStatGroup {
function newLalmaxStatGroups (line 57) | func newLalmaxStatGroups(views []maxlogic.StatGroupView) []LalmaxStatGro...
FILE: server/zlm_compat_config.go
type lalRawPorts (line 14) | type lalRawPorts struct
function buildZlmServerConfig (line 27) | func buildZlmServerConfig(conf *config.Config) map[string]any {
function extractPort (line 100) | func extractPort(addr string) string {
function parsePortRange (line 113) | func parsePortRange(s string) (int, int, bool) {
function boolStr (line 126) | func boolStr(v bool) string {
FILE: server/zlm_compat_ffmpeg.go
type ffmpegRecorder (line 15) | type ffmpegRecorder struct
method startRecord (line 46) | func (r *ffmpegRecorder) startRecord(rtmpAddr, app, stream string, typ...
method stopRecord (line 112) | func (r *ffmpegRecorder) stopRecord(app, stream string, typ int) (stri...
type recordSession (line 21) | type recordSession struct
function newFfmpegRecorder (line 30) | func newFfmpegRecorder(outputDir string) *ffmpegRecorder {
function recordKey (line 41) | func recordKey(app, stream string, typ int) string {
function getSnap (line 131) | func getSnap(srcURL string, timeoutSec int) ([]byte, error) {
FILE: server/zlm_compat_test.go
function TestZlmCompatOpenRtpServer (line 21) | func TestZlmCompatOpenRtpServer(t *testing.T) {
function TestZlmCompatCloseRtpServer (line 53) | func TestZlmCompatCloseRtpServer(t *testing.T) {
function TestZlmCompatCloseRtpServerNotFound (line 88) | func TestZlmCompatCloseRtpServerNotFound(t *testing.T) {
function TestZlmCompatCloseStreams (line 108) | func TestZlmCompatCloseStreams(t *testing.T) {
function TestZlmCompatGetServerConfig (line 137) | func TestZlmCompatGetServerConfig(t *testing.T) {
function TestZlmCompatSetServerConfig (line 176) | func TestZlmCompatSetServerConfig(t *testing.T) {
function TestZlmCompatAddStreamProxy (line 226) | func TestZlmCompatAddStreamProxy(t *testing.T) {
function TestZlmCompatStartStopRecord (line 256) | func TestZlmCompatStartStopRecord(t *testing.T) {
function TestZlmHookOnStreamChangedFormat (line 318) | func TestZlmHookOnStreamChangedFormat(t *testing.T) {
function TestZlmHookOnStreamChangedFieldCompleteness (line 385) | func TestZlmHookOnStreamChangedFieldCompleteness(t *testing.T) {
function TestZlmHookOnServerKeepalive (line 444) | func TestZlmHookOnServerKeepalive(t *testing.T) {
function TestZlmHookOnStreamNoneReader (line 479) | func TestZlmHookOnStreamNoneReader(t *testing.T) {
function TestZlmHookOnRtpServerTimeout (line 521) | func TestZlmHookOnRtpServerTimeout(t *testing.T) {
function TestZlmHookOnStreamChangedOrderPerStream (line 562) | func TestZlmHookOnStreamChangedOrderPerStream(t *testing.T) {
function TestZlmHookOnPublish (line 632) | func TestZlmHookOnPublish(t *testing.T) {
function TestZlmHookOnPlay (line 670) | func TestZlmHookOnPlay(t *testing.T) {
function TestZlmHookOnStreamNotFound (line 708) | func TestZlmHookOnStreamNotFound(t *testing.T) {
function TestZlmHookDispatchByConfig (line 745) | func TestZlmHookDispatchByConfig(t *testing.T) {
FILE: server/zlm_compat_types.go
type ZlmFixedHeader (line 7) | type ZlmFixedHeader struct
type ZlmOpenRtpServerReq (line 14) | type ZlmOpenRtpServerReq struct
type ZlmOpenRtpServerResp (line 20) | type ZlmOpenRtpServerResp struct
type ZlmCloseRtpServerReq (line 28) | type ZlmCloseRtpServerReq struct
type ZlmCloseRtpServerResp (line 32) | type ZlmCloseRtpServerResp struct
type ZlmCloseStreamsReq (line 39) | type ZlmCloseStreamsReq struct
type ZlmCloseStreamsResp (line 47) | type ZlmCloseStreamsResp struct
type ZlmGetServerConfigResp (line 55) | type ZlmGetServerConfigResp struct
type ZlmSetServerConfigResp (line 62) | type ZlmSetServerConfigResp struct
type ZlmStartRecordReq (line 69) | type ZlmStartRecordReq struct
type ZlmStartRecordResp (line 78) | type ZlmStartRecordResp struct
type ZlmStopRecordReq (line 85) | type ZlmStopRecordReq struct
type ZlmStopRecordResp (line 92) | type ZlmStopRecordResp struct
type ZlmAddStreamProxyReq (line 99) | type ZlmAddStreamProxyReq struct
type ZlmAddStreamProxyResp (line 109) | type ZlmAddStreamProxyResp struct
type ZlmGetSnapReq (line 118) | type ZlmGetSnapReq struct
type ZlmOnStreamChangedPayload (line 126) | type ZlmOnStreamChangedPayload struct
type ZlmOriginSock (line 147) | type ZlmOriginSock struct
type ZlmTrack (line 155) | type ZlmTrack struct
type ZlmOnServerKeepalivePayload (line 170) | type ZlmOnServerKeepalivePayload struct
type ZlmOnStreamNoneReaderPayload (line 176) | type ZlmOnStreamNoneReaderPayload struct
type ZlmOnRecordMp4Payload (line 186) | type ZlmOnRecordMp4Payload struct
type ZlmOnPublishPayload (line 202) | type ZlmOnPublishPayload struct
type ZlmOnPlayPayload (line 216) | type ZlmOnPlayPayload struct
type ZlmOnStreamNotFoundPayload (line 230) | type ZlmOnStreamNotFoundPayload struct
type ZlmOnRtpServerTimeoutPayload (line 246) | type ZlmOnRtpServerTimeoutPayload struct
FILE: srt/pub.go
type Publisher (line 15) | type Publisher struct
method SetSession (line 38) | func (p *Publisher) SetSession(session logic.ICustomizePubSessionConte...
method Run (line 42) | func (p *Publisher) Run() {
function NewPublisher (line 25) | func NewPublisher(ctx context.Context, conn srt.Conn, streamName string,...
FILE: srt/server.go
type SrtServer (line 14) | type SrtServer struct
method Run (line 58) | func (s *SrtServer) Run(ctx context.Context) {
method handlePublish (line 109) | func (s *SrtServer) handlePublish(ctx context.Context, conn srt.Conn, ...
method handleSubcribe (line 126) | func (s *SrtServer) handleSubcribe(ctx context.Context, conn srt.Conn,...
method Remove (line 131) | func (s *SrtServer) Remove(host string, ss logic.ICustomizePubSessionC...
type SrtOption (line 19) | type SrtOption struct
type ModSrtOption (line 41) | type ModSrtOption
function NewSrtServer (line 43) | func NewSrtServer(addr string, lal logic.ILalServer, modOptions ...ModSr...
type StreamInfo (line 135) | type StreamInfo struct
function getStreamInfo (line 140) | func getStreamInfo(streamid string) StreamInfo {
FILE: srt/stream_id.go
type StreamID (line 8) | type StreamID struct
function parseStreamID (line 17) | func parseStreamID(streamID string) (*StreamID, error) {
FILE: srt/sub.go
type Subscriber (line 17) | type Subscriber struct
method Run (line 49) | func (s *Subscriber) Run() {
method OnMsg (line 88) | func (s *Subscriber) OnMsg(msg base.RtmpMsg) {
method OnStop (line 155) | func (s *Subscriber) OnStop() {
method GetSubscriberStat (line 160) | func (s *Subscriber) GetSubscriberStat() maxlogic.SubscriberStat {
function NewSubscriber (line 33) | func NewSubscriber(ctx context.Context, conn srt.Conn, streamName string...
FILE: utils/adjustdts.go
type DtsDecoder (line 5) | type DtsDecoder struct
method Decode (line 26) | func (d *DtsDecoder) Decode(ts uint32) time.Duration {
function NewDtsDecoder (line 12) | func NewDtsDecoder(startDts, clockRate time.Duration, prevDts uint32) *D...
function multiplyAndDivide (line 20) | func multiplyAndDivide(v, m, d time.Duration) time.Duration {
Condensed preview — 115 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (682K chars).
[
{
"path": ".github/workflows/go.yml",
"chars": 663,
"preview": "# This workflow will build a golang project\n# For more information see: https://docs.github.com/en/actions/automating-bu"
},
{
"path": ".github/workflows/release.yml",
"chars": 685,
"preview": "# https://github.com/wangyoucao577/go-release-action\n\nname: build-go-binary\n\non:\n release:\n types: [created] # 表示在创建"
},
{
"path": ".gitignore",
"chars": 27,
"preview": ".codex-cache/\nserver/logs/\n"
},
{
"path": "Dockerfile",
"chars": 287,
"preview": "FROM golang:1.23.0\nENV GOPROXY=https://goproxy.cn,https://goproxy.io,direct\nLABEL maintainer=\"Kevin Zang\"\n\nWORKDIR /code"
},
{
"path": "LICENSE",
"chars": 1071,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2023 Chef\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "README.md",
"chars": 2297,
"preview": "# lalmax\nlalmax是在lal的基础上集成第三方库,可以提供SRT、RTC、mp4、gb28181、onvif等解决方案\n\n# 编译\n./build.sh\n\n# 运行\n./run.sh或者./lalmax -c conf/lalm"
},
{
"path": "build.sh",
"chars": 67,
"preview": "#!/usr/bin/env bash\n\necho \"build lalmax\"\ngo build -o lalmax main.go"
},
{
"path": "conf/cert.pem",
"chars": 977,
"preview": "-----BEGIN CERTIFICATE-----\nMIICpDCCAYwCCQDWutSYrD7joDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls\nb2NhbGhvc3QwHhcNMjAwOTA3MTA"
},
{
"path": "conf/key.pem",
"chars": 1675,
"preview": "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEA4XcsIwJ1im2kAgbTQPZm76lbDNkuzErWsPSZHGeOS/zLSsVK\nD/9XoKnQA7QT3mcWZVz8cfr"
},
{
"path": "conf/lalmax.conf.json",
"chars": 4753,
"preview": "{\n \"lalmax\": {\n \"srt_config\": {\n \"enable\": true,\n \"addr\": \":6001\"\n },\n \"rtc_config\": {\n \"enable"
},
{
"path": "config/config.go",
"chars": 8508,
"preview": "package config\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n)\n\nvar defaultConfig Config\n\ntype Config struct {\n\tS"
},
{
"path": "config/config_test.go",
"chars": 3872,
"preview": "package config\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestUnmarshalStructuredConfig(t *testing.T) {\n\traw := []byte(`{\n\t"
},
{
"path": "document/api.md",
"chars": 6609,
"preview": "# HTTP API 总览\n\n`lalmax` 对外建议统一只暴露一个 HTTP 管理入口,也就是 `lalmax.http_config.http_listen_addr`,示例配置通常是 `:1290`。 \n`lal.http_api"
},
{
"path": "document/api_gateway.md",
"chars": 2081,
"preview": "# API Gateway\n\n`lalmax` 作为 `lal` 的统一 API 网关,对外建议只暴露一个 HTTP 入口。\n\nHook 体系的详细设计见 [hook_plugin_architecture.md](./hook_plugi"
},
{
"path": "document/config.md",
"chars": 5844,
"preview": "# lalmax 配置说明\n\n本文档说明 `conf/lalmax.conf.json` 里 `lalmax` 这一段的配置。 \n`lal` 原生配置请看 [lal_config.md](./lal_config.md)。\n\n## 推荐配"
},
{
"path": "document/gb28181.md",
"chars": 5230,
"preview": "# GB28181\n\nlalmax的gb28181功能为单端口监听(TCP/UDP监听端口可以使用tcp_listen_port和udp_listen_port进行配置),根据INVITE消息中的ssrc来区分具体流名,详细的配置见gb28"
},
{
"path": "document/hook_api.md",
"chars": 3867,
"preview": "# Hook API\n\n`lalmax` 统一托管 `lal` 的 notify 事件,并补充 `lalmax` 自身扩展订阅状态。\n\n如果需要理解完整分层、调用链、插件职责和设计边界,见 [hook_plugin_architecture"
},
{
"path": "document/hook_plugin_architecture.md",
"chars": 7672,
"preview": "# Hook Plugin Architecture\n\n本文档详细说明 `lalmax` 当前的 Hook 体系设计,包括:\n\n- 为什么需要由 `lalmax` 统一托管 hook\n- 事件从 `lal` 到业务插件的完整调用链\n- `H"
},
{
"path": "document/lal_api.md",
"chars": 2158,
"preview": "# lal Native HTTP API\n\n`lalmax` 内嵌运行 `lal`。默认情况下,对外建议统一使用 `lalmax` 自己的 API 门面:\n\n- `lalmax.http_config.http_listen_addr` "
},
{
"path": "document/lal_config.md",
"chars": 3349,
"preview": "# lal 原生配置说明\n\n本文档说明 `conf/lalmax.conf.json` 中 `lal` 配置段的常用字段。`lal` 配置段会直接传给 lal 原生服务,用于 RTMP、RTSP、HTTP-FLV、HLS-TS、HTTP-T"
},
{
"path": "document/rtc.md",
"chars": 4563,
"preview": "# WebRTC(WHIP/WHEP)\n\nWebRTC在刚发布的时候仅仅专注于VoIP和点对点用例,它仅限于几个并发的浏览器,并且不能扩展,缺少标准信令交互,故很难用于直播场景。\n\n在此背景下,WHIP和WHEP这2个标准的提出,补齐了信令"
},
{
"path": "document/srt.md",
"chars": 512,
"preview": "# SRT\n\nSRT(Secure Reliable Transport)的简称,主要优化在不可靠网络(非阻塞导致的丢包)环境下实时音视频的传输性能\n\n## 特点\n(1) 基于UDP的用户态协议栈\n\n(2) 抗丢包能力强&低延时\n\n(3) "
},
{
"path": "document/stream_url.md",
"chars": 3412,
"preview": "# 流地址说明\n\n本文档使用 `conf/lalmax.conf.json` 的默认配置举例,默认流名为 `test110`。\n\n## 基本规则\n\n- `lal` 原生能力使用 `lal` 配置段中的端口,例如 RTMP、RTSP、HTTP"
},
{
"path": "fmp4/hls/server.go",
"chars": 2895,
"preview": "package hls\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\tconfig \"github.com/q191201771/lalmax/config\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"gith"
},
{
"path": "fmp4/hls/session.go",
"chars": 9095,
"preview": "package hls\n\nimport (\n\t\"time\"\n\n\tconfig \"github.com/q191201771/lalmax/config\"\n\n\t\"github.com/bluenviron/gohlslib\"\n\t\"github"
},
{
"path": "fmp4/http-fmp4/server.go",
"chars": 376,
"preview": "package httpfmp4\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype HttpFmp4Server struct {\n}\n\nfunc NewHttpFmp4Server() *Http"
},
{
"path": "fmp4/http-fmp4/session.go",
"chars": 4825,
"preview": "package httpfmp4\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/q191201771/lalmax/fmp4/muxer\"\n"
},
{
"path": "fmp4/muxer/codec.go",
"chars": 1439,
"preview": "package muxer\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/q191201771/lal/pkg/aac\"\n)\n\ntype Codec interface {\n\tIsVideo() bool\n\tEqual("
},
{
"path": "fmp4/muxer/file_writer.go",
"chars": 4429,
"preview": "package muxer\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/q191201771/lal/pkg/base\"\n)\n\ntype Fmp4Record "
},
{
"path": "fmp4/muxer/flac_box.go",
"chars": 1208,
"preview": "package muxer\n\nimport (\n\t\"github.com/abema/go-mp4\"\n)\n\nfunc BoxTypeFlac() mp4.BoxType { return mp4.StrToBoxType(\"fLaC\") }"
},
{
"path": "fmp4/muxer/init.go",
"chars": 13906,
"preview": "package muxer\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/abema/go-mp4\"\n\t\"github.com/q191201771/lal/pkg/aac\"\n\t\"github.com/q1912"
},
{
"path": "fmp4/muxer/init_track.go",
"chars": 11867,
"preview": "package muxer\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/abema/go-mp4\"\n\t\"github.com/bluenviron/mediacommon/pkg/codecs/h265\"\n\t\"github"
},
{
"path": "fmp4/muxer/mp4_writer.go",
"chars": 1255,
"preview": "package muxer\n\nimport (\n\t\"io\"\n\n\t\"github.com/abema/go-mp4\"\n)\n\ntype mp4Writer struct {\n\tw *mp4.Writer\n}\n\nfunc newMP4Writer"
},
{
"path": "fmp4/muxer/muxer.go",
"chars": 5350,
"preview": "package muxer\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/q191201771/lal/pkg/avc\"\n\t\"github.com/q191201771/lal/pkg/base\"\n\t\"git"
},
{
"path": "fmp4/muxer/muxer_part.go",
"chars": 3849,
"preview": "package muxer\n\nimport \"time\"\n\nfunc durationGoToMp4(v time.Duration, timeScale uint32) uint64 {\n\ttimeScale64 := uint64(ti"
},
{
"path": "fmp4/muxer/part.go",
"chars": 1919,
"preview": "package muxer\n\nimport (\n\t\"io\"\n\n\t\"github.com/abema/go-mp4\"\n)\n\nconst (\n\ttrunFlagDataOffsetPreset = 0"
},
{
"path": "fmp4/muxer/part_sample.go",
"chars": 1229,
"preview": "package muxer\n\nimport (\n\t\"time\"\n)\n\n// PartSample is a sample of a PartTrack.\ntype PartSample struct {\n\tDts t"
},
{
"path": "fmp4/muxer/part_track.go",
"chars": 1882,
"preview": "package muxer\n\nimport \"github.com/abema/go-mp4\"\n\n// PartTrack is a track of Part.\ntype PartTrack struct {\n\tID int\n"
},
{
"path": "fmp4/muxer/rtmp2fmp4.go",
"chars": 7694,
"preview": "package muxer\n\nimport (\n\t\"bytes\"\n\t\"time\"\n\n\t\"github.com/q191201771/lal/pkg/aac\"\n\t\"github.com/q191201771/lal/pkg/avc\"\n\t\"gi"
},
{
"path": "fmp4/muxer/seekablebuffer.go",
"chars": 1209,
"preview": "package muxer\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n)\n\n// Buffer is a bytes.Buffer with an additional Seek() method.\ntype Buff"
},
{
"path": "fmp4/muxer/track.go",
"chars": 306,
"preview": "package muxer\n\ntype Track struct {\n\tCodec\n\tTrackId uint32\n\ttimeScale uint32\n\tfirstDTS int64\n\tlastDTS int64\n\tsamples"
},
{
"path": "fmp4/muxer/var.go",
"chars": 100,
"preview": "package muxer\n\nimport \"github.com/q191201771/naza/pkg/nazalog\"\n\nvar Log = nazalog.GetGlobalLogger()\n"
},
{
"path": "gb28181/auth.go",
"chars": 1035,
"preview": "package gb28181\n\nimport (\n\t\"crypto/md5\"\n\t\"fmt\"\n\n\t\"github.com/ghettovoice/gosip/sip\"\n\t\"github.com/q191201771/naza/pkg/naz"
},
{
"path": "gb28181/avail_conn_pool.go",
"chars": 1878,
"preview": "// Copyright 2020, Chef. All rights reserved.\n// https://github.com/q191201771/naza\n//\n// Use of this source code is go"
},
{
"path": "gb28181/channel.go",
"chars": 12560,
"preview": "package gb28181\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/q191201771/naza/pkg/n"
},
{
"path": "gb28181/device.go",
"chars": 8848,
"preview": "package gb28181\n\nimport (\n\t\"context\"\n\t\"github.com/ghettovoice/gosip\"\n\t\"net/http\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\tconfig \"gi"
},
{
"path": "gb28181/http_logic.go",
"chars": 5567,
"preview": "package gb28181\n\nimport (\n\t\"sync\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype GbLogic struct {\n\ts *GB28181Server\n}\n\nvar gbLogic"
},
{
"path": "gb28181/inviteoption.go",
"chars": 564,
"preview": "package gb28181\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n)\n\ntype InviteOptions struct {\n\tStart int\n\tEnd int\n\tssrc stri"
},
{
"path": "gb28181/mediaserver/conn.go",
"chars": 7430,
"preview": "package mediaserver\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/q1"
},
{
"path": "gb28181/mediaserver/mediaserver_t.go",
"chars": 317,
"preview": "package mediaserver\n\ntype MediaInfo struct {\n\tIsInvite bool\n\tSsrc uint32\n\tStreamName string\n\tSinglePort "
},
{
"path": "gb28181/mediaserver/server.go",
"chars": 2745,
"preview": "package mediaserver\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/q191201771/lal/pkg/logic\"\n\t\""
},
{
"path": "gb28181/mpegps/bitstream.go",
"chars": 7342,
"preview": "package mpegps\n\nimport (\n\t\"encoding/binary\"\n)\n\nvar BitMask [8]byte = [8]byte{0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0"
},
{
"path": "gb28181/mpegps/pes_proto.go",
"chars": 10373,
"preview": "package mpegps\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\ntype TsStreamType int\n\nconst (\n\tTsStreamAudioMpeg1 TsStreamType = 0x03\n\tTsStrea"
},
{
"path": "gb28181/mpegps/ps_demuxer.go",
"chars": 8859,
"preview": "package mpegps\n\n//单元来源于https://github.com/yapingcat/gomedia\nimport (\n\t\"errors\"\n\t\"github.com/q191201771/lal/pkg/avc\"\n\t\"gi"
},
{
"path": "gb28181/mpegps/ps_demuxer_test.go",
"chars": 6170,
"preview": "package mpegps\n\nimport (\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/lal/pkg/rt"
},
{
"path": "gb28181/mpegps/ps_muxer.go",
"chars": 4322,
"preview": "package mpegps\n\n//单元来源于https://github.com/yapingcat/gomedia\nimport (\n\t\"github.com/q191201771/lal/pkg/avc\"\n\t\"github.com/q"
},
{
"path": "gb28181/mpegps/ps_proto.go",
"chars": 17516,
"preview": "package mpegps\n\nimport (\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n)\n\ntype Error interface {\n\tNeedMore() bool\n\tParserErr"
},
{
"path": "gb28181/mpegps/util.go",
"chars": 6501,
"preview": "package mpegps\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n)\n\nconst (\n\tCodecUnknown = iota\n\tCodecH264\n\tCodecH265\n\tCodecH266\n\tCodecMpeg4"
},
{
"path": "gb28181/ptz.go",
"chars": 4432,
"preview": "package gb28181\n\nimport (\n\t\"encoding/hex\"\n\t\"encoding/xml\"\n)\n\ntype MessagePtz struct {\n\tXMLName xml.Name `xml:\"Control\"`"
},
{
"path": "gb28181/rtppub/manager.go",
"chars": 6795,
"preview": "package rtppub\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\tudpTransport \"github.com/pion/transport/v3/udp\"\n\t\"git"
},
{
"path": "gb28181/rtppub/manager_test.go",
"chars": 3153,
"preview": "package rtppub\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201"
},
{
"path": "gb28181/rtppush/lower_push_session.go",
"chars": 15745,
"preview": "package rtppush\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/q191201771/lal/pkg/aac\"\n\t\"github.com/q191201771/la"
},
{
"path": "gb28181/rtppush/lower_push_session_test.go",
"chars": 9085,
"preview": "package rtppush\n\nimport (\n\t\"io\"\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/q191201771/lal/pkg/aac\"\n\t\"github.com/q191201771/"
},
{
"path": "gb28181/server.go",
"chars": 21932,
"preview": "package gb28181\n\nimport (\n\t\"bytes\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\tudp"
},
{
"path": "gb28181/t_http_api.go",
"chars": 4804,
"preview": "package gb28181\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype DeviceInfos struct {\n\tDeviceItems []*DeviceIt"
},
{
"path": "gb28181/util.go",
"chars": 1374,
"preview": "package gb28181\n\nimport (\n\t\"bytes\"\n\t\"encoding/xml\"\n\t\"golang.org/x/net/html/charset\"\n\t\"golang.org/x/text/encoding/simplif"
},
{
"path": "gb28181/xml.go",
"chars": 1513,
"preview": "package gb28181\n\nimport (\n\t\"encoding/xml\"\n\t\"fmt\"\n)\n\nvar (\n\t// CatalogXML 获取设备列表xml样式\n\tCatalogXML = `<?xml version=\"1.0\"?"
},
{
"path": "go.mod",
"chars": 3749,
"preview": "module github.com/q191201771/lalmax\n\ngo 1.23\n\nrequire (\n\tgithub.com/abema/go-mp4 v1.2.0\n\tgithub.com/bluenviron/gohlslib "
},
{
"path": "go.sum",
"chars": 33832,
"preview": "github.com/abema/go-mp4 v1.2.0 h1:gi4X8xg/m179N/J15Fn5ugywN9vtI6PLk6iLldHGLAk=\ngithub.com/abema/go-mp4 v1.2.0/go.mod h1:"
},
{
"path": "logic/gop_cache.go",
"chars": 2921,
"preview": "package logic\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/q191201771/lal/pkg/base\"\n)\n\ntype GopCache struct {\n\tvideoheader *base.Rtm"
},
{
"path": "logic/group.go",
"chars": 13412,
"preview": "package logic\n\nimport (\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/q191201771/lalmax/fmp4/hls\"\n\n\t\"github.com/q19120177"
},
{
"path": "logic/group_manager.go",
"chars": 7062,
"preview": "package logic\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/q191201771/lalmax/fmp4/hls\"\n\t\"github.com/q191201771/naza/pkg/nazal"
},
{
"path": "logic/group_test.go",
"chars": 19430,
"preview": "package logic\n\nimport (\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/q191201771/lal/pkg/base\"\n)\n\ntype recordSubscriber struc"
},
{
"path": "logic/stat_aggregator.go",
"chars": 2548,
"preview": "package logic\n\nimport \"github.com/q191201771/lal/pkg/base\"\n\n// StatAggregator merges lal native group state with lalmax "
},
{
"path": "logic/stream_key.go",
"chars": 554,
"preview": "package logic\n\ntype StreamKey struct {\n\t// AppName 为空表示兼容历史的 streamName 单键查找。\n\tAppName string\n\tStreamName string\n}\n\nf"
},
{
"path": "logic/subscriber_stat.go",
"chars": 354,
"preview": "package logic\n\n// SubscriberStat is the runtime traffic snapshot for a lalmax external subscriber.\ntype SubscriberStat s"
},
{
"path": "main.go",
"chars": 1932,
"preview": "package main\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/q191201771/lalmax/server\"\n\n\t\"github.com/q1912"
},
{
"path": "rtc/jessibucasession.go",
"chars": 5813,
"preview": "package rtc\n\nimport (\n\t\"context\"\n\t\"math\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"github.com/gofrs/uuid\"\n\t\"github.com/pion/webrtc/v3\"\n\t\""
},
{
"path": "rtc/packer.go",
"chars": 6490,
"preview": "package rtc\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pion/rtp\"\n\t\"github.com/q191201771/lal/pkg/avc\"\n\t\"github.com/q191201771/lal/pk"
},
{
"path": "rtc/peerConnection.go",
"chars": 3187,
"preview": "package rtc\n\nimport (\n\t\"github.com/pion/ice/v2\"\n\t\"github.com/pion/interceptor\"\n\t\"github.com/pion/webrtc/v3\"\n\t\"github.com"
},
{
"path": "rtc/server.go",
"chars": 11984,
"preview": "package rtc\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\tconfig \"github.com/q191201771/lalmax/config\"\n\tmaxlogic \"github"
},
{
"path": "rtc/subscriber_stat.go",
"chars": 536,
"preview": "package rtc\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pion/webrtc/v3\"\n)\n\nfunc remoteAddrFromDTLSTransport(dtls *webrtc.DTLSTranspor"
},
{
"path": "rtc/unpacker.go",
"chars": 4625,
"preview": "package rtc\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v4/pkg/format\"\n\t\"github.com/bluenviron"
},
{
"path": "rtc/whepsession.go",
"chars": 10295,
"preview": "package rtc\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\tmaxlogic \"github.com/q191201771/lalmax/logic\""
},
{
"path": "rtc/whipsession.go",
"chars": 3412,
"preview": "package rtc\n\nimport (\n\t\"github.com/gofrs/uuid\"\n\t\"github.com/pion/webrtc/v3\"\n\t\"github.com/q191201771/lal/pkg/base\"\n\t\"gith"
},
{
"path": "run.sh",
"chars": 33,
"preview": "./lalmax -c conf/lalmax.conf.json"
},
{
"path": "server/hook_builtin_http_plugin.go",
"chars": 3406,
"preview": "package server\n\nimport \"fmt\"\n\ntype hookBuiltinHTTPPlugin struct {\n\tname string\n\thub *HttpNotify\n}\n\nfunc (p *hookBuiltin"
},
{
"path": "server/hook_filter.go",
"chars": 1893,
"preview": "package server\n\nimport (\n\t\"strings\"\n\n\tmaxlogic \"github.com/q191201771/lalmax/logic\"\n)\n\ntype HookEventFilter struct {\n\tEv"
},
{
"path": "server/hook_plugin.go",
"chars": 2083,
"preview": "package server\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n)\n\nconst defaultHookPluginBufferSize = 64\n\ntype HookPlugin interface {\n\tName() s"
},
{
"path": "server/http_notify.go",
"chars": 18136,
"preview": "// Copyright 2020, Chef. All rights reserved.\n// https://github.com/q191201771/lal\n//\n// Use of this source code is gov"
},
{
"path": "server/middle.go",
"chars": 1637,
"preview": "package server\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/q191201771/lal/pkg/base\"\n)\n\nfunc (s *LalM"
},
{
"path": "server/router.go",
"chars": 484,
"preview": "package server\n\nimport \"github.com/gin-gonic/gin\"\n\nfunc (s *LalMaxServer) InitRouter(router *gin.Engine) {\n\tif router =="
},
{
"path": "server/router_ctrl.go",
"chars": 4053,
"preview": "package server\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q19"
},
{
"path": "server/router_flv_proxy.go",
"chars": 2157,
"preview": "package server\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/q191201"
},
{
"path": "server/router_fmp4.go",
"chars": 667,
"preview": "package server\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\nfunc (s *"
},
{
"path": "server/router_helper.go",
"chars": 555,
"preview": "package server\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/q191201771/naza/pkg/nazahttp\"\n\t\"github.com/q19"
},
{
"path": "server/router_hook.go",
"chars": 2963,
"preview": "package server\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/q191201771/lal/pkg/base"
},
{
"path": "server/router_rtc.go",
"chars": 2948,
"preview": "package server\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc (s *LalMaxServer) initRtcRouter(router *gin.En"
},
{
"path": "server/router_stat.go",
"chars": 1573,
"preview": "package server\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/q191201771/lal/pkg/base\"\n\tmaxlogic \"githu"
},
{
"path": "server/router_test.go",
"chars": 27311,
"preview": "package server\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"sync\"\n\t\"sync/a"
},
{
"path": "server/router_zlm_compat.go",
"chars": 12346,
"preview": "package server\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"strconv\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/q191201771/la"
},
{
"path": "server/server.go",
"chars": 6203,
"preview": "package server\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/q191201771/lalmax/srt\"\n\n\t\"git"
},
{
"path": "server/stat_view.go",
"chars": 1750,
"preview": "package server\n\nimport (\n\t\"github.com/q191201771/lal/pkg/base\"\n\tmaxlogic \"github.com/q191201771/lalmax/logic\"\n)\n\ntype La"
},
{
"path": "server/zlm_compat_config.go",
"chars": 4046,
"preview": "package server\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n\n\tconfig \"github.com/q191201771/lalmax/con"
},
{
"path": "server/zlm_compat_ffmpeg.go",
"chars": 3506,
"preview": "package server\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n)\n\n// ffmpegRecorder 管理 ffm"
},
{
"path": "server/zlm_compat_test.go",
"chars": 24042,
"preview": "package server\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n"
},
{
"path": "server/zlm_compat_types.go",
"chars": 7106,
"preview": "package server\n\n// ZLM 兼容层请求/响应类型定义\n// 为什么放在 server 包:ZLM 兼容路由与现有 lalmax 路由同级,需访问 LalMaxServer 内部成员\n\n// ZlmFixedHeader Z"
},
{
"path": "srt/pub.go",
"chars": 2763,
"preview": "package srt\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\tsrt \"github.com/datarhei/gosrt\"\n\t\"github.com/q191201771/lal/pkg/aac\"\n\t\"github"
},
{
"path": "srt/server.go",
"chars": 3574,
"preview": "package srt\n\nimport (\n\t\"context\"\n\t\"strings\"\n\t\"time\"\n\n\tsrt \"github.com/datarhei/gosrt\"\n\t\"github.com/q191201771/lal/pkg/ba"
},
{
"path": "srt/stream_id.go",
"chars": 888,
"preview": "package srt\n\nimport (\n\t\"errors\"\n\t\"strings\"\n)\n\ntype StreamID struct {\n\tUser string\n\tHost string\n\tResource stri"
},
{
"path": "srt/sub.go",
"chars": 4481,
"preview": "package srt\n\nimport (\n\t\"context\"\n\n\tmaxlogic \"github.com/q191201771/lalmax/logic\"\n\n\tsrt \"github.com/datarhei/gosrt\"\n\t\"git"
},
{
"path": "utils/adjustdts.go",
"chars": 772,
"preview": "package utils\n\nimport \"time\"\n\ntype DtsDecoder struct {\n\tstartDts time.Duration\n\tclockRate time.Duration\n\toverall time"
},
{
"path": "version/README.md",
"chars": 84,
"preview": "这个目录用于存放lalmax版本信息说明\n\n版本格式\n\nv0.x1.x2\n\n说明如下\n\nx1为大版本,例如一个大的功能发布或者常规迭代\n\nx2为小版本,例如小问题修复\n"
},
{
"path": "version/v0.1.0.md",
"chars": 313,
"preview": "lalmax v0.1.0版本说明\n\n# 功能点\n\n(1) 支持SRT推拉流(暂不支持加密)\n\n[SRT相关说明](../document/srt.md)\n\nsrt支持以后可以使用srt推流到lalmax,然后使用rtsp/hls/rtmp"
},
{
"path": "version/v0.2.0.md",
"chars": 355,
"preview": "lalmax v0.2.0版本说明\n\n[RTC相关说明](../document/rtc.md)\n\n# 功能点\n(1)支持WHIP推流和WHEP拉流,可以对接[OBS](https://github.com/obsproject/obs-s"
}
]
About this extraction
This page contains the full source code of the q191201771/lalmax GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 115 files (576.3 KB), approximately 201.4k tokens, and a symbol index with 1109 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.