Full Code of q191201771/lalmax for AI

master ffdfe24e1a84 cached
115 files
576.3 KB
201.4k tokens
1109 symbols
1 requests
Download .txt
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

```

# 架构

![图片](document/images/init.png)

# 支持的协议
## 推流
(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
}
```
# 海康设备接入

![图片](images/gb-hk.png)


================================================
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推流配置

![图片](images/rtc_01.jpeg)

vue-wish拉流效果

![图片](images/rtc_02.png)



================================================
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的推流地址
![图片](images/srt_0.png)

(3) VLC进行播放

在VLC中设置streamid,这部分填streamid后面的所有信息
![图片](images/srt_1.png)

输入streamid前面的部分进行拉流
![图片](images/srt_2.png)

![图片](images/srt_3.png)


================================================
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.
Download .txt
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
Download .txt
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.

Copied to clipboard!