[
  {
    "path": ".github/workflows/go.yml",
    "content": "# This workflow will build a golang project\n# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go\n\nname: Go\n\non:\n  push:\n    branches: [ \"master\" ]\n  pull_request:\n    branches: [ \"master\" ]\n\njobs:\n\n  build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v3\n\n    - name: Set up Go\n      uses: actions/setup-go@v4\n      with:\n        go-version: '1.21'\n\n    - name: Build for Linux, macOS, and Windows\n      run: |\n        GOOS=linux   go build -o lalmax-linux main.go\n        GOOS=darwin  go build -o lalmax-macos main.go\n        GOOS=windows go build -o lalmax-windows.exe main.go\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "# https://github.com/wangyoucao577/go-release-action\n\nname: build-go-binary\n\non:\n  release:\n    types: [created] # 表示在创建新的 Release 时触发\n\npermissions:\n  contents: write\n  packages: write\n\njobs:\n  build-go-binary:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        goos: [linux, windows, darwin] # 需要打包的系统\n        goarch: [amd64, arm64] # 需要打包的架构\n    steps:\n      - uses: actions/checkout@v4\n      - uses: wangyoucao577/go-release-action@v1.49\n        with:\n          github_token: ${{ secrets.GITHUB_TOKEN }}\n          goos: ${{ matrix.goos }}\n          goarch: ${{ matrix.goarch }}\n          goversion: 1.22\n          md5sum: false\n          extra_files: ./README.md ./conf\n"
  },
  {
    "path": ".gitignore",
    "content": ".codex-cache/\nserver/logs/\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM golang:1.23.0\nENV GOPROXY=https://goproxy.cn,https://goproxy.io,direct\nLABEL maintainer=\"Kevin Zang\"\n\nWORKDIR /code\nCOPY . .\nRUN /bin/bash ./build.sh\n\nEXPOSE 1935 8080 4433 5544 8083 8084 1290 30000-30100/udp 6001/udp 4888/udp\n\nCMD export LD_LIBRARY_PATH=/usr/local/lib/ && ./run.sh"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2023 Chef\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# lalmax\nlalmax是在lal的基础上集成第三方库，可以提供SRT、RTC、mp4、gb28181、onvif等解决方案\n\n# 编译\n./build.sh\n\n# 运行\n./run.sh或者./lalmax -c conf/lalmax.conf.json\n\n# 配置说明\nlalmax.conf.json 配置主要由 2 部分组成\n\n(1) lalmax: lalmax 扩展能力配置，例如 SRT、RTC、HTTP-FMP4、GB28181 等，具体配置说明见[config.md](./document/config.md)\n\n(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)\n\n旧版平铺配置和 lal_config_path 仍兼容，但推荐使用 lalmax/lal 两个顶层标签维护单个配置文件。\n\n# docker运行\n```\ndocker build -t lalmax:init ./\n\ndocker 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\n\n```\n\n# 架构\n\n![图片](document/images/init.png)\n\n# 支持的协议\n## 推流\n(1) RTSP \n\n(2) SRT\n\n(3) RTMP\n\n(4) RTC(WHIP)\n\n(5) GB28181\n\n具体的推流 URL 地址见[流地址说明](./document/stream_url.md)\n\n## 拉流\n(1) RTSP\n\n(2) SRT\n\n(3) RTMP\n\n(4) HLS(S)-TS\n\n(5) HTTP(S)-FLV\n\n(6) HTTP(S)-TS\n\n(7) RTC(WHEP)\n\n(8) HTTP(S)-FMP4\n\n(9) HLS(S)-FMP4/LLHLS\n\n\n具体的拉流 URL 地址见[流地址说明](./document/stream_url.md)\n\n## [SRT](./document/srt.md)\n（1）使用gosrt库\n\n（2）暂时不支持SRT加密\n\n（3）支持H264/H265/AAC\n\n（4）可以对接OBS/VLC\n\n```\n推流url\nsrt://127.0.0.1:6001?streamid=#!::h=test110,m=publish\n\n拉流url\nsrt://127.0.0.1:6001?streamid=#!::h=test110,m=request\n```\n\n## [WebRTC](./document/rtc.md)\n（1）支持WHIP推流和WHEP拉流,暂时只支持POST信令\n\n（2）支持H264/G711A/G711U/OPUS\n\n（3）可以对接OBS、vue-wish\n\n（4）WHEP支持对接Safari HEVC\n\n（5）支持datachannel,只支持对接jessibuca播放器\n\n（6）WHIP支持对接OBS 30.2 beta HEVC\n\ndatachannel播放地址：webrtc://127.0.0.1:1290/webrtc/play/live/test110\n\n```\nWHIP推流url\nhttp(s)://127.0.0.1:1290/webrtc/whip?streamid=test110\n\nWHEP拉流url\nhttp(s)://127.0.0.1:1290/webrtc/whep?streamid=test110\n```\n\n## Http-fmp4\n(1) 支持H264/H265/AAC/G711A/G711U\n\n```\n拉流url\nhttp(s)://127.0.0.1:1290/live/m4s/test110.mp4\n```\n\n## HLS(fmp4/Low Latency)\n(1) 支持H264/H265/AAC/OPUS\n\n```\n拉流url\nhttp(s)://127.0.0.1:1290/live/hls/test110/index.m3u8\n```\n\n## [GB28181](./document/gb28181.md)\n(1) 作为SIP服务器与设备进行SIP交互,使用单端口/多端口收流\n\n(2) 提供相关API获取设备信息、播放某通道等功能\n\n(3) 支持H264/H265/AAC/G711A/G711U\n\n(4) 支持TCP/UDP\n\n\n# QQ交流群\n11818248\n\n\n\n\n"
  },
  {
    "path": "build.sh",
    "content": "#!/usr/bin/env bash\n\necho \"build lalmax\"\ngo build -o lalmax main.go"
  },
  {
    "path": "conf/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIICpDCCAYwCCQDWutSYrD7joDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls\nb2NhbGhvc3QwHhcNMjAwOTA3MTAxNzI2WhcNMzAwOTA1MTAxNzI2WjAUMRIwEAYD\nVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDh\ndywjAnWKbaQCBtNA9mbvqVsM2S7MStaw9JkcZ45L/MtKxUoP/1egqdADtBPeZxZl\nXPxx+vcox9uO2nPZ+OyjedzMtgddK8ix0u2QPdOoc8+HW0fYGKO+YOXKUXUpawIg\nZUhkUZAgrvlZUIewlZ9T0zMAsN3PUuZtg8ux+V3fY28l2QuulC5Q68i8m5vPVwj8\nQRitxtKj66fE7Ut5xIc9XAuFJcvYVFSoZuB3/xNbyVev1e2bAe4kYq/+Jt2CTZf4\ny7ESQsJn1Ybj0ippOFp9ZJq53roqEF9L0jUKNxNnJHhNc6niUfYehfvAvqXq/QiU\nB2qXpZonaq0AICMYFb+nAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAG98/mR8poSA\nMepIK6CfZsBzbDkl01R4+dw8RY9SwnOgFE5ddTh955UQvK1ORlVrTgBGkpH3djQd\n1I4ADvrYIYHekgoRQX+fFNGyviEftzUUV4aq04JTttrrWrilgoF356pkkID0xSsq\n9dr7at36wzV/Sbt9DrW8S9iBX6aUk3PPDxHJhi6xl+bYE6lepVlQ27FZjONo6cpg\nMnoRnsOhi/T+VHDUOaR+Xl4I77hESq6ipnV0GJfAAJ7R0hjfBmRbdIxo88iHN8vf\niBHq2THAF+mzApI85ASltPNF+i8ZX0Fn9CvJFwci5eiaoEjjXMbon5X6/n5ovnW/\nSaAkF3AobD0=\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "conf/key.pem",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEA4XcsIwJ1im2kAgbTQPZm76lbDNkuzErWsPSZHGeOS/zLSsVK\nD/9XoKnQA7QT3mcWZVz8cfr3KMfbjtpz2fjso3nczLYHXSvIsdLtkD3TqHPPh1tH\n2BijvmDlylF1KWsCIGVIZFGQIK75WVCHsJWfU9MzALDdz1LmbYPLsfld32NvJdkL\nrpQuUOvIvJubz1cI/EEYrcbSo+unxO1LecSHPVwLhSXL2FRUqGbgd/8TW8lXr9Xt\nmwHuJGKv/ibdgk2X+MuxEkLCZ9WG49IqaThafWSaud66KhBfS9I1CjcTZyR4TXOp\n4lH2HoX7wL6l6v0IlAdql6WaJ2qtACAjGBW/pwIDAQABAoIBADTClml64daK4Z43\nyqehAWWD0/Klv/W+bY7rLgkfkoTlmwzcLgCgV/kYw7yaHywkI3GE2O4zNDMu0YoU\nRJf1UCrREYI19nMvE7/JBB6E2UrKDv41thIzcd3S/vLhLPGMQOsjyFTxYTDEwUTN\nO3NvD+GlwoGe4cjqNVHbTYdQO09SnN7lJOIhetJ5MQWvH5kiOE+xh9AxEpYOI2K9\nRMKKaUOBQjm0Q0ezMSZ1gfwDh7i642uDvVpeQ8uDg+1BZ/ulAJpaL6pE4KTUfCAR\nygyQajvj2c93x/876ueacRBRECkvwoliOuQE8SmbjqQ5oDikHD8BNgbIwb/snZbt\nx3bj6kECgYEA+k5c9ZpqSkTtUK+lCVDV4zfSHOn//6J9pFr/f2o44nTsVUuO7Oqi\njxikbx/BjEc6veRnLu2aqqMHuw7wV6EnS7Ikbc0lRNcXsBgN0WYbT26CLmAx0G1t\n9DFyPdGT7/LZzNa11YqK/NuL2EFQ06SC7JA23+yfI5EJjOR0V6npU7sCgYEA5pgm\n9PjyyfCvOF6wPbDRyGO359aXXtavKGPomOrV1FvTGt282NpbOOoOa7XlD3013A7i\npRGiDYZO0FS/2ajYXz/uAaBh0LvKPhTZaXcezJ9GfA2HqeuTbQwhUsj17TLbgC1Y\n7xSbwMwt8uRh3KdbLf220U/WHLPJMjXsME9nBwUCgYAnyCyeHFyoUSwmlsP0JxTX\neBe84LP/PSQa6xuQdKF13H9zTv74SJJti80WnEV2thtv8s0zeDAMzrx7znQEeWh1\nb2q6yNATkNwC8M/BaCkPBtFJ7Z/9MGc5WGJ/0L9ic4aKN9XOiqZsabhgNoFSIeNt\nFb6i+EiSroqGCgkzpZ2f4QKBgHEqrLu+zVBjyWpNtgqgk2PX5HJn8yO9EnstBQK/\nBS/R3Lmrprl5+BjnbSpZO1Atr9gOihZen/wpNNazMPA+F+ou8rxjnH2XG7r5+nTy\n2++qHypUbYbrsQ9sS5JYQ7EkK2stVh8HKyUkT0yL3qcujuX0RNtWZgryBMSaiA5x\neWuNAoGAI/87JrId6LzL9RSJFnXtkbYDNw1Zf7OTtjcXytn5U64sG/DEQNt8m6um\nq/JNM32vH8Fz+JcRVVGJlN2bSsxYxpIzhd7SBS7Cq0a6gHFeRsqcTA5qidxbUPLn\nitJ84Oz1Zo2U5MG+Zj+2sLm4v6611RkYyOiSEMvvV6ZJ/TGBPDc=\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "conf/lalmax.conf.json",
    "content": "{\n  \"lalmax\": {\n    \"srt_config\": {\n      \"enable\": true,\n      \"addr\": \":6001\"\n    },\n    \"rtc_config\": {\n      \"enable\": true,\n      \"ice_host_nat_to_ips\": [],\n      \"ice_udp_mux_port\": 4888,\n      \"ice_tcp_mux_port\": 4888\n    },\n    \"http_config\": {\n      \"http_listen_addr\": \":1290\",\n      \"enable_https\": true,\n      \"https_listen_addr\": \":1233\",\n      \"https_cert_file\": \"./conf/cert.pem\",\n      \"https_key_file\": \"./conf/key.pem\",\n      \"ctrl_auth_whitelist\": {\n        \"ips\": [],\n        \"secrets\": []\n      }\n    },\n    \"fmp4_config\": {\n      \"http\": {\n        \"enable\": true\n      },\n      \"hls\": {\n        \"enable\": true,\n        \"segment_count\": 7,\n        \"segment_duration\": 1,\n        \"part_duration\": 200,\n        \"low_latency\": false\n      }\n    },\n    \"logic_config\": {\n      \"gop_cache_num\": 1,\n      \"single_gop_max_frame_num\": 0\n    },\n    \"server_id\": \"1\",\n    \"http_notify\": {\n      \"enable\": false,\n      \"update_interval_sec\": 5,\n      \"on_update\": \"http://127.0.0.1:10101/on_update\",\n      \"on_group_start\": \"http://127.0.0.1:10101/on_group_start\",\n      \"on_group_stop\": \"http://127.0.0.1:10101/on_group_stop\",\n      \"on_stream_active\": \"http://127.0.0.1:10101/on_stream_active\",\n      \"on_pub_start\": \"http://127.0.0.1:10101/on_pub_start\",\n      \"on_pub_stop\": \"http://127.0.0.1:10101/on_pub_stop\",\n      \"on_sub_start\": \"http://127.0.0.1:10101/on_sub_start\",\n      \"on_sub_stop\": \"http://127.0.0.1:10101/on_sub_stop\",\n      \"on_relay_pull_start\": \"http://127.0.0.1:10101/on_relay_pull_start\",\n      \"on_relay_pull_stop\": \"http://127.0.0.1:10101/on_relay_pull_stop\",\n      \"on_rtmp_connect\": \"http://127.0.0.1:10101/on_rtmp_connect\",\n      \"on_server_start\": \"http://127.0.0.1:10101/on_server_start\",\n      \"on_hls_make_ts\": \"http://127.0.0.1:10101/on_hls_make_ts\"\n    }\n  },\n  \"lal\": {\n    \"# doc of config\": \"./document/lal_config.md\",\n    \"conf_version\": \"v0.4.1\",\n    \"rtmp\": {\n      \"enable\": true,\n      \"addr\": \":1935\",\n      \"rtmps_enable\": true,\n      \"rtmps_addr\": \":4935\",\n      \"rtmps_cert_file\": \"./conf/cert.pem\",\n      \"rtmps_key_file\": \"./conf/key.pem\",\n      \"gop_num\": 1,\n      \"single_gop_max_frame_num\": 0,\n      \"merge_write_size\": 0\n    },\n    \"in_session\": {\n      \"add_dummy_audio_enable\": false,\n      \"add_dummy_audio_wait_audio_ms\": 150\n    },\n    \"default_http\": {\n      \"http_listen_addr\": \":8080\",\n      \"https_listen_addr\": \":4433\",\n      \"https_cert_file\": \"./conf/cert.pem\",\n      \"https_key_file\": \"./conf/key.pem\"\n    },\n    \"httpflv\": {\n      \"enable\": true,\n      \"enable_https\": true,\n      \"url_pattern\": \"/\",\n      \"gop_num\": 0,\n      \"single_gop_max_frame_num\": 0\n    },\n    \"hls\": {\n      \"enable\": false,\n      \"enable_https\": false,\n      \"url_pattern\": \"/hls/\",\n      \"out_path\": \"./lal_record/hls/\",\n      \"fragment_duration_ms\": 3000,\n      \"fragment_num\": 6,\n      \"delete_threshold\": 6,\n      \"cleanup_mode\": 1,\n      \"use_memory_as_disk_flag\": false,\n      \"sub_session_timeout_ms\": 30000,\n      \"sub_session_hash_key\": \"\"\n    },\n    \"httpts\": {\n      \"enable\": false,\n      \"enable_https\": false,\n      \"url_pattern\": \"/\",\n      \"gop_num\": 0,\n      \"single_gop_max_frame_num\": 0\n    },\n    \"rtsp\": {\n      \"enable\": true,\n      \"addr\": \":5544\",\n      \"rtsps_enable\": true,\n      \"rtsps_addr\": \":5322\",\n      \"rtsps_cert_file\": \"./conf/cert.pem\",\n      \"rtsps_key_file\": \"./conf/key.pem\",\n      \"out_wait_key_frame_flag\": true,\n      \"auth_enable\": false,\n      \"auth_method\": 1,\n      \"username\": \"q191201771\",\n      \"password\": \"pengrl\"\n    },\n    \"record\": {\n      \"enable_flv\": false,\n      \"flv_out_path\": \"./lal_record/flv/\",\n      \"enable_mpegts\": false,\n      \"mpegts_out_path\": \"./lal_record/mpegts\"\n    },\n    \"relay_push\": {\n      \"enable\": false,\n      \"addr_list\": []\n    },\n    \"static_relay_pull\": {\n      \"enable\": false,\n      \"addr\": \"\"\n    },\n    \"http_api\": {\n      \"enable\": false,\n      \"addr\": \":8083\"\n    },\n    \"server_id\": \"1\",\n    \"simple_auth\": {\n      \"key\": \"q191201771\",\n      \"dangerous_lal_secret\": \"pengrl\",\n      \"pub_rtmp_enable\": false,\n      \"sub_rtmp_enable\": false,\n      \"sub_httpflv_enable\": false,\n      \"sub_httpts_enable\": false,\n      \"pub_rtsp_enable\": false,\n      \"sub_rtsp_enable\": false,\n      \"hls_m3u8_enable\": false\n    },\n    \"pprof\": {\n      \"enable\": true,\n      \"addr\": \":8084\"\n    },\n    \"log\": {\n      \"level\": 2,\n      \"filename\": \"./logs/lalmax.log\",\n      \"is_to_stdout\": true,\n      \"is_rotate_daily\": true,\n      \"short_file_flag\": true,\n      \"timestamp_flag\": true,\n      \"timestamp_with_ms_flag\": true,\n      \"level_flag\": true,\n      \"assert_behavior\": 1\n    },\n    \"debug\": {\n      \"log_group_interval_sec\": 30,\n      \"log_group_max_group_num\": 10,\n      \"log_group_max_sub_num_per_group\": 10\n    }\n  }\n}\n"
  },
  {
    "path": "config/config.go",
    "content": "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\tSrtConfig        SrtConfig        `json:\"srt_config\"`      // srt配置\n\tRtcConfig        RtcConfig        `json:\"rtc_config\"`      // rtc配置\n\tHttpConfig       HttpConfig       `json:\"http_config\"`     // http/https配置\n\tFmp4Config       Fmp4Config       `json:\"fmp4_config\"`     // fmp4配置\n\tGB28181Config    GB28181Config    `json:\"gb28181_config\"`  // gb28181配置\n\tServerId         string           `json:\"server_id\"`       // http 通知唯一标识\n\tHttpNotifyConfig HttpNotifyConfig `json:\"http_notify\"`     // http 通知配置\n\tLalSvrConfigPath string           `json:\"lal_config_path\"` // lal配置文件路径，兼容旧版配置\n\tLogicConfig      LogicConfig      `json:\"logic_config\"`    // 扩展流组配置\n\tLalRawContent    []byte           `json:\"-\"`               // lal 原始配置内容\n\tConfFilePath     string           `json:\"-\"`               // 配置文件路径，用于持久化\n}\n\ntype SrtConfig struct {\n\tEnable bool   `json:\"enable\"` // srt服务使能配置\n\tAddr   string `json:\"addr\"`   // srt服务监听地址\n}\n\ntype RtcConfig struct {\n\tEnable          bool     `json:\"enable\"`              // rtc服务使能配置\n\tICEHostNATToIPs []string `json:\"ice_host_nat_to_ips\"` // rtc服务公网IP，未设置使用内网\n\tICEUDPMuxPort   int      `json:\"ice_udp_mux_port\"`    // rtc udp mux port\n\tICETCPMuxPort   int      `json:\"ice_tcp_mux_port\"`    // rtc tcp mux port\n\tWriteChanSize   int      `json:\"write_chan_size\"`\n}\n\ntype HttpConfig struct {\n\tListenAddr        string            `json:\"http_listen_addr\"`  // http服务监听地址\n\tEnableHttps       bool              `json:\"enable_https\"`      // https使能标志\n\tHttpsListenAddr   string            `json:\"https_listen_addr\"` // https监听地址\n\tHttpsCertFile     string            `json:\"https_cert_file\"`   // https cert 文件\n\tHttpsKeyFile      string            `json:\"https_key_file\"`    // https key 文件\n\tCtrlAuthWhitelist CtrlAuthWhitelist `json:\"ctrl_auth_whitelist\"`\n}\n\n// CtrlAuthWhitelist 控制类接口鉴权。\ntype CtrlAuthWhitelist struct {\n\tIPs     []string // 允许访问的远程 IP，零值时不生效\n\tSecrets []string // 认证信息，零值时不生效\n}\n\ntype Fmp4Config struct {\n\tHttp Fmp4HttpConfig `json:\"http\"`\n\tHls  Fmp4HlsConfig  `json:\"hls\"`\n}\n\ntype Fmp4HttpConfig struct {\n\tEnable bool `json:\"enable\"` // http-fmp4使能标志\n}\n\ntype Fmp4HlsConfig struct {\n\tEnable          bool `json:\"enable\"`           // hls使能标志\n\tSegmentCount    int  `json:\"segment_count\"`    // 分片个数,llhls默认7个\n\tSegmentDuration int  `json:\"segment_duration\"` // hls分片时长,默认1s\n\tPartDuration    int  `json:\"part_duration\"`    // llhls part时长,默认200ms\n\tLowLatency      bool `json:\"low_latency\"`      // 是否开启llhls\n}\n\ntype GB28181Config struct {\n\tEnable            bool               `json:\"enable\"`             // gb28181使能标志\n\tListenAddr        string             `json:\"listen_addr\"`        // gb28181监听地址\n\tSipIP             string             `json:\"sip_ip\"`             // sip 服务器公网IP\n\tSipPort           uint16             `json:\"sip_port\"`           // sip 服务器端口，默认 5060\n\tSerial            string             `json:\"serial\"`             // sip 服务器 id, 默认 34020000002000000001\n\tRealm             string             `json:\"realm\"`              // sip 服务器域，默认 3402000000\n\tUsername          string             `json:\"username\"`           // sip 服务器账号\n\tPassword          string             `json:\"password\"`           // sip 服务器密码\n\tKeepaliveInterval int                `json:\"keepalive_interval\"` // 心跳包时长\n\tQuickLogin        bool               `json:\"quick_login\"`        // 快速登陆,有keepalive就认为在线\n\tMediaConfig       GB28181MediaConfig `json:\"media_config\"`       // 媒体服务器配置\n}\n\ntype GB28181MediaConfig struct {\n\tMediaIp               string `json:\"media_ip\"`                 // 流媒体IP,用于在SDP中指定\n\tListenPort            uint16 `json:\"listen_port\"`              // tcp,udp监听端口 默认启动\n\tMultiPortMaxIncrement uint16 `json:\"multi_port_max_increment\"` // 多端口范围 ListenPort+1至ListenPort+MultiPortMax\n}\n\n// ZlmCompatHookConfig ZLM 兼容 hook URL 配置\n// 为什么独立结构体：隔离 ZLM 适配层，lalmax 原有字段保持不变\ntype ZlmCompatHookConfig struct {\n\tZlmOnStreamChanged    string `json:\"zlm_on_stream_changed\"`\n\tZlmOnServerKeepalive  string `json:\"zlm_on_server_keepalive\"`\n\tZlmOnStreamNoneReader string `json:\"zlm_on_stream_none_reader\"`\n\tZlmOnRtpServerTimeout string `json:\"zlm_on_rtp_server_timeout\"`\n\tZlmOnRecordMp4        string `json:\"zlm_on_record_mp4\"`\n\tZlmOnPublish          string `json:\"zlm_on_publish\"`\n\tZlmOnPlay             string `json:\"zlm_on_play\"`\n\tZlmOnStreamNotFound   string `json:\"zlm_on_stream_not_found\"`\n\tZlmOnServerStarted    string `json:\"zlm_on_server_started\"`\n}\n\n// HasZlmHooks 任一 ZLM 兼容 hook 字段有值即返回 true\n// 为什么：ZLM 回调与 lalmax 原有回调二选一，此方法为判断条件\nfunc (c ZlmCompatHookConfig) HasZlmHooks() bool {\n\treturn c.ZlmOnStreamChanged != \"\" ||\n\t\tc.ZlmOnServerKeepalive != \"\" ||\n\t\tc.ZlmOnStreamNoneReader != \"\" ||\n\t\tc.ZlmOnRtpServerTimeout != \"\" ||\n\t\tc.ZlmOnRecordMp4 != \"\" ||\n\t\tc.ZlmOnPublish != \"\" ||\n\t\tc.ZlmOnPlay != \"\" ||\n\t\tc.ZlmOnStreamNotFound != \"\"\n}\n\ntype HttpNotifyConfig struct {\n\tEnable               bool   `json:\"enable\"`\n\tUpdateIntervalSec    int    `json:\"update_interval_sec\"`\n\tKeepaliveIntervalSec int    `json:\"keepalive_interval_sec\"`\n\tHookTimeoutSec       int    `json:\"hook_timeout_sec\"`\n\tOnServerStart        string `json:\"on_server_start\"`\n\tOnUpdate             string `json:\"on_update\"`\n\tOnGroupStart         string `json:\"on_group_start\"`\n\tOnGroupStop          string `json:\"on_group_stop\"`\n\tOnStreamActive       string `json:\"on_stream_active\"`\n\tOnPubStart           string `json:\"on_pub_start\"`\n\tOnPubStop            string `json:\"on_pub_stop\"`\n\tOnSubStart           string `json:\"on_sub_start\"`\n\tOnSubStop            string `json:\"on_sub_stop\"`\n\tOnRelayPullStart     string `json:\"on_relay_pull_start\"`\n\tOnRelayPullStop      string `json:\"on_relay_pull_stop\"`\n\tOnRtmpConnect        string `json:\"on_rtmp_connect\"`\n\tOnHlsMakeTs          string `json:\"on_hls_make_ts\"`\n\n\t// --- ZLM 兼容 hook 配置 ---\n\tZlmCompatHookConfig\n}\n\ntype LogicConfig struct {\n\tGopCacheNum          int `json:\"gop_cache_num\"`\n\tSingleGopMaxFrameNum int `json:\"single_gop_max_frame_num\"`\n}\n\nfunc Open(filepath string) error {\n\tdata, err := ioutil.ReadFile(filepath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = Unmarshal(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc Unmarshal(data []byte) error {\n\tvar file struct {\n\t\tLalMax json.RawMessage `json:\"lalmax\"`\n\t\tLal    json.RawMessage `json:\"lal\"`\n\t}\n\tif err := json.Unmarshal(data, &file); err != nil {\n\t\treturn err\n\t}\n\n\tvar cfg Config\n\tif len(file.LalMax) != 0 {\n\t\tif err := unmarshalConfig(file.LalMax, &cfg); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcfg.LalRawContent = append([]byte(nil), file.Lal...)\n\t} else {\n\t\tif err := unmarshalConfig(data, &cfg); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tdefaultConfig = cfg\n\treturn nil\n}\n\nfunc unmarshalConfig(data []byte, cfg *Config) error {\n\tif err := json.Unmarshal(data, cfg); err != nil {\n\t\treturn err\n\t}\n\tvar raw map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &raw); err != nil {\n\t\treturn err\n\t}\n\tif cfg.LalSvrConfigPath == \"\" {\n\t\tvar legacy struct {\n\t\t\tLalSvrConfigPath string `json:\"lal_config_path:\"`\n\t\t}\n\t\tif err := json.Unmarshal(data, &legacy); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcfg.LalSvrConfigPath = legacy.LalSvrConfigPath\n\t}\n\tif _, ok := raw[\"logic_config\"]; !ok {\n\t\tvar legacy struct {\n\t\t\tLogicConfig LogicConfig `json:\"hook_config\"`\n\t\t}\n\t\tif err := json.Unmarshal(data, &legacy); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcfg.LogicConfig = legacy.LogicConfig\n\t}\n\tif _, ok := raw[\"fmp4_config\"]; !ok {\n\t\tvar legacy struct {\n\t\t\tHttp Fmp4HttpConfig `json:\"httpfmp4_config\"`\n\t\t\tHls  Fmp4HlsConfig  `json:\"hls_config\"`\n\t\t}\n\t\tif err := json.Unmarshal(data, &legacy); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tcfg.Fmp4Config.Http = legacy.Http\n\t\tcfg.Fmp4Config.Hls = legacy.Hls\n\t}\n\treturn nil\n}\n\nfunc GetConfig() *Config {\n\treturn &defaultConfig\n}\n\n// SaveToFile 将当前配置持久化到配置文件\n// 为什么：setServerConfig 动态修改后需落盘，重启后配置仍生效\nfunc (c *Config) SaveToFile() error {\n\tif c.ConfFilePath == \"\" {\n\t\treturn nil\n\t}\n\n\tdata, err := os.ReadFile(c.ConfFilePath)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"read config file: %w\", err)\n\t}\n\n\tvar file map[string]json.RawMessage\n\tif err := json.Unmarshal(data, &file); err != nil {\n\t\treturn fmt.Errorf(\"parse config file: %w\", err)\n\t}\n\n\tlalmax, err := json.MarshalIndent(c, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"marshal lalmax config: %w\", err)\n\t}\n\tfile[\"lalmax\"] = lalmax\n\n\tout, err := json.MarshalIndent(file, \"\", \"  \")\n\tif err != nil {\n\t\treturn fmt.Errorf(\"marshal config file: %w\", err)\n\t}\n\tout = append(out, '\\n')\n\n\treturn os.WriteFile(c.ConfFilePath, out, 0o644)\n}\n"
  },
  {
    "path": "config/config_test.go",
    "content": "package config\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestUnmarshalStructuredConfig(t *testing.T) {\n\traw := []byte(`{\n\t\t\"lalmax\": {\n\t\t\t\"srt_config\": {\n\t\t\t\t\"enable\": true,\n\t\t\t\t\"addr\": \":6001\"\n\t\t\t},\n\t\t\t\"server_id\": \"lalmax-1\"\n\t\t},\n\t\t\"lal\": {\n\t\t\t\"rtmp\": {\n\t\t\t\t\"enable\": true,\n\t\t\t\t\"addr\": \":1935\"\n\t\t\t}\n\t\t}\n\t}`)\n\n\tif err := Unmarshal(raw); err != nil {\n\t\tt.Fatalf(\"unmarshal structured config: %v\", err)\n\t}\n\n\tcfg := GetConfig()\n\tif !cfg.SrtConfig.Enable || cfg.SrtConfig.Addr != \":6001\" {\n\t\tt.Fatalf(\"unexpected srt config: %+v\", cfg.SrtConfig)\n\t}\n\tif cfg.ServerId != \"lalmax-1\" {\n\t\tt.Fatalf(\"unexpected server id: %s\", cfg.ServerId)\n\t}\n\tif !strings.Contains(string(cfg.LalRawContent), `\"rtmp\"`) {\n\t\tt.Fatalf(\"lal raw content not preserved: %s\", string(cfg.LalRawContent))\n\t}\n}\n\nfunc TestUnmarshalLegacyConfig(t *testing.T) {\n\traw := []byte(`{\n\t\t\"srt_config\": {\n\t\t\t\"enable\": true,\n\t\t\t\"addr\": \":6001\"\n\t\t},\n\t\t\"httpfmp4_config\": {\n\t\t\t\"enable\": true\n\t\t},\n\t\t\"hls_config\": {\n\t\t\t\"enable\": true,\n\t\t\t\"segment_count\": 3,\n\t\t\t\"segment_duration\": 2,\n\t\t\t\"part_duration\": 100,\n\t\t\t\"low_latency\": true\n\t\t},\n\t\t\"hook_config\": {\n\t\t\t\"gop_cache_num\": 3,\n\t\t\t\"single_gop_max_frame_num\": 120\n\t\t},\n\t\t\"lal_config_path:\": \"./conf/lalserver.conf.json\"\n\t}`)\n\n\tif err := Unmarshal(raw); err != nil {\n\t\tt.Fatalf(\"unmarshal legacy config: %v\", err)\n\t}\n\n\tcfg := GetConfig()\n\tif !cfg.SrtConfig.Enable || cfg.SrtConfig.Addr != \":6001\" {\n\t\tt.Fatalf(\"unexpected srt config: %+v\", cfg.SrtConfig)\n\t}\n\tif cfg.LalSvrConfigPath != \"./conf/lalserver.conf.json\" {\n\t\tt.Fatalf(\"unexpected lal config path: %s\", cfg.LalSvrConfigPath)\n\t}\n\tif cfg.LogicConfig.GopCacheNum != 3 || cfg.LogicConfig.SingleGopMaxFrameNum != 120 {\n\t\tt.Fatalf(\"unexpected legacy logic config: %+v\", cfg.LogicConfig)\n\t}\n\tif !cfg.Fmp4Config.Http.Enable {\n\t\tt.Fatalf(\"unexpected legacy fmp4 http config: %+v\", cfg.Fmp4Config.Http)\n\t}\n\tif !cfg.Fmp4Config.Hls.Enable || cfg.Fmp4Config.Hls.SegmentCount != 3 || cfg.Fmp4Config.Hls.SegmentDuration != 2 || cfg.Fmp4Config.Hls.PartDuration != 100 || !cfg.Fmp4Config.Hls.LowLatency {\n\t\tt.Fatalf(\"unexpected legacy fmp4 hls config: %+v\", cfg.Fmp4Config.Hls)\n\t}\n\tif len(cfg.LalRawContent) != 0 {\n\t\tt.Fatalf(\"legacy config should not set lal raw content: %s\", string(cfg.LalRawContent))\n\t}\n}\n\nfunc TestUnmarshalStructuredFmp4ConfigKeepsExplicitZero(t *testing.T) {\n\traw := []byte(`{\n\t\t\"lalmax\": {\n\t\t\t\"fmp4_config\": {\n\t\t\t\t\"http\": {\n\t\t\t\t\t\"enable\": false\n\t\t\t\t},\n\t\t\t\t\"hls\": {\n\t\t\t\t\t\"enable\": true,\n\t\t\t\t\t\"segment_count\": 0,\n\t\t\t\t\t\"segment_duration\": 0,\n\t\t\t\t\t\"part_duration\": 0,\n\t\t\t\t\t\"low_latency\": false\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"httpfmp4_config\": {\n\t\t\t\t\"enable\": true\n\t\t\t},\n\t\t\t\"hls_config\": {\n\t\t\t\t\"enable\": true,\n\t\t\t\t\"segment_count\": 3,\n\t\t\t\t\"segment_duration\": 2,\n\t\t\t\t\"part_duration\": 100,\n\t\t\t\t\"low_latency\": true\n\t\t\t}\n\t\t}\n\t}`)\n\n\tif err := Unmarshal(raw); err != nil {\n\t\tt.Fatalf(\"unmarshal structured config: %v\", err)\n\t}\n\n\tcfg := GetConfig()\n\tif cfg.Fmp4Config.Http.Enable {\n\t\tt.Fatalf(\"explicit fmp4 http config should not be overwritten: %+v\", cfg.Fmp4Config.Http)\n\t}\n\tif !cfg.Fmp4Config.Hls.Enable || cfg.Fmp4Config.Hls.SegmentCount != 0 || cfg.Fmp4Config.Hls.SegmentDuration != 0 || cfg.Fmp4Config.Hls.PartDuration != 0 || cfg.Fmp4Config.Hls.LowLatency {\n\t\tt.Fatalf(\"explicit fmp4 hls config should not be overwritten: %+v\", cfg.Fmp4Config.Hls)\n\t}\n}\n\nfunc TestUnmarshalStructuredLogicConfigKeepsExplicitZero(t *testing.T) {\n\traw := []byte(`{\n\t\t\"lalmax\": {\n\t\t\t\"logic_config\": {\n\t\t\t\t\"gop_cache_num\": 0,\n\t\t\t\t\"single_gop_max_frame_num\": 0\n\t\t\t},\n\t\t\t\"hook_config\": {\n\t\t\t\t\"gop_cache_num\": 3,\n\t\t\t\t\"single_gop_max_frame_num\": 120\n\t\t\t}\n\t\t}\n\t}`)\n\n\tif err := Unmarshal(raw); err != nil {\n\t\tt.Fatalf(\"unmarshal structured config: %v\", err)\n\t}\n\n\tcfg := GetConfig()\n\tif cfg.LogicConfig.GopCacheNum != 0 || cfg.LogicConfig.SingleGopMaxFrameNum != 0 {\n\t\tt.Fatalf(\"explicit logic config should not be overwritten: %+v\", cfg.LogicConfig)\n\t}\n}\n"
  },
  {
    "path": "document/api.md",
    "content": "# HTTP API 总览\n\n`lalmax` 对外建议统一只暴露一个 HTTP 管理入口，也就是 `lalmax.http_config.http_listen_addr`，示例配置通常是 `:1290`。  \n`lal.http_api` 仍然可以保留给排查 `lal` 原生行为时使用，但默认建议关闭，不要和 `lalmax` 的管理入口混用。\n\n建议配合以下文档一起看：\n\n- [api_gateway.md](./api_gateway.md)：统一入口和状态聚合说明\n- [hook_api.md](./hook_api.md)：Hook 查询与订阅接口\n- [hook_plugin_architecture.md](./hook_plugin_architecture.md)：HookHub 与插件化架构\n- [lal_api.md](./lal_api.md)：`lal` 原生 HTTP API 的定位和兼容关系\n\n## 基本约定\n\n- 对外统一入口默认是 `http://127.0.0.1:1290`\n- `/api/stat/*`、`/api/ctrl/*`、`/api/hook/*` 共用同一套鉴权配置：`lalmax.http_config.ctrl_auth_whitelist`\n- 如果鉴权失败，HTTP 状态码仍然是 `200`，返回体里的 `error_code` 为 `401`\n- 除 `GET /api/hook/stream` 之外，其余接口都返回 JSON\n\n统一返回结构如下：\n\n```json\n{\n  \"error_code\": 0,\n  \"desp\": \"succ\",\n  \"data\": {}\n}\n```\n\n常见 `error_code` 如下：\n\n| error_code | desp | 说明 |\n| --- | --- | --- |\n| 0 | succ | 调用成功 |\n| 401 | Unauthorized | 鉴权失败 |\n| 1001 | group not found | 流分组不存在，或者仅传 `stream_name` 时无法唯一定位 |\n| 1002 | param missing | 必填参数缺失 |\n| 1003 | session not found | 会话不存在 |\n| 2001 | 具体错误信息见 `desp` | `start_relay_pull` 执行失败 |\n| 2002 | 具体错误信息见 `desp` | `start_rtp_pub` 执行失败 |\n\n## 当前接口列表\n\n### 统计接口\n\n- `GET /api/stat/group`\n- `GET /api/stat/all_group`\n- `GET /api/stat/lal_info`\n\n### 控制接口\n\n- `POST /api/ctrl/start_relay_pull`\n- `GET /api/ctrl/stop_relay_pull`\n- `POST /api/ctrl/stop_relay_pull`\n- `POST /api/ctrl/kick_session`\n- `POST /api/ctrl/start_rtp_pub`\n- `POST /api/ctrl/stop_rtp_pub`\n\n### Hook 接口\n\n- `GET /api/hook/recent`\n- `GET /api/hook/stream`\n\n## 统计接口\n\n### `GET /api/stat/group`\n\n查询单个流分组的当前状态。\n\n请求参数：\n\n- `stream_name`：必填，流名\n- `app_name`：可选，应用名\n\n说明：\n\n- 如果同一个 `stream_name` 只对应一个流分组，可以只传 `stream_name`\n- 如果同一个 `stream_name` 在多个 `app_name` 下都存在，建议同时传 `app_name + stream_name`\n- 当前返回结果会在兼容 `lal` 原生字段的基础上，额外补充 `lalmax.ext_subs`\n\n请求示例：\n\n```bash\ncurl \"http://127.0.0.1:1290/api/stat/group?stream_name=test110\"\ncurl \"http://127.0.0.1:1290/api/stat/group?app_name=live&stream_name=test110\"\n```\n\n返回示例：\n\n```json\n{\n  \"error_code\": 0,\n  \"desp\": \"succ\",\n  \"data\": {\n    \"stream_name\": \"test110\",\n    \"app_name\": \"live\",\n    \"audio_codec\": \"AAC\",\n    \"video_codec\": \"H264\",\n    \"video_width\": 1920,\n    \"video_height\": 1080,\n    \"pub\": {\n      \"session_id\": \"RTMPPUB1\",\n      \"protocol\": \"RTMP\",\n      \"base_type\": \"PUB\"\n    },\n    \"subs\": [\n      {\n        \"session_id\": \"RTMPSUB1\",\n        \"protocol\": \"RTMP\",\n        \"base_type\": \"SUB\"\n      },\n      {\n        \"session_id\": \"whep-123\",\n        \"protocol\": \"WHEP\",\n        \"base_type\": \"SUB\"\n      }\n    ],\n    \"pull\": {\n      \"base_type\": \"PULL\"\n    },\n    \"in_frame_per_sec\": [],\n    \"lalmax\": {\n      \"ext_subs\": [\n        {\n          \"session_id\": \"whep-123\",\n          \"protocol\": \"WHEP\",\n          \"base_type\": \"SUB\"\n        }\n      ]\n    }\n  }\n}\n```\n\n字段语义：\n\n- `subs`：统一后的订阅者视图，包含 `lal` 原生订阅者和 `lalmax` 扩展订阅者\n- `lalmax.ext_subs`：只包含 `lalmax` 扩展层维护的订阅者，便于业务侧区分来源\n\n### `GET /api/stat/all_group`\n\n查询当前所有流分组。\n\n请求示例：\n\n```bash\ncurl \"http://127.0.0.1:1290/api/stat/all_group\"\n```\n\n返回示例：\n\n```json\n{\n  \"error_code\": 0,\n  \"desp\": \"succ\",\n  \"data\": {\n    \"groups\": [\n      {\n        \"stream_name\": \"test110\",\n        \"app_name\": \"live\",\n        \"lalmax\": {\n          \"ext_subs\": []\n        }\n      }\n    ]\n  }\n}\n```\n\n其中 `groups[*]` 的结构和 `/api/stat/group` 的 `data` 完全一致。\n\n### `GET /api/stat/lal_info`\n\n查询服务基础信息。\n\n请求示例：\n\n```bash\ncurl \"http://127.0.0.1:1290/api/stat/lal_info\"\n```\n\n该接口直接返回内嵌 `lal` 的运行信息，常见字段包括：\n\n- `server_id`\n- `bin_info`\n- `lal_version`\n- `api_version`\n- `notify_version`\n- `start_time`\n\n## 控制接口\n\n### `POST /api/ctrl/start_relay_pull`\n\n让服务主动去远端拉流。\n\n请求体为 JSON，必填字段：\n\n- `url`\n\n常用可选字段：\n\n- `stream_name`\n- `pull_timeout_ms`\n- `pull_retry_num`\n- `auto_stop_pull_after_no_out_ms`\n- `rtsp_mode`\n- `debug_dump_packet`\n\n当前程序里的默认值如下：\n\n- `pull_timeout_ms` 默认 `10000`\n- `pull_retry_num` 默认 `0`\n- `auto_stop_pull_after_no_out_ms` 默认 `-1`\n- `rtsp_mode` 默认 `0`，也就是 TCP\n\n请求示例：\n\n```bash\ncurl -H \"Content-Type: application/json\" \\\n  -X POST \\\n  -d \"{\\\"url\\\":\\\"rtmp://127.0.0.1/live/test110\\\"}\" \\\n  \"http://127.0.0.1:1290/api/ctrl/start_relay_pull\"\n```\n\n说明：\n\n- 接口返回成功，只代表命令已经被接受\n- 是否真的拉流成功，需要看后续状态接口，或者看 Hook 事件中的 `on_relay_pull_start`、`on_update`\n\n### `GET /api/ctrl/stop_relay_pull`\n### `POST /api/ctrl/stop_relay_pull`\n\n关闭指定的 relay pull 会话。\n\n当前实现里，这个接口无论用 `GET` 还是 `POST`，都从查询参数里读取：\n\n- `stream_name`：必填\n\n请求示例：\n\n```bash\ncurl \"http://127.0.0.1:1290/api/ctrl/stop_relay_pull?stream_name=test110\"\ncurl -X POST \"http://127.0.0.1:1290/api/ctrl/stop_relay_pull?stream_name=test110\"\n```\n\n说明：\n\n- 也可以用 `kick_session` 关闭 pull 会话\n\n### `POST /api/ctrl/kick_session`\n\n强制关闭指定会话。\n\n请求体为 JSON，必填字段：\n\n- `stream_name`\n- `session_id`\n\n请求示例：\n\n```bash\ncurl -H \"Content-Type: application/json\" \\\n  -X POST \\\n  -d \"{\\\"stream_name\\\":\\\"test110\\\",\\\"session_id\\\":\\\"FLVSUB1\\\"}\" \\\n  \"http://127.0.0.1:1290/api/ctrl/kick_session\"\n```\n\n适用对象：\n\n- 推流会话\n- 拉流会话\n- relay pull 会话\n\n### `POST /api/ctrl/start_rtp_pub`\n\n打开一个 GB28181/RTP 接收会话。\n\n请求体为 JSON，必填字段：\n\n- `stream_name`\n\n常用可选字段：\n\n- `port`\n- `timeout_ms`\n- `is_tcp_flag`\n- `debug_dump_packet`\n\n当前程序里的默认值：\n\n- `timeout_ms` 默认 `60000`\n\n请求示例：\n\n```bash\ncurl -H \"Content-Type: application/json\" \\\n  -X POST \\\n  -d \"{\\\"stream_name\\\":\\\"gb28181-test\\\",\\\"port\\\":0}\" \\\n  \"http://127.0.0.1:1290/api/ctrl/start_rtp_pub\"\n```\n\n说明：\n\n- `port=0` 表示由服务自动分配端口\n- 成功后会返回 `session_id` 和最终监听端口\n\n### `POST /api/ctrl/stop_rtp_pub`\n\n关闭 GB28181/RTP 接收会话。\n\n当前实现支持两种传参方式：\n\n- 查询参数：`stream_name` 或 `session_id`\n- JSON 请求体：`stream_name` 或 `session_id`\n\n两者至少传一个。\n\n请求示例：\n\n```bash\ncurl -X POST \"http://127.0.0.1:1290/api/ctrl/stop_rtp_pub?stream_name=gb28181-test\"\ncurl -H \"Content-Type: application/json\" \\\n  -X POST \\\n  -d \"{\\\"session_id\\\":\\\"PSSUB1\\\"}\" \\\n  \"http://127.0.0.1:1290/api/ctrl/stop_rtp_pub\"\n```\n\n成功后会返回被关闭的 `session_id`。\n\n## Hook 接口\n\n### `GET /api/hook/recent`\n\n读取最近的 Hook 事件快照。\n\n常用查询参数：\n\n- `limit`：返回条数，默认 `20`\n- `app_name`\n- `stream_name`\n- `session_id`\n- `event`\n- `events`：多个事件名，逗号分隔\n\n请求示例：\n\n```bash\ncurl \"http://127.0.0.1:1290/api/hook/recent?limit=5\"\ncurl \"http://127.0.0.1:1290/api/hook/recent?stream_name=test110&events=on_group_start,on_stream_active,on_group_stop,on_update\"\n```\n\n### `GET /api/hook/stream`\n\n使用 SSE 持续订阅 Hook 事件。\n\n它和 `/api/hook/recent` 使用同一套过滤参数。连接建立后，会先回放最近一批命中的事件，然后继续推送实时事件。\n\n请求示例：\n\n```bash\ncurl -N \"http://127.0.0.1:1290/api/hook/stream\"\ncurl -N \"http://127.0.0.1:1290/api/hook/stream?stream_name=test110&events=on_update,on_group_stop\"\n```\n\n返回格式示例：\n\n```text\nid: 12\nevent: on_pub_start\ndata: {\"server_id\":\"1\",\"session_id\":\"RTMPPUB1\",\"protocol\":\"RTMP\",\"base_type\":\"PUB\",\"stream_name\":\"test110\"}\n```\n\n当前 Hook 体系里的事件名称、语义、过滤规则和插件化接入方式，请直接参考 [hook_api.md](./hook_api.md) 和 [hook_plugin_architecture.md](./hook_plugin_architecture.md)。\n"
  },
  {
    "path": "document/api_gateway.md",
    "content": "# API Gateway\n\n`lalmax` 作为 `lal` 的统一 API 网关，对外建议只暴露一个 HTTP 入口。\n\nHook 体系的详细设计见 [hook_plugin_architecture.md](./hook_plugin_architecture.md)。\n\n默认入口来自：\n\n```json\n{\n  \"lalmax\": {\n    \"http_config\": {\n      \"http_listen_addr\": \":1290\"\n    }\n  }\n}\n```\n\n## Exposed Routes\n\n### Stat\n\n- `GET /api/stat/group`\n- `GET /api/stat/all_group`\n- `GET /api/stat/lal_info`\n\n### Control\n\n- `POST /api/ctrl/start_relay_pull`\n- `GET /api/ctrl/stop_relay_pull`\n- `POST /api/ctrl/stop_relay_pull`\n- `POST /api/ctrl/kick_session`\n- `POST /api/ctrl/start_rtp_pub`\n- `POST /api/ctrl/stop_rtp_pub`\n\n### Hook\n\n- `GET /api/hook/recent`\n- `GET /api/hook/stream`\n\n## Why Use lalmax Gateway\n\n- `lal` 原生流状态仍由 `lal` 负责，避免双事实源\n- `lalmax` 在响应中补充扩展协议订阅者统计\n- hook 事件统一从 `lalmax` 读取，不必同时维护 HTTP notify 和内部状态\n- 控制接口、查询接口、hook 接口共用一套鉴权策略\n\n## Group Visibility\n\n`lalmax` 获取 group 视图的方式是：\n\n1. 通过内嵌 `lal` 的 `StatAllGroup()` 获取原生 group 快照\n2. 通过 `lalmax/logic` 获取扩展订阅者状态\n3. 聚合成统一视图后再对外返回或分发到 hook hub\n\n这样 `lalmax` 可以知道 `lal group` 中所有流的原生状态，同时保留自己的扩展消费层状态。\n\n## Stat Response Extension\n\n`/api/stat/group` 和 `/api/stat/all_group` 在保持 `lal` 原有字段的同时，会额外返回一个 `lalmax` 扩展块。\n\n兼容原则如下：\n\n- 原有 `stream_name`、`app_name`、`pub`、`subs`、`pull`、`in_frame_per_sec` 等字段继续保留\n- `subs` 仍然表示统一后的订阅者视图，其中会合并 `lal` 原生订阅者和 `lalmax` 扩展订阅者\n- `lalmax.ext_subs` 只列出来自 `lalmax` 扩展层的订阅者，便于业务侧区分来源\n\n示例：\n\n```json\n{\n  \"error_code\": 0,\n  \"desp\": \"succ\",\n  \"data\": {\n    \"stream_name\": \"camera01\",\n    \"app_name\": \"live\",\n    \"pub\": {},\n    \"subs\": [\n      {\n        \"session_id\": \"RTMPSUB1\",\n        \"protocol\": \"RTMP\"\n      },\n      {\n        \"session_id\": \"whep-123\",\n        \"protocol\": \"WHEP\"\n      }\n    ],\n    \"pull\": {},\n    \"in_frame_per_sec\": [],\n    \"lalmax\": {\n      \"ext_subs\": [\n        {\n          \"session_id\": \"whep-123\",\n          \"protocol\": \"WHEP\"\n        }\n      ]\n    }\n  }\n}\n```\n\n如果业务只想拿 `lal` 原生兼容视图，可以继续只读原字段；如果业务需要知道 `lalmax` 在该流上维护了哪些扩展订阅者，则读取 `lalmax.ext_subs`。\n\n## Control API Scope\n\n`/api/ctrl/*` 仍然保持轻量控制接口定位，不会在响应中额外塞入完整流状态、订阅者列表或 `lalmax` 扩展统计。\n\n原因是：\n\n- 控制接口的职责是执行动作并返回动作结果\n- 流状态属于查询语义，应统一从 `/api/stat/*` 获取\n- 避免控制响应膨胀，降低兼容性和调用方解析成本\n"
  },
  {
    "path": "document/config.md",
    "content": "# lalmax 配置说明\n\n本文档说明 `conf/lalmax.conf.json` 里 `lalmax` 这一段的配置。  \n`lal` 原生配置请看 [lal_config.md](./lal_config.md)。\n\n## 推荐配置结构\n\n当前程序推荐使用一个统一的配置文件，并按顶层标签拆开：\n\n```json\n{\n  \"lalmax\": {\n    \"server_id\": \"1\",\n    \"srt_config\": {},\n    \"rtc_config\": {},\n    \"http_config\": {},\n    \"fmp4_config\": {},\n    \"logic_config\": {},\n    \"http_notify\": {},\n    \"gb28181_config\": {}\n  },\n  \"lal\": {}\n}\n```\n\n说明：\n\n- `lalmax`：`lalmax` 自己的扩展能力配置\n- `lal`：内嵌 `lal` 的原生配置\n\n如果同时提供了顶层 `lal` 标签，程序会优先使用这段内容作为 `lal` 的原生配置，不再读取 `lal_config_path` 指向的文件。\n\n## srt_config\n\nSRT 服务配置。\n\n- `enable`：是否启用 SRT\n- `addr`：SRT 监听地址，示例 `:6001`\n\n示例：\n\n```json\n{\n  \"enable\": true,\n  \"addr\": \":6001\"\n}\n```\n\n## rtc_config\n\nRTC 服务配置。目前主要用于 WHIP、WHEP 和 Jessibuca 播放链路。\n\n- `enable`：是否启用 RTC\n- `ice_host_nat_to_ips`：对外暴露的 ICE 地址列表；为空时使用本机可用地址\n- `ice_udp_mux_port`：ICE UDP 复用端口\n- `ice_tcp_mux_port`：ICE TCP 复用端口\n- `write_chan_size`：RTC 订阅侧写队列大小；如果填 `0`，程序会自动使用 `1024`\n\n示例：\n\n```json\n{\n  \"enable\": true,\n  \"ice_host_nat_to_ips\": [\"192.168.0.1\"],\n  \"ice_udp_mux_port\": 4888,\n  \"ice_tcp_mux_port\": 4888,\n  \"write_chan_size\": 1024\n}\n```\n\n## http_config\n\n`lalmax` 自己的 HTTP/HTTPS 配置。管理接口、RTC 信令、HTTP-FMP4、HLS-FMP4/LLHLS 都依赖这里。\n\n- `http_listen_addr`：HTTP 监听地址，示例 `:1290`\n- `enable_https`：是否启用 HTTPS\n- `https_listen_addr`：HTTPS 监听地址\n- `https_cert_file`：HTTPS 证书文件\n- `https_key_file`：HTTPS 私钥文件\n- `ctrl_auth_whitelist`：管理接口鉴权配置\n\n`ctrl_auth_whitelist` 的字段如下：\n\n- `secrets`：允许的令牌列表，请求时通过查询参数 `token` 传入\n- `ips`：允许访问的客户端 IP 列表\n\n当前鉴权覆盖范围：\n\n- `/api/stat/*`\n- `/api/ctrl/*`\n- `/api/hook/*`\n\n规则说明：\n\n- 如果 `secrets` 和 `ips` 都为空，表示不做鉴权\n- 如果两者都配置了，请求必须同时满足两项\n- 鉴权失败时，HTTP 状态码仍然是 `200`，返回体里的 `error_code` 是 `401`\n\n示例：\n\n```json\n{\n  \"http_listen_addr\": \":1290\",\n  \"enable_https\": true,\n  \"https_listen_addr\": \":1233\",\n  \"https_cert_file\": \"./conf/cert.pem\",\n  \"https_key_file\": \"./conf/key.pem\",\n  \"ctrl_auth_whitelist\": {\n    \"ips\": [\"192.168.1.10\"],\n    \"secrets\": [\"EC3D1536-5D93-4BD6-9FBD-96A52CB1596D\"]\n  }\n}\n```\n\n## fmp4_config\n\n`lalmax` 的 FMP4 相关配置，分成 `http` 和 `hls` 两段。\n\n### fmp4_config.http\n\nHTTP-FMP4 配置。\n\n- `enable`：是否启用 HTTP-FMP4\n\n### fmp4_config.hls\n\nHLS-FMP4 / LLHLS 配置。\n\n- `enable`：是否启用 HLS-FMP4 / LLHLS\n- `segment_count`：m3u8 保留的切片数量\n- `segment_duration`：切片时长，单位秒\n- `part_duration`：LLHLS part 时长，单位毫秒\n- `low_latency`：是否启用低延迟 HLS\n\n示例：\n\n```json\n{\n  \"http\": {\n    \"enable\": true\n  },\n  \"hls\": {\n    \"enable\": true,\n    \"segment_count\": 7,\n    \"segment_duration\": 1,\n    \"part_duration\": 200,\n    \"low_latency\": false\n  }\n}\n```\n\n## logic_config\n\n`lalmax` 扩展流分组配置。\n\n- `gop_cache_num`：GOP 缓存数量\n- `single_gop_max_frame_num`：单个 GOP 最多缓存多少帧；`0` 表示自动判断\n\n示例：\n\n```json\n{\n  \"gop_cache_num\": 1,\n  \"single_gop_max_frame_num\": 0\n}\n```\n\n## server_id\n\n服务实例标识。\n\n这个值会出现在：\n\n- Hook 事件的 `server_id`\n- HTTP 回调的 payload\n\n示例：\n\n```json\n\"server_id\": \"1\"\n```\n\n## http_notify\n\n内置 HTTP 回调插件配置。\n\n先说明两件事：\n\n- 这段配置控制的是“是否向外发 HTTP 回调”\n- 不影响内部 HookHub、本地插件注册、`/api/hook/*` 查询和订阅能力\n\n字段如下：\n\n- `enable`：是否启用内置 HTTP 回调插件\n- `update_interval_sec`：周期性生成 `on_update` 事件的间隔秒数\n- `on_server_start`\n- `on_update`\n- `on_group_start`\n- `on_group_stop`\n- `on_stream_active`\n- `on_pub_start`\n- `on_pub_stop`\n- `on_sub_start`\n- `on_sub_stop`\n- `on_relay_pull_start`\n- `on_relay_pull_stop`\n- `on_rtmp_connect`\n- `on_hls_make_ts`\n\n关于 `update_interval_sec`，当前程序的行为是：\n\n- 大于 `0` 时，`lalmax` 会按这个周期向 HookHub 发布 `on_update`\n- 即使 `enable=false`，这些事件依然会进入 HookHub，也能被 `/api/hook/*` 和进程内插件看到\n- 只有在 `enable=true` 且对应回调地址非空时，内置插件才会真正向外发 HTTP 请求\n\n示例：\n\n```json\n{\n  \"enable\": true,\n  \"update_interval_sec\": 5,\n  \"on_update\": \"http://127.0.0.1:10101/on_update\",\n  \"on_group_start\": \"http://127.0.0.1:10101/on_group_start\",\n  \"on_group_stop\": \"http://127.0.0.1:10101/on_group_stop\",\n  \"on_stream_active\": \"http://127.0.0.1:10101/on_stream_active\",\n  \"on_pub_start\": \"http://127.0.0.1:10101/on_pub_start\",\n  \"on_pub_stop\": \"http://127.0.0.1:10101/on_pub_stop\",\n  \"on_sub_start\": \"http://127.0.0.1:10101/on_sub_start\",\n  \"on_sub_stop\": \"http://127.0.0.1:10101/on_sub_stop\",\n  \"on_relay_pull_start\": \"http://127.0.0.1:10101/on_relay_pull_start\",\n  \"on_relay_pull_stop\": \"http://127.0.0.1:10101/on_relay_pull_stop\",\n  \"on_rtmp_connect\": \"http://127.0.0.1:10101/on_rtmp_connect\",\n  \"on_server_start\": \"http://127.0.0.1:10101/on_server_start\",\n  \"on_hls_make_ts\": \"http://127.0.0.1:10101/on_hls_make_ts\"\n}\n```\n\n建议：\n\n- 对外统一只配置 `lalmax.http_notify`\n- `lal` 配置段里的原生 `http_notify` 建议保持关闭\n- 如果两边同时往外发，尤其都带 `on_update`，很容易出现重复回调\n\nHook 事件的具体语义请看 [hook_api.md](./hook_api.md)。\n\n## gb28181_config\n\nGB28181 服务配置。\n\n字段如下：\n\n- `enable`：是否启用 GB28181\n- `listen_addr`：SIP 服务监听 IP，默认会补成 `0.0.0.0`\n- `sip_ip`：SIP 对外地址，生成设备交互内容时会用到\n- `sip_port`：SIP 端口，默认 `5060`\n- `serial`：平台 ID，默认 `34020000002000000001`\n- `realm`：平台域，默认 `3402000000`\n- `username`：认证用户名\n- `password`：认证密码\n- `keepalive_interval`：设备心跳周期，默认 `60`\n- `quick_login`：是否允许设备通过 Keepalive 快速建档\n- `media_config`：媒体端口配置\n\n`media_config` 字段如下：\n\n- `media_ip`：在 SDP 中对外声明的媒体 IP；默认 `0.0.0.0`\n- `listen_port`：固定媒体端口起点；默认 `30000`\n- `multi_port_max_increment`：多端口模式下可分配的附加端口范围；默认 `3000`\n\n示例：\n\n```json\n{\n  \"enable\": true,\n  \"listen_addr\": \"0.0.0.0\",\n  \"sip_ip\": \"100.100.100.101\",\n  \"sip_port\": 5060,\n  \"serial\": \"34020000002000000001\",\n  \"realm\": \"3402000000\",\n  \"username\": \"admin\",\n  \"password\": \"admin123\",\n  \"keepalive_interval\": 60,\n  \"quick_login\": false,\n  \"media_config\": {\n    \"media_ip\": \"100.100.100.101\",\n    \"listen_port\": 30000,\n    \"multi_port_max_increment\": 3000\n  }\n}\n```\n\n## 兼容说明\n\n当前程序还兼容一部分旧配置写法：\n\n- 旧版平铺配置仍然可以读\n- `lal_config_path` 仍然保留兼容\n- 如果没有 `logic_config`，会尝试兼容旧字段 `hook_config`\n- 如果没有 `fmp4_config`，会尝试兼容旧字段 `httpfmp4_config` 和 `hls_config`\n\n但新项目建议统一使用当前这套结构，也就是：\n\n- 顶层使用 `lalmax` 和 `lal`\n- `lalmax` 内部使用当前代码里的 snake_case 字段名\n\n## 相关文档\n\n- [lal_config.md](./lal_config.md)：`lal` 原生配置\n- [api.md](./api.md)：统一管理 API 总览\n- [hook_api.md](./hook_api.md)：Hook 查询与订阅接口\n- [hook_plugin_architecture.md](./hook_plugin_architecture.md)：HookHub 与插件化架构\n"
  },
  {
    "path": "document/gb28181.md",
    "content": "# GB28181\n\nlalmax的gb28181功能为单端口监听（TCP/UDP监听端口可以使用tcp_listen_port和udp_listen_port进行配置）,根据INVITE消息中的ssrc来区分具体流名，详细的配置见gb28181_config\n\n# GB28181相关HTTP API\n\n目前主要提供的API如下\n\n[/api/gb/device_infos](#apigbdevice_infos)\n\n[/api/gb/update_all_notify](#apigbupdate_all_notify)\n\n[/api/gb/update_notify](#apigbupdate_notify)\n\n[/api/gb/start_play](#apigbstart_play)\n\n[/api/gb/ptz_direction](#apigbptz_direction)\n\n[/api/gb/ptz_zoom](#apigbptz_zoom)\n\n[/api/gb/ptz_fi](#apigbptz_fi)\n\n[/api/gb/ptz_preset](#apigbptz_preset)\n\n[/api/gb/ptz_stop](#apigbptz_stop)\n\n\n返回信息格式如下\n```\n{\n    \"code\": <int64>,    // 状态码\n    \"msg\": <string>,    // 状态码对应的解释\n    \"data\": <any>       // 具体返回信息\n}\n\n其中code和msg的对应关系如下\n1000: success\n1001: 请求参数错误\n1002: 服务繁忙\n1003: 设备暂时未注册\n1004: 设备停止播放错误\n```\n\n## /api/gb/device_infos\nAPI含义: 获取注册的设备信息\n\nMethod: GET\n\ndata信息: \n```\n\"data\": {\n    \"device_items\": [\n        {\n            \"device_id\": <string>,              // 设备ID\n            \"channels\": [                       // 通道信息\n                {\n                    \"channel_id\": <string>,     // 通道ID\n                    \"name\": <string>,           // 设备名称\n                    \"manufacturer\": <string>,   // 制造厂商\n                    \"owner: <string>,           // 设备归属\n                    \"civilCode\": <string>,      // 行政区划编码\n                    \"address\": <string>,        // 地址\n                    \"status\": <string>,         // 设备状态，ON/OFF\n                    \"longitude\": <string>,      // 经度\n                    \"latitude\": <string>        // 纬度\n                }\n            ]\n        }\n       \n    ]\n}\n```\n\n示例\n```\ncurl http://127.0.0.1:1290/api/gb/device_infos -X GET\n\n{\n    \"code\":1000,\n    \"msg\":\"success\",\n    \"data\":{\n        \"device_items\":[\n            {\n                \"device_id\":\"34020000001320000001\",\n                \"channels\":[\n                    {\n                        \"channel_id\":\"34020000001320000001\",\n                        \"name\":\"Camera 01\",\n                        \"manufacturer\":\"Hikvision\",\n                        \"owner\":\"Owner\",\n                        \"civilCode\":\"3402000000\",\n                        \"address\":\"Address\",\n                        \"status\":\"ON\",\n                        \"longitude\":\"\",\n                        \"latitude\":\"\"\n                    }\n                ]\n            }\n        ]\n    }\n}\n```\n\n\n## /api/gb/update_all_notify\nAPI含义: 更新全部信息\n\nMethod: POST\n\n请求body信息: 无\n\ndata信息: 无\n\n示例:\n```\ncurl http://127.0.0.1:1290/api/gb/update_all_notify -X POST  \n\n{\n    \"code\":1000,\n    \"msg\":\"success\"\n}\n```\n\n## /api/gb/update_notify\nAPI含义: 更新某个设备信息\n\nMethod: POST\n\n请求body信息\n```\n{\n    \"device_id\": <string>   // 设备ID\n}\n```\n\ndata信息: 无\n\n示例:\n```\ncurl \"http://127.0.0.1:1290/api/gb/update_notify\" -X POST -d '{\"device_id\": \"34020000001320000001\"}' \n\n{\n    \"code\":1000,\n    \"msg\":\"success\"\n}\n```\n\n## /api/gb/start_play\nAPI含义: 播放某通道\n\nMethod: POST\n\n请求body信息:\n```\n{\n    \"device_id\": <string>,      // 设备ID\n    \"channel_id\": <string>,     // 通道ID\n    \"network\": <string>,        // 传输协议类型, tcp/udp\n    \"stream_name\": <string>     // 对应的流名，不指定的话就使用channel_id\n    \"single_port\": <bool>       // 是否单端口\n    \"dump_file_name\": <string>  // dump文件路径\n}\n```\n\ndata信息:\n```\n{\n    \"stream_name\": <string>     // 流名\n}\n```\n\n示例:\n```\ncurl \"http://127.0.0.1:1290/api/gb/start_play\" -X POST -d '{\"device_id\": \"34020000001320000001\", \"channel_id\": \"34020000001320000001\", \"network\": \"udp\", \"stream_name\": \"test001}' \n\n{\n    \"code\":1000,\n    \"msg\":\"success\"\n    \"data\": {\n        \"stream_name\": \"test001\"\n    }\n}\n```\n\n## /api/gb/stop_play\n\nAPI含义: 停止播放某通道\n\nMethod: POST\n\n请求body信息:\n```\n{\n    \"device_id\": <string>,      // 设备ID\n    \"channel_id\": <string>,     // 通道ID\n    \"stream_name\": <string>     // ssrc对应的流名，不指定的话就使用channel_id\n}\n```\n\ndata信息: 无\n\n示例:\n```\ncurl \"http://127.0.0.1:1290/api/gb/stop_play\" -X POST -d '{\"device_id\": \"34020000001320000001\", \"channel_id\": \"34020000001320000001\", \"stream_name\": \"test001}' \n\n{\n    \"code\":1000,\n    \"msg\":\"success\"\n}\n```\n\n## /api/gb/ptz_direction\nAPI含义: ptz 方向控制 \n\nMethod: POST\n\n请求body信息:\n```\n{\n    \"device_id\": <string>,      // 设备ID\n    \"channel_id\": <string>,     // 通道ID\n    \"up\": <bool>,        // 上\n    \"down\": <bool>     // 下\n    \"left\": <bool>       // 左\n    \"right\": <bool>  // 右\n    \"speed\": <int>  // 步长，1~8\n}\n```\n\n## /api/gb/ptz_zoom\nAPI含义: 镜头变倍\n\nMethod: POST\n\n请求body信息:\n```\n{\n    \"device_id\": <string>,      // 设备ID\n    \"channel_id\": <string>,     // 通道ID\n    \"zoom_out\": <bool>,        // 缩小\n    \"zoom_in\": <bool>     // 放大\n    \"speed\": <int>  // 步长，1~8\n}\n```\n## /api/gb/ptz_fi\nAPI含义: 光圈控制和聚焦控制\n\nMethod: POST\n\n请求body信息:\n```\n{\n    \"device_id\": <string>,      // 设备ID\n    \"channel_id\": <string>,     // 通道ID\n    \"iris_in\": <bool>,        // 光圈小\n    \"iris_out\": <bool>       // 光圈大\n    \"focus_near\": <bool>       // 聚焦近\n    \"focus_far\": <bool>      // 聚焦远\n    \"speed\": <int>             // 步长，1~8\n}\n```\n## /api/gb/ptz_preset\nAPI含义: 预置位操作\n\nMethod: POST\n\n请求body信息:\n```\n{\n    \"device_id\": <string>,      // 设备ID\n    \"channel_id\": <string>,     // 通道ID\n    \"cmd\": <int>,        // 0：添加，1：删除，2：调用\n    \"point\": <int>     // 预置点\n}\n```\n## /api/gb/ptz_stop\nAPI含义: 停止ptz\n\nMethod: POST\n\n请求body信息:\n```\n{\n    \"device_id\": <string>,      // 设备ID\n    \"channel_id\": <string>,     // 通道ID\n}\n```\n# 海康设备接入\n\n![图片](images/gb-hk.png)\n"
  },
  {
    "path": "document/hook_api.md",
    "content": "# Hook API\n\n`lalmax` 统一托管 `lal` 的 notify 事件，并补充 `lalmax` 自身扩展订阅状态。\n\n如果需要理解完整分层、调用链、插件职责和设计边界，见 [hook_plugin_architecture.md](./hook_plugin_architecture.md)。\n\n默认建议：\n\n- 对外状态与控制走 `lalmax` 的 `/api/stat/*` 和 `/api/ctrl/*`\n- 对外 hook 事件读取也走 `lalmax`\n- `lal.http_api` 和外部业务直接对接 `lal` 原生 notify 只作为调试手段\n\n## Event Source\n\n`lalmax` 内部将以下事件统一写入 hook hub：\n\n- `on_server_start`\n- `on_update`\n- `on_group_start`\n- `on_group_stop`\n- `on_stream_active`\n- `on_pub_start`\n- `on_pub_stop`\n- `on_sub_start`\n- `on_sub_stop`\n- `on_relay_pull_start`\n- `on_relay_pull_stop`\n- `on_rtmp_connect`\n- `on_hls_make_ts`\n\n其中 `on_update` 的 `groups` 数据已经过 `lalmax` 聚合，包含：\n\n- `lal` 原生 group 状态\n- `lalmax` 扩展订阅者统计\n\n与 `/api/stat/group`、`/api/stat/all_group` 一样，`on_update.groups[*]` 中的 `subs` 也是统一聚合后的订阅列表；如果业务需要显式区分 `lalmax` 扩展订阅者，建议结合 stat API 中的 `lalmax.ext_subs` 使用。\n\n`on_group_start`、`on_stream_active` 和 `on_group_stop` 是 `lalmax` 基于统一输入流生命周期直接生成的事件，payload 结构如下：\n\n```json\n{\n  \"server_id\": \"1\",\n  \"app_name\": \"live\",\n  \"stream_name\": \"test110\"\n}\n```\n\n注意：当前上游 `lal` 的 `WithOnHookSession` 只直接提供 `streamName`，因此这类 group 生命周期事件里的 `app_name` 在部分场景下可能为空，不能把它当成始终可靠存在的字段。\n\n三者的语义区别是：\n\n- `on_group_start`: 流生命周期进入 `lalmax`\n- `on_stream_active`: 收到首个音频或视频 RTMP 消息，只触发一次\n- `on_group_stop`: 流生命周期结束。业务上要判断“没有流了”，应使用这个事件\n\n其中“没有流了”不单独新增新的 hook，仍统一使用 `on_group_stop`。\n\n## HTTP API\n\n### `GET /api/hook/recent`\n\n读取最近 hook 事件快照。\n\n请求参数：\n\n- `limit`: 可选，返回事件数量，默认 `20`\n- `app_name`: 可选，只返回指定 app 的事件\n- `stream_name`: 可选，只返回指定流的事件\n- `session_id`: 可选，只返回指定会话的事件\n- `event`: 可选，只返回单个事件类型\n- `events`: 可选，逗号分隔的多个事件类型\n\n示例：\n\n```bash\ncurl \"http://127.0.0.1:1290/api/hook/recent?limit=5\"\ncurl \"http://127.0.0.1:1290/api/hook/recent?stream_name=test110&events=on_group_start,on_stream_active,on_group_stop,on_update\"\n```\n\n响应示例：\n\n```json\n{\n  \"error_code\": 0,\n  \"desp\": \"succ\",\n  \"data\": {\n    \"events\": [\n      {\n        \"id\": 12,\n        \"event\": \"on_pub_start\",\n        \"timestamp\": \"2026-04-24T15:20:11.123456789+08:00\",\n        \"payload\": {\n          \"server_id\": \"1\",\n          \"session_id\": \"RTMPPUB1\",\n          \"protocol\": \"RTMP\",\n          \"base_type\": \"PUB\",\n          \"stream_name\": \"test110\"\n        }\n      }\n    ]\n  }\n}\n```\n\n### `GET /api/hook/stream`\n\n以 `Server-Sent Events` 持续订阅 hook 事件。\n\n示例：\n\n```bash\ncurl -N http://127.0.0.1:1290/api/hook/stream\ncurl -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\"\n```\n\n返回格式：\n\n```text\nid: 12\nevent: on_pub_start\ndata: {\"server_id\":\"1\",\"session_id\":\"RTMPPUB1\",\"protocol\":\"RTMP\",\"base_type\":\"PUB\",\"stream_name\":\"test110\"}\n```\n\n连接建立后会先回放最近一批事件，再进入实时流。\n\n## In-Process Usage\n\n如果业务代码和 `lalmax` 在同一进程内，可以直接使用：\n\n```go\nhub := serverInstance.HookHub()\n_, ch, cancel := hub.Subscribe(64)\ndefer cancel()\n\nfor event := range ch {\n    // event.Event\n    // event.Payload\n}\n```\n\n## Plugin Usage\n\n如果具体业务希望由插件处理，而不是把逻辑写进 `lalmax` 主流程，可以注册 hook 插件：\n\n```go\ntype BizPlugin struct{}\n\nfunc (p *BizPlugin) Name() string { return \"biz-plugin\" }\n\nfunc (p *BizPlugin) OnHookEvent(event server.HookEvent) error {\n    // 业务处理\n    return nil\n}\n\ncancel, err := serverInstance.RegisterHookPlugin(&BizPlugin{}, server.HookPluginOptions{\n    Filter: server.NewHookEventFilter(\"live\", \"test110\", \"\", []string{\n        server.HookEventPubStart,\n        server.HookEventPubStop,\n    }),\n})\nif err != nil {\n    panic(err)\n}\ndefer cancel()\n```\n\n当前默认的 HTTP notify 转发已经作为内置插件存在，外部业务插件只需要关注自己的处理逻辑。\n\n## Notes\n\n- `/api/hook/*` 使用和 `/api/stat/*`、`/api/ctrl/*` 相同的鉴权中间件\n- 当前 `lal` 的 `WithOnHookSession` 回调只提供 `streamName`，不提供 `appName`\n- 因此扩展订阅者与 `app_name` 的精确归属能力仍受上游 hook 入参限制\n- 建议只使用 `lalmax.http_notify` 作为对外 webhook 配置；如果 `lal` 配置段也单独开启原生 `http_notify`，尤其是 `update_interval_sec`，可能出现重复的 `on_update`\n- `on_group_start` / `on_stream_active` / `on_group_stop` 比基于 `on_update` 快照 diff 的方案更实时，也更不容易漏掉短生命周期流\n- `on_update` 仍然建议保留给状态快照、巡检和最终一致对账使用\n"
  },
  {
    "path": "document/hook_plugin_architecture.md",
    "content": "# Hook Plugin Architecture\n\n本文档详细说明 `lalmax` 当前的 Hook 体系设计，包括：\n\n- 为什么需要由 `lalmax` 统一托管 hook\n- 事件从 `lal` 到业务插件的完整调用链\n- `HookHub`、过滤器、插件调度器各自的职责\n- 默认 HTTP notify 在新架构中的位置\n- 业务插件的推荐接入方式\n- 当前设计边界与后续演进方向\n\n## 1. 设计目标\n\n这套 Hook 架构的目标不是把业务逻辑写进 `lalmax`，而是把 `lalmax` 固定为一个稳定的媒体事件平台层。\n\n核心目标：\n\n- `lal` 继续作为原生流状态事实源\n- `lalmax` 统一聚合原生状态和扩展订阅状态\n- `lalmax` 统一对外暴露 Hook 读取能力\n- 具体业务处理通过插件完成，而不是散落在主流程中\n- 慢业务不能阻塞媒体主链路\n\n一句话概括：\n\n`lalmax` 负责“采集、聚合、过滤、分发”，业务插件负责“消费和处理”。\n\n## 2. 分层结构\n\n当前 Hook 链路分为 4 层。\n\n### 2.1 `lal` 原生事件层\n\n`lal` 通过 `INotifyHandler` 向外抛出原生事件，例如：\n\n- `OnServerStart`\n- `OnUpdate`\n- `OnPubStart`\n- `OnPubStop`\n- `OnSubStart`\n- `OnSubStop`\n- `OnRelayPullStart`\n- `OnRelayPullStop`\n- `OnRtmpConnect`\n- `OnHlsMakeTs`\n\n这一层只负责产生事件，不负责业务分发。\n\n在 `lalmax` 这一层，还会基于统一输入流生命周期派生额外的 group 生命周期事件：\n\n- `on_group_start`\n- `on_stream_active`\n- `on_group_stop`\n\n### 2.2 `lalmax` HookHub 层\n\n`lalmax` 使用 [http_notify.go](./../server/http_notify.go) 中的 `HttpNotify` 作为统一 HookHub。\n\n它当前承担 5 类职责：\n\n1. 接住 `lal` 发出的原生 notify 事件\n2. 对 `on_update` 的 group 数据做聚合增强\n3. 为事件补充过滤所需的元数据\n4. 将事件写入历史缓存，并提供 SSE/Recent 读取\n5. 将事件异步分发给插件\n\n虽然这个结构体名字仍叫 `HttpNotify`，但职责已经不只是“发 HTTP 回调”，而是整个 Hook 总线。\n\n### 2.3 过滤层\n\n过滤逻辑在 [hook_filter.go](./../server/hook_filter.go)。\n\n这层负责统一定义事件匹配规则，当前支持：\n\n- `app_name`\n- `stream_name`\n- `session_id`\n- `event`\n- `events`\n\n这一层的意义是“统一语义”，保证：\n\n- `/api/hook/recent`\n- `/api/hook/stream`\n- 业务插件注册过滤\n\n三者使用同一套过滤规则，而不是每处自己实现一套判断逻辑。\n\n### 2.4 插件层\n\n插件接口在 [hook_plugin.go](./../server/hook_plugin.go)：\n\n```go\ntype HookPlugin interface {\n    Name() string\n    OnHookEvent(event HookEvent) error\n}\n```\n\n插件层只关心一件事：收到匹配事件后做自己的业务处理。\n\n典型插件可以是：\n\n- HTTP webhook 转发\n- Kafka 生产者\n- Redis Stream 写入器\n- 数据库落表\n- 业务内存回调\n- 审计日志插件\n\n## 3. 事件调用链\n\n以 `OnPubStart` 为例，完整调用链如下：\n\n```text\nlal native event\n  -> HttpNotify.NotifyPubStart(info)\n  -> publish(HookEventPubStart, info)\n  -> 填充过滤元数据\n  -> 写入 history\n  -> 推送给 SSE / recent 订阅者\n  -> dispatchPlugins(event)\n  -> 匹配到的插件各自异步消费\n```\n\n以 `OnUpdate` 为例，还会多一步聚合：\n\n```text\nlal native update\n  -> HttpNotify.NotifyUpdate(info)\n  -> 聚合 lal group + lalmax 扩展订阅者\n  -> publish(HookEventUpdate, mergedInfo)\n  -> history / SSE / plugin dispatch\n```\n\n而 `on_group_start` / `on_stream_active` / `on_group_stop` 并不是在 `OnUpdate` 流程内 diff 生成的，而是直接跟随输入流生命周期与首个媒体消息触发：\n\n```text\ngroup/media lifecycle\n  -> WithOnHookSession create\n  -> Group.OnMsg first real media\n  -> Group.OnStop\n  -> publish(HookEventGroupStart / HookEventStreamActive / HookEventGroupStop, info)\n```\n\n这意味着：\n\n- 查询接口拿到的是聚合后的视图\n- hook 事件里的 `on_update` 也是聚合后的视图\n- HTTP notify 与插件消费看到的是同一份增强数据\n\n## 4. 为什么默认 HTTP notify 也做成插件\n\n旧模式下，`NotifyPubStart/NotifyUpdate/...` 会直接在主流程里发 HTTP POST。\n\n这样做的问题是：\n\n- HTTP 转发是业务出口的一种，不应该写死在主流程\n- 后续增加 Kafka、Redis、数据库 sink 时会继续污染主流程\n- 不同业务出口的生命周期与重试策略难以统一管理\n\n现在的做法是：\n\n- 主流程只负责 `publish`\n- 默认 HTTP notify 转发实现为内置插件\n- 内置插件文件在 [hook_builtin_http_plugin.go](./../server/hook_builtin_http_plugin.go)\n\n这样后续无论新增什么业务出口，都和默认 HTTP notify 处于同一层级。\n\n## 5. HookEvent 结构说明\n\n对外公开的事件结构是：\n\n```go\ntype HookEvent struct {\n    ID        int64\n    Event     string\n    Timestamp string\n    Payload   json.RawMessage\n}\n```\n\n其中：\n\n- `ID` 用于事件顺序控制\n- `Event` 是事件类型名，例如 `on_pub_start`\n- `Timestamp` 是事件产生时间\n- `Payload` 是具体事件数据\n\n此外，内部还会维护用于过滤的元数据，例如：\n\n- `sessionID`\n- `streamName`\n- `appName`\n- `groupKeys`\n\n这些字段不直接暴露给外部 API，但会用于：\n\n- 路由层过滤\n- 插件过滤\n- `on_update` 的 group 命中判断\n\n## 6. 过滤语义\n\n过滤规则统一由 `HookEventFilter.Match` 决定。\n\n### 6.1 单会话事件\n\n例如：\n\n- `on_group_start`\n- `on_stream_active`\n- `on_group_stop`\n- `on_pub_start`\n- `on_pub_stop`\n- `on_sub_start`\n- `on_sub_stop`\n- `on_relay_pull_start`\n- `on_relay_pull_stop`\n\n除 group 级事件外，这类事件会直接携带：\n\n- `session_id`\n- `stream_name`\n- `app_name`\n\n因此过滤时按单个流或单个会话精准匹配。\n\n其中 `on_group_start` / `on_stream_active` / `on_group_stop` 是 group 级别事件，没有 `session_id`，只携带：\n\n- `stream_name`\n- `app_name`\n\n其中 `app_name` 当前并不保证始终非空，它仍受上游 `WithOnHookSession` 只提供 `streamName` 的限制。\n\n### 6.2 `on_update`\n\n`on_update` 一次可能携带多个 group。\n\n因此内部会把它展开成一组 `groupKeys`，过滤时判断：\n\n- 是否有任意一个 group 命中过滤条件\n\n也就是说，一个 `on_update` 事件只要包含目标流，就会被保留。\n\n`on_group_start` / `on_stream_active` / `on_group_stop` 是直接跟随输入流生命周期产生的，因此比基于 `on_update` 快照 diff 的方案更实时，也更不容易漏掉短生命周期流。\n\n其中：\n\n- `on_group_start` 表示 group 生命周期开始\n- `on_stream_active` 表示首个音频或视频消息真正到达，只触发一次\n- `on_group_stop` 表示 group 生命周期结束，也是“没有流了”应使用的事件\n\n但当前仍有一个边界：\n\n- 上游 `lal` 的 `WithOnHookSession` 只提供 `streamName`\n- 因此这类 direct lifecycle hook 的 `app_name` 归属能力仍受上游接口限制\n- 如果同时保留 `lal` 原生 `http_notify` 和 `lalmax` 自己的 HookHub 出口，尤其同时配置两个 `update_interval_sec`，`on_update` 可能重复\n\n### 6.3 当前支持的过滤条件\n\n- `app_name`\n- `stream_name`\n- `session_id`\n- `event`\n- `events`\n\n建议：\n\n- 单流订阅优先同时带 `app_name + stream_name`\n- 精确追踪某个连接时使用 `session_id`\n- 降低噪音时优先限制 `event/events`\n\n## 7. 业务插件如何接入\n\n业务代码和 `lalmax` 同进程时，推荐直接注册插件。\n\n### 7.1 最小插件示例\n\n```go\ntype BizPlugin struct{}\n\nfunc (p *BizPlugin) Name() string {\n    return \"biz-plugin\"\n}\n\nfunc (p *BizPlugin) OnHookEvent(event server.HookEvent) error {\n    // 业务处理\n    return nil\n}\n```\n\n### 7.2 注册示例\n\n```go\ncancel, err := serverInstance.RegisterHookPlugin(&BizPlugin{}, server.HookPluginOptions{\n    Filter: server.NewHookEventFilter(\"live\", \"test110\", \"\", []string{\n        server.HookEventPubStart,\n        server.HookEventPubStop,\n        server.HookEventUpdate,\n    }),\n    BufferSize: 64,\n})\nif err != nil {\n    panic(err)\n}\ndefer cancel()\n```\n\n### 7.3 字段说明\n\n- `Name()`\n  用作插件唯一标识。重复名称不允许重复注册。\n\n- `Filter`\n  用于控制这个插件只消费自己关心的事件。\n\n- `BufferSize`\n  用于控制插件异步队列大小。\n\n### 7.4 为什么推荐插件而不是直接改主流程\n\n因为主流程的职责应该稳定，而业务处理天然是变化的。\n\n如果把每个业务都写进主流程，会出现：\n\n- 发布一个新业务就要改核心代码\n- 多业务逻辑互相影响\n- 回归成本越来越高\n- 业务异常更容易污染核心链路\n\n插件化之后，核心层和业务层边界清晰很多。\n\n## 8. 插件调度模型\n\n插件分发是异步的，每个插件有自己的缓冲队列。\n\n调度模型：\n\n```text\npublish(event)\n  -> 遍历已注册插件\n  -> 根据 Filter 判断是否命中\n  -> 命中则投递到该插件自己的 queue\n  -> 插件 goroutine 从 queue 中消费\n```\n\n这个模型的含义是：\n\n- 插件之间互不阻塞\n- 插件不会反压媒体主链路\n- 某个慢插件只影响自己\n\n当前策略下，如果插件队列满了：\n\n- 当前事件会被丢弃\n- 记录 warn 日志\n\n这是有意选择，优先保证媒体主链路稳定。\n\n## 9. 当前默认行为\n\n当前系统启动后，默认会注册一个内置插件：\n\n- `builtin-http-notify`\n\n它负责把事件按旧配置转发到：\n\n- `on_server_start`\n- `on_update`\n- `on_group_start`\n- `on_stream_active`\n- `on_group_stop`\n- `on_pub_start`\n- `on_pub_stop`\n- `on_sub_start`\n- `on_sub_stop`\n- `on_relay_pull_start`\n- `on_relay_pull_stop`\n- `on_rtmp_connect`\n- `on_hls_make_ts`\n\n这意味着旧的 `http_notify` 配置仍然可用，但实现方式已经改成：\n\n```text\nHookHub -> builtin-http-notify plugin -> HTTP callback\n```\n\n而不再是主流程直接发 HTTP。\n\n## 10. API、SSE、插件三者关系\n\n三者读的是同一个 HookHub。\n\n### 10.1 `/api/hook/recent`\n\n适合：\n\n- 排查最近事件\n- 调试过滤表达式\n- 运维观察\n\n### 10.2 `/api/hook/stream`\n\n适合：\n\n- 实时消费\n- 调试前端或外部观察程序\n- 对接轻量事件订阅方\n\n### 10.3 插件\n\n适合：\n\n- 同进程业务接入\n- 需要更复杂处理逻辑\n- 需要将事件转发到第三方系统\n\n三者的事件源一致，过滤语义一致，只是使用方式不同。\n\n## 11. 推荐使用方式\n\n### 11.1 业务和 `lalmax` 同进程\n\n优先用插件：\n\n- 延迟低\n- 无需再走 HTTP\n- 易于封装业务逻辑\n\n### 11.2 业务和 `lalmax` 不同进程\n\n优先用：\n\n- `/api/hook/stream`\n- 或内置 HTTP notify 插件\n\n### 11.3 需要统一平台出口\n\n可以继续在插件层增加：\n\n- Kafka 插件\n- Redis 插件\n- 数据库存档插件\n\n## 12. 当前边界与限制\n\n### 12.1 `app_name` 边界\n\n当前上游 `lal` 的 `WithOnHookSession` 仍只提供 `streamName`，不提供 `appName`。\n\n这意味着：\n\n- `lal` 原生 group 状态本身是可信的\n- 但扩展订阅者与 `app_name` 的精确归属能力仍受上游输入限制\n\n因此文档里一直建议：\n\n- 需要精确路由时，尽量同时使用 `app_name + stream_name`\n\n### 12.2 插件可靠性策略\n\n当前插件队列满时是丢弃策略，不是阻塞策略，也不是持久化重试策略。\n\n这是为了媒体主链路稳定。\n\n如果未来某类插件需要强可靠投递，建议不要直接在 `lalmax` 内核层强推重试，而是：\n\n- 插件内自己做持久化\n- 或者把事件转发给外部消息系统\n\n### 12.3 当前插件装配方式\n\n目前插件仍然通过代码注册。\n\n也就是说：\n\n- 你需要拿到 `LalMaxServer`\n- 调用 `RegisterHookPlugin(...)`\n\n下一步可以继续演进成“配置化装配”，由配置声明启用哪些插件和参数。\n\n## 13. 后续演进建议\n\n比较合理的后续方向有 3 个。\n\n### 13.1 插件配置化装配\n\n目标：\n\n- 不用业务代码手动注册插件\n- 配置文件直接声明插件列表、参数、过滤条件\n\n### 13.2 标准化插件参数\n\n例如统一定义：\n\n- HTTP webhook 插件参数\n- Kafka 插件参数\n- Redis 插件参数\n\n### 13.3 更强的可靠性模型\n\n例如：\n\n- 插件失败重试\n- 死信队列\n- 插件级别熔断\n- 指标与监控\n\n## 14. 小结\n\n现在的 Hook 架构已经完成了从“固定 HTTP 回调实现”到“统一 HookHub + 插件化业务处理”的转换。\n\n当前职责边界可以概括为：\n\n- `lal`: 原生媒体事件事实源\n- `lalmax` HookHub: 聚合、过滤、缓存、分发\n- 插件: 具体业务处理\n\n这套结构的核心价值是：\n\n- 主流程稳定\n- 业务接入灵活\n- 多业务可并存\n- 后续扩展成本更低\n"
  },
  {
    "path": "document/lal_api.md",
    "content": "# lal Native HTTP API\n\n`lalmax` 内嵌运行 `lal`。默认情况下，对外建议统一使用 `lalmax` 自己的 API 门面：\n\n- `lalmax.http_config.http_listen_addr` 下的 `/api/stat/*`\n- `lalmax.http_config.http_listen_addr` 下的 `/api/ctrl/*`\n- `lalmax.http_config.http_listen_addr` 下的 `/api/hook/*`\n\n`lal.http_api` 只建议在调试 `lal` 原生行为时临时开启。\n\n默认配置：\n\n```json\n{\n  \"http_api\": {\n    \"enable\": false,\n    \"addr\": \":8083\"\n  }\n}\n```\n\n启用后可访问：\n\n```text\nhttp://127.0.0.1:8083\n```\n\n## Native Endpoints\n\n`lal` 原生 HTTP API 当前主要包含：\n\n- `GET /lal.html`\n- `GET /api/stat/lal_info`\n- `GET /api/stat/all_group`\n- `GET /api/stat/group`\n- `POST /api/ctrl/start_relay_pull`\n- `GET /api/ctrl/stop_relay_pull`\n- `POST /api/ctrl/kick_session`\n- `POST /api/ctrl/start_rtp_pub`\n\n## Recommended Gateway\n\n推荐直接使用 `lalmax` API，因为它会在 `lal` 原生结果基础上补充：\n\n- `lalmax` 扩展订阅者统计\n- 更完整的统一状态视图\n- 统一的 hook 事件读取能力\n- 统一鉴权入口\n\n默认地址：\n\n```text\nhttp://127.0.0.1:1290/api/stat/group\nhttp://127.0.0.1:1290/api/stat/all_group\nhttp://127.0.0.1:1290/api/stat/lal_info\nhttp://127.0.0.1:1290/api/ctrl/start_relay_pull\nhttp://127.0.0.1:1290/api/ctrl/stop_relay_pull\nhttp://127.0.0.1:1290/api/ctrl/kick_session\nhttp://127.0.0.1:1290/api/ctrl/start_rtp_pub\nhttp://127.0.0.1:1290/api/ctrl/stop_rtp_pub\nhttp://127.0.0.1:1290/api/hook/recent\nhttp://127.0.0.1:1290/api/hook/stream\n```\n\n## Compatibility Notes\n\n- `lalmax` 的 `/api/ctrl/*` 请求/响应结构与 `lal` 原生 API 基本保持一致\n- `lalmax` 的 `/api/stat/group` 和 `/api/stat/all_group` 会在兼容 `lal` 原有字段的基础上新增 `lalmax` 扩展块\n- `stop_relay_pull` 在 `lalmax` 中兼容 `GET`\n- `stat/group` 在 `lalmax` 中会优先结合 `app_name + stream_name` 做更精确的 group 匹配\n- `on_update` 等 hook 事件在 `lalmax` 中已经过聚合增强\n\n## Stat API Extension\n\n`lalmax` 的统计接口会返回两层信息：\n\n1. `lal` 兼容层\n   也就是原有的 `stream_name`、`app_name`、`pub`、`subs`、`pull`、`in_frame_per_sec` 等字段\n2. `lalmax` 扩展层\n   当前主要是 `lalmax.ext_subs`\n\n其中：\n\n- `subs` 是聚合后的统一订阅列表\n- `lalmax.ext_subs` 是其中来自 `lalmax` 扩展协议层的子集\n\n这意味着调用方如果完全按照 `lal` 老接口解析，通常仍然可以工作；如果要区分哪些订阅者是 `lalmax` 自己维护的，就再读取 `lalmax.ext_subs`。\n\n控制类接口不附带这些扩展状态。如果执行控制动作后还需要查看最新流状态，应再调用 `/api/stat/group` 或 `/api/stat/all_group`。\n\n## Debug Usage\n\n只有在以下场景，才建议单独开启 `lal.http_api`：\n\n- 排查 `lal` 原生 HTTP API 行为\n- 对比 `lal` 原始 group 数据和 `lalmax` 聚合数据\n- 调试上游 `lal` 升级后的兼容性\n"
  },
  {
    "path": "document/lal_config.md",
    "content": "# lal 原生配置说明\n\n本文档说明 `conf/lalmax.conf.json` 中 `lal` 配置段的常用字段。`lal` 配置段会直接传给 lal 原生服务，用于 RTMP、RTSP、HTTP-FLV、HLS-TS、HTTP-TS、录制、鉴权和原生 HTTP API。\n\n## rtmp\n\n- `enable`: 是否启用 RTMP 服务。\n- `addr`: RTMP 监听地址，例如 `:1935`。\n- `rtmps_enable`: 是否启用 RTMPS。\n- `rtmps_addr`: RTMPS 监听地址，例如 `:4935`。\n- `rtmps_cert_file`: RTMPS 证书文件路径。\n- `rtmps_key_file`: RTMPS 私钥文件路径。\n- `gop_num`: RTMP 拉流 GOP 缓存数量。\n- `single_gop_max_frame_num`: 单个 GOP 最大缓存帧数，`0` 表示不限制。\n- `merge_write_size`: 合并写大小，`0` 表示关闭合并写。\n\n## in_session\n\n- `add_dummy_audio_enable`: 没有音频时是否补静音音频。\n- `add_dummy_audio_wait_audio_ms`: 等待真实音频的时间，超过后才补静音音频。\n\n## default_http\n\nHTTP 类协议的默认监听配置。HTTP-FLV、HTTP-TS、HLS-TS 未单独配置监听地址时，会使用这里的地址。\n\n- `http_listen_addr`: 默认 HTTP 监听地址，例如 `:8080`。\n- `https_listen_addr`: 默认 HTTPS 监听地址，例如 `:4433`。\n- `https_cert_file`: HTTPS 证书文件路径。\n- `https_key_file`: HTTPS 私钥文件路径。\n\n## httpflv\n\n- `enable`: 是否启用 HTTP-FLV。\n- `enable_https`: 是否启用 HTTPS HTTP-FLV。\n- `url_pattern`: URL 路径匹配前缀。示例配置为 `/`，因此 `/live/test110.flv` 可用。\n- `gop_num`: HTTP-FLV GOP 缓存数量。\n- `single_gop_max_frame_num`: 单个 GOP 最大缓存帧数。\n\n## hls\n\n这里是 lal 原生 HLS-TS 配置，不是 lalmax 的 HLS-FMP4/LLHLS 配置。\n\n- `enable`: 是否启用 HLS-TS。\n- `enable_https`: 是否启用 HTTPS HLS-TS。\n- `url_pattern`: HLS-TS URL 路径前缀，常用 `/hls/`。\n- `out_path`: HLS-TS 文件输出目录。\n- `fragment_duration_ms`: 单个 TS 分片时长。\n- `fragment_num`: m3u8 中保留的分片数量。\n- `delete_threshold`: 清理旧分片的阈值。\n- `cleanup_mode`: 清理模式。\n- `use_memory_as_disk_flag`: 是否使用内存模拟磁盘。\n- `sub_session_timeout_ms`: HLS 拉流会话超时时间。\n- `sub_session_hash_key`: HLS 会话哈希 key。\n\n## httpts\n\n- `enable`: 是否启用 HTTP-TS。\n- `enable_https`: 是否启用 HTTPS HTTP-TS。\n- `url_pattern`: URL 路径匹配前缀。示例配置为 `/`，因此 `/live/test110.ts` 可用。\n- `gop_num`: HTTP-TS GOP 缓存数量。\n- `single_gop_max_frame_num`: 单个 GOP 最大缓存帧数。\n\n## rtsp\n\n- `enable`: 是否启用 RTSP。\n- `addr`: RTSP 监听地址，例如 `:5544`。\n- `rtsps_enable`: 是否启用 RTSPS。\n- `rtsps_addr`: RTSPS 监听地址，例如 `:5322`。\n- `rtsps_cert_file`: RTSPS 证书文件路径。\n- `rtsps_key_file`: RTSPS 私钥文件路径。\n- `out_wait_key_frame_flag`: RTSP 拉流是否等待关键帧后再输出。\n- `auth_enable`: 是否启用 RTSP 鉴权。\n- `auth_method`: 鉴权方式。\n- `username`: RTSP 鉴权用户名。\n- `password`: RTSP 鉴权密码。\n\n## record\n\n- `enable_flv`: 是否启用 FLV 录制。\n- `flv_out_path`: FLV 录制输出目录。\n- `enable_mpegts`: 是否启用 MPEG-TS 录制。\n- `mpegts_out_path`: MPEG-TS 录制输出目录。\n\n## relay_push\n\n- `enable`: 是否启用静态转推。\n- `addr_list`: 转推目标地址列表。\n\n## static_relay_pull\n\n- `enable`: 是否启用静态回源拉流。\n- `addr`: 静态回源地址。\n\n## http_api\n\n- `enable`: 是否启用 lal 原生 HTTP API。\n- `addr`: lal 原生 HTTP API 监听地址，例如 `:8083`。\n\n接口说明见 [lal_api.md](./lal_api.md)。\n\n## simple_auth\n\n简单鉴权配置，鉴权值通常按 `key + streamName` 计算。\n\n- `key`: 鉴权 key。\n- `dangerous_lal_secret`: 管理类接口使用的 secret。\n- `pub_rtmp_enable`: 是否启用 RTMP 推流鉴权。\n- `sub_rtmp_enable`: 是否启用 RTMP 拉流鉴权。\n- `sub_httpflv_enable`: 是否启用 HTTP-FLV 拉流鉴权。\n- `sub_httpts_enable`: 是否启用 HTTP-TS 拉流鉴权。\n- `pub_rtsp_enable`: 是否启用 RTSP 推流鉴权。\n- `sub_rtsp_enable`: 是否启用 RTSP 拉流鉴权。\n- `hls_m3u8_enable`: 是否启用 HLS m3u8 鉴权。\n\n## pprof\n\n- `enable`: 是否启用 pprof。\n- `addr`: pprof 监听地址，例如 `:8084`。\n\n## log\n\n- `level`: 日志级别。\n- `filename`: 日志文件路径。\n- `is_to_stdout`: 是否输出到标准输出。\n- `is_rotate_daily`: 是否按天切分日志。\n- `short_file_flag`: 是否打印短文件名。\n- `timestamp_flag`: 是否打印时间戳。\n- `timestamp_with_ms_flag`: 时间戳是否包含毫秒。\n- `level_flag`: 是否打印日志级别。\n- `assert_behavior`: 断言行为。\n\n## debug\n\n- `log_group_interval_sec`: group 状态日志输出间隔。\n- `log_group_max_group_num`: 单次最多输出的 group 数量。\n- `log_group_max_sub_num_per_group`: 单个 group 最多输出的订阅者数量。\n"
  },
  {
    "path": "document/rtc.md",
    "content": "# WebRTC(WHIP/WHEP)\n\nWebRTC在刚发布的时候仅仅专注于VoIP和点对点用例，它仅限于几个并发的浏览器,并且不能扩展，缺少标准信令交互，故很难用于直播场景。\n\n在此背景下,WHIP和WHEP这2个标准的提出，补齐了信令交互这一个环节，使WebRTC可以运用在直播场景。\n\n## WHIP(WebRTC-HTTP Ingestion Protocol)\n协议链接:https://datatracker.ietf.org/doc/html/draft-murillo-whip-02\n\nWHIP(WebRTC-HTTP Ingestion Protocol)是Milicast的技术团队提出的,在与媒体服务器通信时,WHIP提供了使用标准信令协议的编码软件和硬件，这样就可以实现厂商的WebRTC推流。WHIP在WebRTC上增加了一个简单的信令层，可用于将WebRTC发布者连接到WebRTC媒体服务器，发布者只发送媒体而不接收媒体。\n\n\n### 交互流程\n```\n +-------------+    +---------------+ +--------------+ +---------------+\n | WHIP client |    | WHIP endpoint | | Media Server | | WHIP Resource |\n +--+----------+    +---------+-----+ +------+-------+ +--------|------+\n    |                         |              |                  |\n    |                         |              |                  |\n    |HTTP POST (SDP Offer)    |              |                  |\n    +------------------------>+              |                  |\n    |201 Created (SDP answer) |              |                  |\n    +<------------------------+              |                  |\n    |          ICE REQUEST                   |                  |\n    +--------------------------------------->+                  |\n    |          ICE RESPONSE                  |                  |\n    |<---------------------------------------+                  |\n    |          DTLS SETUP                    |                  |\n    |<======================================>|                  |\n    |          RTP/RTCP FLOW                 |                  |\n    +<-------------------------------------->+                  |\n    | HTTP DELETE                                               |\n    +---------------------------------------------------------->+\n    | 200 OK                                                    |\n    <-----------------------------------------------------------x\n```\n\n（1）WHIP client使用HTTP POST请求执行单次SDP Offer/Answer，以便在编码器/媒体生产者(WHIP客户端)和广播接收端点(媒体服务器)之间建立ICE/DTLS会话。\n\n（2）一旦ICE/DTLS会话建立，媒体将从编码器/媒体生成器(WHIP客户端)单向流向广播接收端点(媒体服务器)。为了降低复杂性，不支持SDP重新协商，因此在完成通过HTTP的初始SDP Offer/Answer后，不能添加或删除任何track或stream。\n\n（3）HTTP POST请求的内容类型为“application/sdp”，并包含作为主体的SDP Offer。WHIP端点将生成一个SDP Answer并返回一个“201 Created”响应，内容类型为“application/SDP”。\n\n## WHEP(WebRTC-HTTP Egress Protocol)\n协议链接:https://datatracker.ietf.org/doc/html/draft-murillo-whep-02\n\nWHEP(WebRTC-HTTP Egress Protocol)也是在WebRTC上增加了一个简单的信令层，可用于将WebRTC播放者连接到WebRTC媒体服务器，播放者只接收媒体，不发送媒体。\n\n```\n +-------------+    +---------------+ +--------------+ +---------------+\n | WHEP Player |    | WHEP endpoint | | Media Server | | WHEP Resource |\n +--+----------+    +---------+-----+ +------+-------+ +--------|------+\n    |                         |              |                  |\n    |                         |              |                  |\n    |HTTP POST (SDP Offer)    |              |                  |\n    +------------------------>+              |                  |\n    |201 Created (SDP answer) |              |                  |\n    +<------------------------+              |                  |\n    |          ICE REQUEST                   |                  |\n    +--------------------------------------->+                  |\n    |          ICE RESPONSE                  |                  |\n    |<---------------------------------------+                  |\n    |          DTLS SETUP                    |                  |\n    |<======================================>|                  |\n    |          RTP/RTCP FLOW                 |                  |\n    +<-------------------------------------->+                  |\n    | HTTP DELETE                                               |\n    +---------------------------------------------------------->+\n    | 200 OK                                                    |\n    <-----------------------------------------------------------x\n```\n\n（1）WHEP Player使用HTTP POST请求执行单次SDP Offer/Answer，以便在WHEP Player和媒体服务器之间建立ICE/DTLS会话。\n\n（2）一旦ICE/DTLS会话建立，媒体将从媒体服务器流向WHEP Player。为了降低复杂性，不支持SDP重新协商，因此在完成通过HTTP的初始SDP Offer/Answer后，不能添加或删除任何track或stream。\n\n（3）HTTP POST请求的内容类型为“application/sdp”，并包含作为主体的SDP Offer。WHEP端点将生成一个SDP Answer并返回一个“201 Created”响应，内容类型为“application/SDP”。\n\n## lalmax RTC\nlalmax支持WHIP推流和WHEP拉流\n\n视频:H264\n\n音频:G711A/G711U\n\nWHIP可以使用[vue-wish](https://github.com/zllovesuki/vue-wish)、[OBS](https://github.com/obsproject/obs-studio/actions/runs/5227109208?pr=7926)测试\n\nWHEP拉流可以使用[vue-wish](https://github.com/zllovesuki/vue-wish)测试\n\n### OBS测试效果\n使用OBS进行whip推流到lalmax中，并用vue-wish拉流，测试延时可以做到200ms以内\n\nOBS推流配置\n\n![图片](images/rtc_01.jpeg)\n\nvue-wish拉流效果\n\n![图片](images/rtc_02.png)\n\n"
  },
  {
    "path": "document/srt.md",
    "content": "# SRT\n\nSRT(Secure Reliable Transport)的简称,主要优化在不可靠网络(非阻塞导致的丢包)环境下实时音视频的传输性能\n\n## 特点\n（1） 基于UDP的用户态协议栈\n\n（2） 抗丢包能力强&低延时\n\n（3） 传输负载无关\n\n（4） 传输加密\n\n## 应用场景\n(1) 上行最后一公里推流加速\n\n(2) CDN内部传输分发加速\n\n(3) 丢包重传率比较高的场景\n\n## 支持的流媒体服务和工具\n(1) OBS\n\n(2) VLC\n\n(3) FFmpeg,编译需集成libsrt\n\n(4) SRS\n\n(5) ZLMediaKit\n\n(6) LALMax\n\n## 测试\n(1) 启动LalMax服务\n\n(2) 使用OBS进行推流，在\"直播\"中输入srt的推流地址\n![图片](images/srt_0.png)\n\n(3) VLC进行播放\n\n在VLC中设置streamid,这部分填streamid后面的所有信息\n![图片](images/srt_1.png)\n\n输入streamid前面的部分进行拉流\n![图片](images/srt_2.png)\n\n![图片](images/srt_3.png)\n"
  },
  {
    "path": "document/stream_url.md",
    "content": "# 流地址说明\n\n本文档使用 `conf/lalmax.conf.json` 的默认配置举例，默认流名为 `test110`。\n\n## 基本规则\n\n- `lal` 原生能力使用 `lal` 配置段中的端口，例如 RTMP、RTSP、HTTP-FLV、HLS-TS、HTTP-TS。\n- `lalmax` 扩展能力使用 `lalmax` 配置段中的端口，例如 SRT、WHIP/WHEP、HTTP-FMP4、HLS-FMP4/LLHLS。\n- 当前 lal 使用简单流管理时主要按 `streamName` 匹配。示例中的 `/live/test110` 里，`test110` 是流名，`live` 可作为常用路径前缀。\n- lalmax 扩展拉流接口支持可选 `app_name` 参数，用于未来多 appName 同 streamName 的精确匹配；不传时仍按历史 `streamName` 兼容查找。\n- 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` 可用。\n- HTTPS、RTMPS、RTSPS 依赖配置中的证书文件，浏览器或播放器可能需要信任测试证书。\n\n## 推流地址\n\n### RTMP\n\n```text\nrtmp://127.0.0.1:1935/live/test110\n```\n\nFFmpeg 示例：\n\n```bash\nffmpeg -re -i demo.flv -c:a copy -c:v copy -f flv rtmp://127.0.0.1:1935/live/test110\n```\n\n如果开启 RTMPS：\n\n```text\nrtmps://127.0.0.1:4935/live/test110\n```\n\n### RTSP\n\n```text\nrtsp://127.0.0.1:5544/live/test110\n```\n\nFFmpeg 示例：\n\n```bash\nffmpeg -re -i demo.flv -c:a copy -c:v copy -f rtsp rtsp://127.0.0.1:5544/live/test110\n```\n\n如果开启 RTSPS：\n\n```text\nrtsps://127.0.0.1:5322/live/test110\n```\n\n### SRT\n\n```text\nsrt://127.0.0.1:6001?streamid=#!::h=test110,m=publish\n```\n\n`h` 表示流名，`m=publish` 表示推流。\n\n### WebRTC WHIP\n\n```text\nhttp://127.0.0.1:1290/webrtc/whip?streamid=test110\nhttps://127.0.0.1:1233/webrtc/whip?streamid=test110\n```\n\nWHIP 使用 HTTP POST 传输 SDP offer，通常由 OBS、WHIP 客户端或 WebRTC 工具调用。\n\n### GB28181\n\nGB28181 不是普通 URL 推流。设备通过 SIP 注册到 lalmax，平台再通过 API 控制播放。详见 [gb28181.md](./gb28181.md)。\n\n## 拉流地址\n\n### RTMP\n\n```text\nrtmp://127.0.0.1:1935/live/test110\n```\n\nffplay 示例：\n\n```bash\nffplay rtmp://127.0.0.1:1935/live/test110\n```\n\n### RTSP\n\n```text\nrtsp://127.0.0.1:5544/live/test110\n```\n\nffplay 示例：\n\n```bash\nffplay rtsp://127.0.0.1:5544/live/test110\n```\n\n如果开启 RTSPS：\n\n```text\nrtsps://127.0.0.1:5322/live/test110\n```\n\n### HTTP-FLV\n\n```text\nhttp://127.0.0.1:8080/live/test110.flv\nhttps://127.0.0.1:4433/live/test110.flv\n```\n\nffplay 示例：\n\n```bash\nffplay http://127.0.0.1:8080/live/test110.flv\n```\n\n### HTTP-TS\n\n需要启用 `lal.httpts.enable`。\n\n```text\nhttp://127.0.0.1:8080/live/test110.ts\nhttps://127.0.0.1:4433/live/test110.ts\n```\n\n### HLS-TS\n\n需要启用 `lal.hls.enable`。\n\n```text\nhttp://127.0.0.1:8080/hls/test110/playlist.m3u8\nhttp://127.0.0.1:8080/hls/test110/record.m3u8\nhttp://127.0.0.1:8080/hls/test110.m3u8\n```\n\n### SRT\n\n```text\nsrt://127.0.0.1:6001?streamid=#!::h=test110,m=request\n```\n\n`h` 表示流名，`m=request` 表示拉流。\n\n### WebRTC WHEP\n\n```text\nhttp://127.0.0.1:1290/webrtc/whep?streamid=test110\nhttps://127.0.0.1:1233/webrtc/whep?streamid=test110\n```\n\n如果需要指定 appName：\n\n```text\nhttp://127.0.0.1:1290/webrtc/whep?streamid=test110&app_name=live\n```\n\nWHEP 使用 HTTP POST 传输 SDP offer，通常由 WHEP 播放器或 WebRTC 工具调用。\n\n### Jessibuca DataChannel\n\n```text\nwebrtc://127.0.0.1:1290/webrtc/play/live/test110\n```\n\n如果需要指定 appName：\n\n```text\nwebrtc://127.0.0.1:1290/webrtc/play/live/test110?app_name=live\n```\n\n### HTTP-FMP4\n\n```text\nhttp://127.0.0.1:1290/live/m4s/test110.mp4\nhttps://127.0.0.1:1233/live/m4s/test110.mp4\n```\n\n如果需要指定 appName：\n\n```text\nhttp://127.0.0.1:1290/live/m4s/test110.mp4?app_name=live\n```\n\n### HLS-FMP4/LLHLS\n\n需要启用 `lalmax.fmp4_config.hls.enable`。\n\n```text\nhttp://127.0.0.1:1290/live/hls/test110/index.m3u8\nhttps://127.0.0.1:1233/live/hls/test110/index.m3u8\n```\n\n如果需要指定 appName：\n\n```text\nhttp://127.0.0.1:1290/live/hls/test110/index.m3u8?app_name=live\n```\n\n如果需要低延迟 HLS，设置 `lalmax.fmp4_config.hls.low_latency` 为 `true`。\n"
  },
  {
    "path": "fmp4/hls/server.go",
    "content": "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\"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\ntype HlsServer struct {\n\tsessions        sync.Map\n\tconf            config.Fmp4HlsConfig\n\tinvalidSessions sync.Map\n}\n\nfunc NewHlsServer(conf config.Fmp4HlsConfig) *HlsServer {\n\tsvr := &HlsServer{\n\t\tconf: conf,\n\t}\n\n\tgo svr.cleanInvalidSession()\n\n\treturn svr\n}\n\nfunc (s *HlsServer) NewHlsSession(streamName string) {\n\ts.NewHlsSessionWithAppName(\"\", streamName)\n}\n\nfunc (s *HlsServer) NewHlsSessionWithAppName(appName, streamName string) {\n\tnazalog.Infof(\"new hls session, appName:%s, streamName:%s\", appName, streamName)\n\tsession := NewHlsSessionWithAppName(appName, streamName, s.conf)\n\ts.sessions.Store(hlsSessionKey(appName, streamName), session)\n}\n\nfunc (s *HlsServer) OnMsg(streamName string, msg base.RtmpMsg) {\n\ts.OnMsgWithAppName(\"\", streamName, msg)\n}\n\nfunc (s *HlsServer) OnMsgWithAppName(appName, streamName string, msg base.RtmpMsg) {\n\tvalue, ok := s.sessions.Load(hlsSessionKey(appName, streamName))\n\tif ok {\n\t\tsession := value.(*HlsSession)\n\t\tsession.OnMsg(msg)\n\t}\n}\n\nfunc (s *HlsServer) OnStop(streamName string) {\n\ts.OnStopWithAppName(\"\", streamName)\n}\n\nfunc (s *HlsServer) OnStopWithAppName(appName, streamName string) {\n\tkey := hlsSessionKey(appName, streamName)\n\tvalue, ok := s.sessions.Load(key)\n\tif ok {\n\t\tsession := value.(*HlsSession)\n\t\ts.invalidSessions.Store(session.SessionId, session)\n\t\ts.sessions.Delete(key)\n\t}\n}\n\nfunc (s *HlsServer) HandleRequest(ctx *gin.Context) {\n\tstreamName := ctx.Param(\"streamid\")\n\tappName := ctx.Query(\"app_name\")\n\tif session, ok := s.getSession(appName, streamName); ok {\n\t\tsession.HandleRequest(ctx)\n\t}\n}\n\nfunc (s *HlsServer) getSession(appName, streamName string) (*HlsSession, bool) {\n\tvalue, ok := s.sessions.Load(hlsSessionKey(appName, streamName))\n\tif ok {\n\t\treturn value.(*HlsSession), true\n\t}\n\n\tif appName != \"\" {\n\t\treturn nil, false\n\t}\n\n\tvar found *HlsSession\n\tmatchCount := 0\n\ts.sessions.Range(func(_, value interface{}) bool {\n\t\tsession := value.(*HlsSession)\n\t\tif session.streamName != streamName {\n\t\t\treturn true\n\t\t}\n\t\tfound = session\n\t\tmatchCount++\n\t\treturn matchCount <= 1\n\t})\n\tif matchCount != 1 {\n\t\treturn nil, false\n\t}\n\treturn found, true\n}\n\ntype sessionKey struct {\n\tappName    string\n\tstreamName string\n}\n\nfunc hlsSessionKey(appName, streamName string) sessionKey {\n\treturn sessionKey{\n\t\tappName:    appName,\n\t\tstreamName: streamName,\n\t}\n}\n\nfunc (s *HlsServer) cleanInvalidSession() {\n\tticker := time.NewTicker(30 * time.Second)\n\tdefer ticker.Stop()\n\tfor range ticker.C {\n\t\ts.invalidSessions.Range(func(k, v interface{}) bool {\n\t\t\tsession := v.(*HlsSession)\n\t\t\tnazalog.Info(\"clean invalid session, streamName:\", session.streamName, \" sessionId:\", k)\n\t\t\tsession.OnStop()\n\t\t\ts.invalidSessions.Delete(k)\n\t\t\treturn true\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "fmp4/hls/session.go",
    "content": "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.com/bluenviron/gohlslib/pkg/codecs\"\n\t\"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/q191201771/lal/pkg/avc\"\n\t\"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/lal/pkg/hevc\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n\tuuid \"github.com/satori/go.uuid\"\n)\n\ntype HlsSession struct {\n\tmuxer               *gohlslib.Muxer\n\tdone                bool\n\tdata                []Frame\n\taudioCodecId        int\n\tvideoCodecId        int\n\tmaxMsgSize          int\n\tappName             string\n\tstreamName          string\n\tsps                 []byte\n\tpps                 []byte\n\tvps                 []byte\n\tasc                 []byte\n\tstartAudioPts       time.Duration\n\tstartVideoPts       time.Duration\n\taudioStartPTSFilled bool\n\tvideoStartPTSFilled bool\n\tSessionId           string\n}\n\nfunc NewHlsSession(streamName string, conf config.Fmp4HlsConfig) *HlsSession {\n\treturn NewHlsSessionWithAppName(\"\", streamName, conf)\n}\n\nfunc NewHlsSessionWithAppName(appName, streamName string, conf config.Fmp4HlsConfig) *HlsSession {\n\tvariant := gohlslib.MuxerVariantFMP4\n\tif conf.LowLatency {\n\t\tvariant = gohlslib.MuxerVariantLowLatency\n\t}\n\n\tu, _ := uuid.NewV4()\n\n\tsession := &HlsSession{\n\t\tmuxer: &gohlslib.Muxer{\n\t\t\tVariant: variant,\n\t\t},\n\t\taudioCodecId: -1,\n\t\tvideoCodecId: -1,\n\t\tmaxMsgSize:   128,\n\t\tdata:         make([]Frame, 10)[0:0],\n\t\tappName:      appName,\n\t\tstreamName:   streamName,\n\t\tSessionId:    u.String(),\n\t}\n\n\tuid, _ := uuid.NewV4()\n\tsession.SessionId = uid.String()\n\n\tif !conf.LowLatency && conf.SegmentCount > 0 {\n\t\t// fmp4模式下可以设置分片个数\n\t\tsession.muxer.SegmentCount = conf.SegmentCount\n\t}\n\n\tif conf.LowLatency && conf.PartDuration > 0 {\n\t\t// llhls设置part duration\n\t\tsession.muxer.PartDuration = time.Millisecond * time.Duration(conf.PartDuration)\n\t}\n\n\tif conf.SegmentDuration > 0 {\n\t\tsession.muxer.SegmentDuration = time.Second * time.Duration(conf.SegmentDuration)\n\t}\n\n\treturn session\n}\n\nfunc (session *HlsSession) OnMsg(msg base.RtmpMsg) {\n\tif session.done {\n\t\tif msg.Header.MsgTypeId == base.RtmpTypeIdVideo {\n\t\t\tif msg.IsVideoKeySeqHeader() {\n\t\t\t\tsession.videoCodecId = int(msg.VideoCodecId())\n\t\t\t\tif msg.IsAvcKeySeqHeader() {\n\t\t\t\t\tvar err error\n\t\t\t\t\tsession.sps, session.pps, err = avc.ParseSpsPpsFromSeqHeader(msg.Payload)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tnazalog.Error(\"ParseSpsPpsFromSeqHeader err:\", err)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tsession.vps, session.sps, session.pps, _ = hevc.ParseVpsSpsPpsFromSeqHeaderWithoutMalloc(msg.Payload)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tnals, err := avc.SplitNaluAvcc(msg.Payload[5:])\n\t\t\t\tif err != nil {\n\t\t\t\t\tnazalog.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tvar nalus [][]byte\n\t\t\t\tif msg.IsAvcKeyNalu() || msg.IsHevcKeyNalu() {\n\t\t\t\t\tif msg.IsAvcKeyNalu() {\n\t\t\t\t\t\tnalus = append(nalus, session.sps)\n\t\t\t\t\t\tnalus = append(nalus, session.pps)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tnalus = append(nalus, session.vps)\n\t\t\t\t\t\tnalus = append(nalus, session.sps)\n\t\t\t\t\t\tnalus = append(nalus, session.pps)\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tnalus = append(nalus, nals...)\n\t\t\t\tpts := time.Millisecond*time.Duration(msg.Pts()) - session.startVideoPts\n\t\t\t\terr = session.muxer.WriteH26x(time.Now(), pts, nalus)\n\t\t\t\tif err != nil {\n\t\t\t\t\tnazalog.Error(\"hls-fmp4 WriteH26x failed, err:\", err)\n\t\t\t\t}\n\t\t\t}\n\n\t\t} else {\n\t\t\tif session.audioCodecId == int(base.RtmpSoundFormatAac) {\n\t\t\t\tpts := time.Millisecond*time.Duration(msg.Dts()) - session.startAudioPts\n\t\t\t\terr := session.muxer.WriteMPEG4Audio(time.Now(), pts, [][]byte{msg.Payload[2:]})\n\t\t\t\tif err != nil {\n\t\t\t\t\tnazalog.Error(\"hls-fmp4 WriteMPEG4Audio failed, err:\", err)\n\t\t\t\t}\n\t\t\t} else if session.audioCodecId == int(base.RtmpSoundFormatOpus) {\n\t\t\t\tpts := time.Millisecond*time.Duration(msg.Dts()) - session.startAudioPts\n\t\t\t\terr := session.muxer.WriteOpus(time.Now(), pts, [][]byte{msg.Payload[1:]})\n\t\t\t\tif err != nil {\n\t\t\t\t\tnazalog.Error(\"hls-fmp4 WriteOpus failed, err:\", err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn\n\t}\n\n\tswitch msg.Header.MsgTypeId {\n\tcase base.RtmpTypeIdAudio:\n\t\tsession.audioCodecId = int(msg.AudioCodecId())\n\t\tif session.audioCodecId == int(base.RtmpSoundFormatAac) {\n\t\t\tif msg.IsAacSeqHeader() {\n\t\t\t\tsession.asc = msg.Payload[2:]\n\t\t\t} else {\n\t\t\t\tif !session.audioStartPTSFilled {\n\t\t\t\t\tsession.startAudioPts = time.Millisecond * time.Duration(msg.Dts())\n\t\t\t\t\tsession.audioStartPTSFilled = true\n\t\t\t\t}\n\n\t\t\t\tpts := time.Millisecond*time.Duration(msg.Dts()) - session.startAudioPts\n\n\t\t\t\tframe := Frame{\n\t\t\t\t\tntp:       time.Now(),\n\t\t\t\t\tpts:       pts,\n\t\t\t\t\tau:        [][]byte{msg.Payload[2:]},\n\t\t\t\t\tcodecType: msg.AudioCodecId(),\n\t\t\t\t}\n\t\t\t\tsession.data = append(session.data, frame)\n\t\t\t}\n\t\t} else if session.audioCodecId == int(base.RtmpSoundFormatOpus) {\n\t\t\tif !session.audioStartPTSFilled {\n\t\t\t\tsession.startAudioPts = time.Millisecond * time.Duration(msg.Dts())\n\t\t\t\tsession.audioStartPTSFilled = true\n\t\t\t}\n\n\t\t\tpts := time.Millisecond*time.Duration(msg.Dts()) - session.startAudioPts\n\t\t\tframe := Frame{\n\t\t\t\tntp:       time.Now(),\n\t\t\t\tpts:       pts,\n\t\t\t\tau:        [][]byte{msg.Payload[1:]},\n\t\t\t\tcodecType: msg.AudioCodecId(),\n\t\t\t}\n\t\t\tsession.data = append(session.data, frame)\n\t\t} else {\n\t\t\treturn\n\t\t}\n\tcase base.RtmpTypeIdVideo:\n\t\tif msg.IsVideoKeySeqHeader() {\n\t\t\tsession.videoCodecId = int(msg.VideoCodecId())\n\t\t\tif msg.IsAvcKeySeqHeader() {\n\t\t\t\tvar err error\n\t\t\t\tsession.sps, session.pps, err = avc.ParseSpsPpsFromSeqHeader(msg.Payload)\n\t\t\t\tif err != nil {\n\t\t\t\t\tnazalog.Error(\"ParseSpsPpsFromSeqHeader err:\", err)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tsession.vps, session.sps, session.pps, _ = hevc.ParseVpsSpsPpsFromSeqHeaderWithoutMalloc(msg.Payload)\n\t\t\t}\n\t\t} else {\n\t\t\tnals, err := avc.SplitNaluAvcc(msg.Payload[5:])\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !session.videoStartPTSFilled {\n\t\t\t\tsession.startVideoPts = time.Millisecond * time.Duration(msg.Pts())\n\t\t\t\tsession.videoStartPTSFilled = true\n\t\t\t}\n\n\t\t\tvar nalus [][]byte\n\t\t\tif msg.IsAvcKeyNalu() || msg.IsHevcKeyNalu() {\n\t\t\t\tif msg.IsAvcKeyNalu() {\n\t\t\t\t\tnalus = append(nalus, session.sps)\n\t\t\t\t\tnalus = append(nalus, session.pps)\n\t\t\t\t} else {\n\t\t\t\t\tnalus = append(nalus, session.vps)\n\t\t\t\t\tnalus = append(nalus, session.sps)\n\t\t\t\t\tnalus = append(nalus, session.pps)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tnalus = append(nalus, nals...)\n\n\t\t\tpts := time.Millisecond*time.Duration(msg.Pts()) - session.startVideoPts\n\n\t\t\tframe := Frame{\n\t\t\t\tntp:       time.Now(),\n\t\t\t\tpts:       pts,\n\t\t\t\tau:        nalus,\n\t\t\t\tcodecType: msg.VideoCodecId(),\n\t\t\t}\n\n\t\t\tsession.data = append(session.data, frame)\n\t\t}\n\t}\n\n\tif session.videoCodecId != -1 && session.audioCodecId != -1 {\n\t\tsession.drain()\n\t\treturn\n\t}\n\n\tif len(session.data) >= session.maxMsgSize {\n\t\tsession.drain()\n\t\treturn\n\t}\n}\n\nfunc (session *HlsSession) drain() {\n\tif session.videoCodecId != -1 {\n\t\tif session.videoCodecId == int(base.RtmpCodecIdAvc) {\n\t\t\tsession.muxer.VideoTrack = &gohlslib.Track{\n\t\t\t\tCodec: &codecs.H264{\n\t\t\t\t\tSPS: session.sps,\n\t\t\t\t\tPPS: session.pps,\n\t\t\t\t},\n\t\t\t}\n\t\t} else if session.videoCodecId == int(base.RtmpCodecIdHevc) {\n\t\t\tsession.muxer.VideoTrack = &gohlslib.Track{\n\t\t\t\tCodec: &codecs.H265{\n\t\t\t\t\tVPS: session.vps,\n\t\t\t\t\tSPS: session.sps,\n\t\t\t\t\tPPS: session.pps,\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t}\n\n\tif session.audioCodecId != -1 {\n\t\tif session.audioCodecId == int(base.RtmpSoundFormatAac) {\n\t\t\tvar mpegConf mpeg4audio.Config\n\t\t\terr := mpegConf.Unmarshal(session.asc)\n\t\t\tif err != nil {\n\t\t\t\tnazalog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tsession.muxer.AudioTrack = &gohlslib.Track{\n\t\t\t\tCodec: &codecs.MPEG4Audio{\n\t\t\t\t\tConfig: mpegConf,\n\t\t\t\t},\n\t\t\t}\n\t\t} else if session.audioCodecId == int(base.RtmpSoundFormatOpus) {\n\t\t\tsession.muxer.AudioTrack = &gohlslib.Track{\n\t\t\t\tCodec: &codecs.Opus{\n\t\t\t\t\tChannelCount: 1,\n\t\t\t\t},\n\t\t\t}\n\t\t}\n\t}\n\n\tif err := session.muxer.Start(); err != nil {\n\t\tnazalog.Error(err)\n\t\treturn\n\t}\n\n\tfor _, data := range session.data {\n\t\tif (data.codecType == base.RtmpCodecIdAvc || data.codecType == base.RtmpCodecIdHevc) && session.videoCodecId != -1 {\n\t\t\terr := session.muxer.WriteH26x(data.ntp, data.pts, data.au)\n\t\t\tif err != nil {\n\t\t\t\tnazalog.Error(\"hls-fmp4 WriteH26x failed, err:\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t} else if session.audioCodecId != -1 {\n\t\t\tif data.codecType == base.RtmpSoundFormatAac {\n\t\t\t\terr := session.muxer.WriteMPEG4Audio(data.ntp, data.pts, data.au)\n\t\t\t\tif err != nil {\n\t\t\t\t\tnazalog.Error(\"hls-fmp4 WriteMPEG4Audio failed, err:\", err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t} else if data.codecType == base.RtmpSoundFormatOpus {\n\t\t\t\terr := session.muxer.WriteOpus(data.ntp, data.pts, data.au)\n\t\t\t\tif err != nil {\n\t\t\t\t\tnazalog.Error(\"hls-fmp4 WriteMPEG4Audio failed, err:\", err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// gohlslib不支持g711\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t}\n\n\tsession.done = true\n}\n\nfunc (session *HlsSession) OnStop() {\n\tif session.done {\n\t\tsession.muxer.Close()\n\t}\n}\n\nfunc (session *HlsSession) HandleRequest(ctx *gin.Context) {\n\tnazalog.Info(\"handle hls request, appName:\", session.appName, \" streamName:\", session.streamName, \" path:\", ctx.Request.URL.Path)\n\tsession.muxer.Handle(ctx.Writer, ctx.Request)\n}\n\ntype Frame struct {\n\tntp       time.Time\n\tpts       time.Duration\n\tau        [][]byte\n\tcodecType uint8\n}\n"
  },
  {
    "path": "fmp4/http-fmp4/server.go",
    "content": "package httpfmp4\n\nimport (\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype HttpFmp4Server struct {\n}\n\nfunc NewHttpFmp4Server() *HttpFmp4Server {\n\tsvr := &HttpFmp4Server{}\n\n\treturn svr\n}\n\nfunc (s *HttpFmp4Server) HandleRequest(c *gin.Context) {\n\tstreamid := c.Param(\"streamid\")\n\tappName := c.Query(\"app_name\")\n\n\tsession := NewHttpFmp4Session(appName, streamid)\n\tsession.handleSession(c)\n}\n"
  },
  {
    "path": "fmp4/http-fmp4/session.go",
    "content": "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\tmaxlogic \"github.com/q191201771/lalmax/logic\"\n\n\t\"github.com/gofrs/uuid\"\n\t\"github.com/q191201771/naza/pkg/connection\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\nvar ErrWriteChanFull = errors.New(\"Fmp4  Session write channel full\")\n\nvar (\n\treadBufSize = 4096 //  session connection读缓冲的大小\n\twChanSize   = 256  //  session 发送数据时，channel 的大小\n)\n\ntype HttpFmp4Session struct {\n\tappName      string\n\tstreamid     string\n\tgroup        *maxlogic.Group\n\tsubscriberId string\n\n\trtmp2Fmp4Remuxer *muxer.Rtmp2Fmp4Remuxer\n\tw                gin.ResponseWriter\n\tconn             connection.Connection\n\tdisposeOnce      sync.Once\n\tlog              nazalog.Logger\n}\n\nfunc NewHttpFmp4Session(appName, streamid string) *HttpFmp4Session {\n\n\tstreamid = strings.TrimSuffix(streamid, \".mp4\")\n\tu, _ := uuid.NewV4()\n\n\tsession := &HttpFmp4Session{\n\t\tappName:      appName,\n\t\tstreamid:     streamid,\n\t\tsubscriberId: u.String(),\n\t\tlog:          nazalog.WithPrefix(u.String()),\n\t}\n\n\tsession.rtmp2Fmp4Remuxer = muxer.NewRtmp2Fmp4Remuxer(session).WithLog(session.log)\n\n\tsession.log.Infof(\"create http fmp4 session, appName:%s, streamid:%s\", appName, streamid)\n\n\treturn session\n}\nfunc (session *HttpFmp4Session) OnInitFmp4(init []byte) {\n\tsession.conn.Write(init)\n}\n\nfunc (session *HttpFmp4Session) OnFmp4Packets(currentPart *muxer.MuxerPart, lastSampleDuration time.Duration, end bool, isVideo bool) {\n\tif currentPart != nil {\n\t\tif err := currentPart.Encode(lastSampleDuration, end); err == nil {\n\t\t\tsession.conn.Write(currentPart.Bytes())\n\t\t}\n\t}\n}\n\nfunc (session *HttpFmp4Session) Dispose() error {\n\treturn session.dispose()\n}\nfunc (session *HttpFmp4Session) dispose() error {\n\tvar retErr error\n\tsession.disposeOnce.Do(func() {\n\t\tsession.OnStop()\n\t\tif session.conn == nil {\n\t\t\tretErr = base.ErrSessionNotStarted\n\t\t\treturn\n\t\t}\n\t\tretErr = session.conn.Close()\n\t})\n\treturn retErr\n}\nfunc (session *HttpFmp4Session) handleSession(c *gin.Context) {\n\tok, group := maxlogic.GetGroupManagerInstance().GetGroup(maxlogic.NewStreamKey(session.appName, session.streamid))\n\tif !ok {\n\t\tnazalog.Errorf(\"stream is not found, appName:%s, streamid:%s\", session.appName, session.streamid)\n\t\tc.Status(http.StatusNotFound)\n\t\treturn\n\t}\n\n\tsession.group = group\n\tsession.w = c.Writer\n\n\tc.Header(\"Content-Type\", \"video/mp4\")\n\tc.Header(\"Connection\", \"close\")\n\tc.Header(\"Expires\", \"-1\")\n\th, ok := session.w.(http.Hijacker)\n\tif !ok {\n\t\tnazalog.Error(\"gin response does not implement http.Hijacker\")\n\t\treturn\n\t}\n\n\tconn, bio, err := h.Hijack()\n\tif err != nil {\n\t\tnazalog.Errorf(\"hijack failed. err=%+v\", err)\n\t\treturn\n\t}\n\tif bio.Reader.Buffered() != 0 || bio.Writer.Buffered() != 0 {\n\t\tnazalog.Errorf(\"hijack but buffer not empty. rb=%d, wb=%d\", bio.Reader.Buffered(), bio.Writer.Buffered())\n\t}\n\tsession.conn = connection.New(conn, func(option *connection.Option) {\n\t\toption.ReadBufSize = readBufSize\n\t\toption.WriteChanSize = wChanSize\n\t})\n\tif err = session.writeHttpHeader(session.w.Header()); err != nil {\n\t\tnazalog.Errorf(\"session writeHttpHeader. err=%+v\", err)\n\t\treturn\n\t}\n\tsession.group.AddSubscriber(maxlogic.SubscriberInfo{\n\t\tSubscriberID: session.subscriberId,\n\t\tProtocol:     maxlogic.SubscriberProtocolHTTPFMP4,\n\t}, session)\n\n\tgo func() {\n\t\treadBuf := make([]byte, 1024)\n\t\t_, err = session.conn.Read(readBuf)\n\t\tsession.dispose()\n\t}()\n\n}\n\nfunc (session *HttpFmp4Session) writeHttpHeader(header http.Header) error {\n\tp := make([]byte, 0, 1024)\n\tp = append(p, []byte(\"HTTP/1.1 200 OK\\r\\n\")...)\n\tfor k, vs := range header {\n\t\tfor _, v := range vs {\n\t\t\tp = append(p, k...)\n\t\t\tp = append(p, \": \"...)\n\t\t\tfor i := 0; i < len(v); i++ {\n\t\t\t\tb := v[i]\n\t\t\t\tif b <= 31 {\n\t\t\t\t\t// prevent response splitting.\n\t\t\t\t\tb = ' '\n\t\t\t\t}\n\t\t\t\tp = append(p, b)\n\t\t\t}\n\t\t\tp = append(p, \"\\r\\n\"...)\n\t\t}\n\t}\n\tp = append(p, \"\\r\\n\"...)\n\n\treturn session.write(p)\n}\nfunc (session *HttpFmp4Session) write(buf []byte) (err error) {\n\tif session.conn != nil {\n\t\t_, err = session.conn.Write(buf)\n\t}\n\treturn err\n}\nfunc (session *HttpFmp4Session) OnMsg(msg base.RtmpMsg) {\n\tif session.rtmp2Fmp4Remuxer != nil {\n\t\tsession.rtmp2Fmp4Remuxer.FeedRtmpMessage(msg)\n\t}\n}\n\nfunc (session *HttpFmp4Session) OnStop() {\n\tif session.group != nil {\n\t\tsession.group.RemoveSubscriber(session.subscriberId)\n\t}\n}\n\nfunc (session *HttpFmp4Session) GetSubscriberStat() maxlogic.SubscriberStat {\n\tif session == nil || session.conn == nil {\n\t\treturn maxlogic.SubscriberStat{}\n\t}\n\n\tconnStat := session.conn.GetStat()\n\tstat := maxlogic.SubscriberStat{\n\t\tReadBytesSum:  connStat.ReadBytesSum,\n\t\tWroteBytesSum: connStat.WroteBytesSum,\n\t}\n\tif remoteAddr := session.conn.RemoteAddr(); remoteAddr != nil {\n\t\tstat.RemoteAddr = remoteAddr.String()\n\t}\n\treturn stat\n}\n"
  },
  {
    "path": "fmp4/muxer/codec.go",
    "content": "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(other Codec) bool\n\tString() string\n}\n\ntype CodecH264 struct {\n\tSPS []byte\n\tPPS []byte\n}\n\nfunc (c *CodecH264) IsVideo() bool {\n\treturn true\n}\n\nfunc (c *CodecH264) Equal(other Codec) bool {\n\tif other2, ok := other.(*CodecH264); ok {\n\t\treturn bytes.Equal(c.SPS, other2.SPS) && bytes.Equal(c.PPS, other2.PPS)\n\t}\n\n\treturn false\n}\n\nfunc (c *CodecH264) String() string {\n\treturn \"H264\"\n}\n\ntype CodecH265 struct {\n\tSPS []byte\n\tPPS []byte\n\tVPS []byte\n}\n\nfunc (c *CodecH265) IsVideo() bool {\n\treturn true\n}\n\nfunc (c *CodecH265) Equal(other Codec) bool {\n\tif other2, ok := other.(*CodecH265); ok {\n\t\treturn bytes.Equal(c.SPS, other2.SPS) && bytes.Equal(c.PPS, other2.PPS) && bytes.Equal(c.VPS, other2.VPS)\n\t}\n\n\treturn false\n}\n\nfunc (c *CodecH265) String() string {\n\treturn \"H265\"\n}\n\ntype CodecAAC struct {\n\tCtx     *aac.AscContext\n\tAscData []byte\n}\n\nfunc (c *CodecAAC) IsVideo() bool {\n\treturn false\n}\n\nfunc (c *CodecAAC) Equal(other Codec) bool {\n\tif other2, ok := other.(*CodecAAC); ok {\n\t\treturn bytes.Equal(c.AscData, other2.AscData)\n\t}\n\n\treturn false\n}\n\nfunc (c *CodecAAC) String() string {\n\treturn \"AAC\"\n}\n\ntype CodecOpus struct {\n\tChannelCount int\n}\n\nfunc (c *CodecOpus) IsVideo() bool {\n\treturn false\n}\n\nfunc (c *CodecOpus) Equal(other Codec) bool {\n\treturn false\n}\n\nfunc (c *CodecOpus) String() string {\n\treturn \"OPUS\"\n}\n"
  },
  {
    "path": "fmp4/muxer/file_writer.go",
    "content": "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 struct {\n\tcurfw                            *FileWriter\n\tinitMp4                          []byte\n\trecordInterval                   int\n\tenableRecordByInterval           bool\n\tstreamName                       string\n\trecordPath                       string\n\tcurDuration                      float64\n\thasWriteInit                     bool\n\tcurrentPart                      *MuxerPart\n\tcurrentFileNextPartId            uint64\n\tcurrentFileLastPartVideoStartDts time.Duration\n\tcurrentFIleLastPartAudioStartDts time.Duration\n}\n\nfunc NewFmp4Record(recordInterval int, enableRecordByInterval bool, streamName, recordPath string) *Fmp4Record {\n\tr := &Fmp4Record{\n\t\trecordInterval:         recordInterval,\n\t\tenableRecordByInterval: enableRecordByInterval,\n\t\tstreamName:             streamName,\n\t\trecordPath:             recordPath,\n\t}\n\n\tif !r.enableRecordByInterval {\n\t\terr := r.createFile()\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn r\n}\n\nfunc (r *Fmp4Record) createFile() (err error) {\n\tr.curfw = &FileWriter{}\n\tfilename := fmt.Sprintf(\"%s-%d.mp4\", r.streamName, time.Now().Unix())\n\tfilenameWithPath := filepath.Join(r.recordPath, filename)\n\tif err := r.curfw.Create(filenameWithPath); err != nil {\n\t\tLog.Errorf(\"[%s] record fmp4 open file failed. filename=%s, err=%+v\", filenameWithPath, err)\n\t\tr.curfw = nil\n\t\treturn err\n\t}\n\n\treturn\n}\n\nfunc (r *Fmp4Record) WriteInitFmp4(init []byte) {\n\tr.initMp4 = init\n}\n\nfunc (r *Fmp4Record) WriteFmp4Segment(part *MuxerPart, lastSampleDuration time.Duration, end bool) (err error) {\n\tif r.enableRecordByInterval {\n\t\tr.WriteMultiFile(part, lastSampleDuration, end)\n\t} else {\n\t\tr.writeSingleFile(part, lastSampleDuration, end)\n\t}\n\treturn\n}\n\nfunc (r *Fmp4Record) WriteMultiFile(part *MuxerPart, lastSampleDuration time.Duration, end bool) {\n\tif part == nil {\n\t\treturn\n\t}\n\n\tif r.curfw == nil {\n\t\terr := r.createFile()\n\t\tif err != nil {\n\t\t\tLog.Error(\"create file failed, err:\", err)\n\t\t\treturn\n\t\t}\n\n\t\tr.curDuration = 0\n\t\tr.currentFileLastPartVideoStartDts = 0\n\t\tr.currentFIleLastPartAudioStartDts = 0\n\t\tr.currentFileNextPartId = 0\n\t\tr.curfw.Write(r.initMp4)\n\t}\n\n\tif r.currentFileLastPartVideoStartDts == 0 {\n\t\tr.currentFileLastPartVideoStartDts = part.StartVideoDts()\n\t}\n\tbaseDecodeVideoTime := part.StartVideoDts() - r.currentFileLastPartVideoStartDts\n\n\tif r.currentFIleLastPartAudioStartDts == 0 {\n\t\tr.currentFIleLastPartAudioStartDts = part.StartAudioDts()\n\t}\n\tbaseDecodeAudioTime := part.StartAudioDts() - r.currentFIleLastPartAudioStartDts\n\n\tif end {\n\t\tpart.SetVideoStartDts(baseDecodeVideoTime)\n\t\tpart.SetAudioStartDts(baseDecodeAudioTime)\n\t\tpart.SetPartId(r.currentFileNextPartId)\n\t\tpart.Encode(lastSampleDuration, end)\n\t\tr.curfw.Write(part.Bytes())\n\n\t\t// 结束写文件\n\t\tr.curfw.Dispose()\n\t\tr.curfw = nil\n\t} else {\n\t\tcurpartduration := part.CalcDuration(lastSampleDuration, end)\n\t\tpart.SetVideoStartDts(baseDecodeVideoTime)\n\t\tpart.SetAudioStartDts(baseDecodeAudioTime)\n\t\tpart.SetPartId(r.currentFileNextPartId)\n\t\tpart.Encode(lastSampleDuration, end)\n\t\tr.curfw.Write(part.Bytes())\n\n\t\tr.currentFileNextPartId++\n\n\t\tr.curDuration += curpartduration.Seconds()\n\t\tif r.curDuration >= float64(r.recordInterval) {\n\n\t\t\t// 结束写文件\n\t\t\tr.curfw.Dispose()\n\t\t\tr.curfw = nil\n\t\t}\n\t}\n}\n\nfunc (r *Fmp4Record) writeSingleFile(part *MuxerPart, lastSampleDuration time.Duration, end bool) {\n\tif r.hasWriteInit {\n\t\terr := part.Encode(lastSampleDuration, false)\n\t\tif err != nil {\n\t\t\tLog.Errorf(\"encode muxer part failed: %v\", err)\n\t\t\treturn\n\t\t}\n\t\tr.curfw.Write(part.Bytes())\n\t} else {\n\t\tr.curfw.Write(r.initMp4)\n\t\terr := part.Encode(lastSampleDuration, false)\n\t\tif err != nil {\n\t\t\tLog.Errorf(\"encode muxer part failed: %v\", err)\n\t\t\treturn\n\t\t}\n\t\tr.curfw.Write(part.Bytes())\n\t\tr.hasWriteInit = true\n\t}\n}\n\nfunc (r *Fmp4Record) Dispose() error {\n\tif r.curfw != nil {\n\t\treturn r.curfw.Dispose()\n\t}\n\n\treturn nil\n}\n\ntype FileWriter struct {\n\tfp *os.File\n}\n\nfunc (fw *FileWriter) Create(filename string) (err error) {\n\tfw.fp, err = os.Create(filename)\n\treturn\n}\n\nfunc (fw *FileWriter) Write(b []byte) (err error) {\n\tif fw.fp == nil {\n\t\treturn base.ErrFileNotExist\n\t}\n\t_, err = fw.fp.Write(b)\n\treturn\n}\n\nfunc (fw *FileWriter) Dispose() error {\n\tif fw.fp == nil {\n\t\treturn base.ErrFileNotExist\n\t}\n\treturn fw.fp.Close()\n}\n\nfunc (fw *FileWriter) Name() string {\n\tif fw.fp == nil {\n\t\treturn \"\"\n\t}\n\treturn fw.fp.Name()\n}\n"
  },
  {
    "path": "fmp4/muxer/flac_box.go",
    "content": "package muxer\n\nimport (\n\t\"github.com/abema/go-mp4\"\n)\n\nfunc BoxTypeFlac() mp4.BoxType { return mp4.StrToBoxType(\"fLaC\") }\n\nfunc init() {\n\tmp4.AddAnyTypeBoxDef(&mp4.AudioSampleEntry{}, BoxTypeFlac())\n}\n\n/*\nfunc BoxTypeFlac() mp4.BoxType {\n\treturn mp4.StrToBoxType(\"fLaC\")\n}\n\nfunc init() {\n\tmp4.AddBoxDef(&FlacBox{})\n}\n\ntype FlacBox struct {\n\tmp4.FullBox `mp4:\"0,extend\"`\n}\n\nfunc (f *FlacBox) GetType() mp4.BoxType {\n\treturn BoxTypeFlac()\n}\n*/\n\nfunc BoxTypeDfla() mp4.BoxType {\n\treturn mp4.StrToBoxType(\"dfLa\")\n}\n\nfunc init() {\n\tmp4.AddBoxDef(&DflaBox{})\n}\n\ntype DflaBox struct {\n\tmp4.BaseCustomFieldObject\n\tData []byte `mp4:\"0,size=8,dynamic\"`\n}\n\nfunc (d *DflaBox) GetType() mp4.BoxType {\n\treturn BoxTypeDfla()\n}\n\n/*\nfunc (d *DflaBox) GetFieldLength(name string, ctx mp4.Context) uint {\n\tswitch name {\n\tcase \"NALUnit\":\n\t\treturn uint(d.Length)\n\t}\n\treturn 0\n}\n*/\n\n// AddFlag adds the flag\nfunc (d *DflaBox) AddFlag(uint32) {}\n\nfunc (d *DflaBox) CheckFlag(uint32) bool {\n\treturn false\n}\n\nfunc (d *DflaBox) GetFlags() uint32 {\n\treturn 0\n}\n\nfunc (d *DflaBox) GetVersion() uint8 {\n\treturn 0\n}\n\nfunc (d *DflaBox) RemoveFlag(uint32) {\n}\n\nfunc (d *DflaBox) SetFlags(uint32) {\n}\n\nfunc (d *DflaBox) SetVersion(uint8) {\n}\n"
  },
  {
    "path": "fmp4/muxer/init.go",
    "content": "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/q191201771/lal/pkg/hevc\"\n)\n\n// Specification: ISO 14496-1, Table 5\nconst (\n\tobjectTypeIndicationVisualISO14496part2    = 0x20\n\tobjectTypeIndicationAudioISO14496part3     = 0x40\n\tobjectTypeIndicationVisualISO1318part2Main = 0x61\n\tobjectTypeIndicationAudioISO11172part3     = 0x6B\n\tobjectTypeIndicationVisualISO10918part1    = 0x6C\n)\n\n// Specification: ISO 14496-1, Table 6\nconst (\n\tstreamTypeVisualStream = 0x04\n\tstreamTypeAudioStream  = 0x05\n)\n\nfunc h265FindParams(params []mp4.HEVCNaluArray) ([]byte, []byte, []byte, error) {\n\tvar vps []byte\n\tvar sps []byte\n\tvar pps []byte\n\n\tfor _, arr := range params {\n\t\tswitch hevc.ParseNaluType(arr.NaluType) {\n\t\tcase hevc.NaluTypeVps, hevc.NaluTypeSps, hevc.NaluTypePps:\n\t\t\tif arr.NumNalus != 1 {\n\t\t\t\treturn nil, nil, nil, fmt.Errorf(\"multiple VPS/SPS/PPS are not supported\")\n\t\t\t}\n\t\t}\n\n\t\tswitch hevc.ParseNaluType(arr.NaluType) {\n\t\tcase hevc.NaluTypeVps:\n\t\t\tvps = arr.Nalus[0].NALUnit\n\t\tcase hevc.NaluTypeSps:\n\t\t\tsps = arr.Nalus[0].NALUnit\n\t\tcase hevc.NaluTypePps:\n\t\t\tpps = arr.Nalus[0].NALUnit\n\t\t}\n\t}\n\n\tif vps == nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"VPS not provided\")\n\t}\n\n\tif sps == nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"SPS not provided\")\n\t}\n\n\tif pps == nil {\n\t\treturn nil, nil, nil, fmt.Errorf(\"PPS not provided\")\n\t}\n\n\treturn vps, sps, pps, nil\n}\n\nfunc h264FindParams(avcc *mp4.AVCDecoderConfiguration) ([]byte, []byte, error) {\n\tif len(avcc.SequenceParameterSets) > 1 {\n\t\treturn nil, nil, fmt.Errorf(\"multiple SPS are not supported\")\n\t}\n\n\tvar sps []byte\n\tif len(avcc.SequenceParameterSets) == 1 {\n\t\tsps = avcc.SequenceParameterSets[0].NALUnit\n\t}\n\n\tif len(avcc.PictureParameterSets) > 1 {\n\t\treturn nil, nil, fmt.Errorf(\"multiple PPS are not supported\")\n\t}\n\n\tvar pps []byte\n\tif len(avcc.PictureParameterSets) == 1 {\n\t\tpps = avcc.PictureParameterSets[0].NALUnit\n\t}\n\n\treturn sps, pps, nil\n}\n\nfunc esdsFindDecoderConf(descriptors []mp4.Descriptor) *mp4.DecoderConfigDescriptor {\n\tfor _, desc := range descriptors {\n\t\tif desc.Tag == mp4.DecoderConfigDescrTag {\n\t\t\treturn desc.DecoderConfigDescriptor\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc esdsFindDecoderSpecificInfo(descriptors []mp4.Descriptor) []byte {\n\tfor _, desc := range descriptors {\n\t\tif desc.Tag == mp4.DecSpecificInfoTag {\n\t\t\treturn desc.Data\n\t\t}\n\t}\n\treturn nil\n}\n\n// Init is a fMP4 initialization block.\ntype Init struct {\n\tTracks []*InitTrack\n}\n\n// Marshal encodes a fMP4 initialization file.\nfunc (i *Init) Marshal(w io.WriteSeeker) error {\n\t/*\n\t\t|ftyp|\n\t\t|moov|\n\t\t|    |mvhd|\n\t\t|    |trak|\n\t\t|    |trak|\n\t\t|    |....|\n\t\t|    |mvex|\n\t\t|    |    |trex|\n\t\t|    |    |trex|\n\t\t|    |    |....|\n\t*/\n\n\tmw := newMP4Writer(w)\n\n\t_, err := mw.writeBox(&mp4.Ftyp{ // <ftyp/>\n\t\tMajorBrand:   [4]byte{'m', 'p', '4', '2'},\n\t\tMinorVersion: 1,\n\t\tCompatibleBrands: []mp4.CompatibleBrandElem{\n\t\t\t{CompatibleBrand: [4]byte{'m', 'p', '4', '1'}},\n\t\t\t{CompatibleBrand: [4]byte{'m', 'p', '4', '2'}},\n\t\t\t{CompatibleBrand: [4]byte{'i', 's', 'o', 'm'}},\n\t\t\t{CompatibleBrand: [4]byte{'h', 'l', 's', 'f'}},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = mw.writeBoxStart(&mp4.Moov{}) // <moov>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = mw.writeBox(&mp4.Mvhd{ // <mvhd/>\n\t\tTimescale:   1000,\n\t\tRate:        65536,\n\t\tVolume:      256,\n\t\tMatrix:      [9]int32{0x00010000, 0, 0, 0, 0x00010000, 0, 0, 0, 0x40000000},\n\t\tNextTrackID: 4294967295,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, track := range i.Tracks {\n\t\terr = track.marshal(mw)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t_, err = mw.writeBoxStart(&mp4.Mvex{}) // <mvex>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, track := range i.Tracks {\n\t\t_, err = mw.writeBox(&mp4.Trex{ // <trex/>\n\t\t\tTrackID:                       uint32(track.ID),\n\t\t\tDefaultSampleDescriptionIndex: 1,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\terr = mw.writeBoxEnd() // </mvex>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = mw.writeBoxEnd() // </moov>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Unmarshal decodes a fMP4 initialization block.\nfunc (i *Init) Unmarshal(r io.ReadSeeker) error {\n\ttype readState int\n\n\tconst (\n\t\twaitingTrak readState = iota\n\t\twaitingTkhd\n\t\twaitingMdhd\n\t\twaitingCodec\n\t\twaitingAv1C\n\t\twaitingVpcC\n\t\twaitingHvcC\n\t\twaitingAvcC\n\t\twaitingVideoEsds\n\t\twaitingAudioEsds\n\t\twaitingDOps\n\t\twaitingDac3\n\t\twaitingPcmC\n\t)\n\n\tstate := waitingTrak\n\tvar curTrack *InitTrack\n\n\t/*\n\t\tvar width int\n\t\tvar height int\n\t\tvar sampleRate int\n\t\tvar channelCount int\n\t*/\n\n\t_, err := mp4.ReadBoxStructure(r, func(h *mp4.ReadHandle) (interface{}, error) {\n\t\tif !h.BoxInfo.IsSupportedType() {\n\t\t\tif state != waitingTrak {\n\t\t\t\ti.Tracks = i.Tracks[:len(i.Tracks)-1]\n\t\t\t\tstate = waitingTrak\n\t\t\t}\n\t\t} else {\n\t\t\tswitch h.BoxInfo.Type.String() {\n\t\t\tcase \"moov\":\n\t\t\t\treturn h.Expand()\n\n\t\t\tcase \"trak\":\n\t\t\t\tif state != waitingTrak {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t}\n\n\t\t\t\tcurTrack = &InitTrack{}\n\t\t\t\ti.Tracks = append(i.Tracks, curTrack)\n\t\t\t\tstate = waitingTkhd\n\t\t\t\treturn h.Expand()\n\n\t\t\tcase \"tkhd\":\n\t\t\t\tif state != waitingTkhd {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t}\n\n\t\t\t\tbox, _, err := h.ReadPayload()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\ttkhd := box.(*mp4.Tkhd)\n\n\t\t\t\tcurTrack.ID = int(tkhd.TrackID)\n\t\t\t\tstate = waitingMdhd\n\n\t\t\tcase \"mdia\":\n\t\t\t\treturn h.Expand()\n\n\t\t\tcase \"mdhd\":\n\t\t\t\tif state != waitingMdhd {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t}\n\n\t\t\t\tbox, _, err := h.ReadPayload()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tmdhd := box.(*mp4.Mdhd)\n\n\t\t\t\tcurTrack.TimeScale = mdhd.Timescale\n\t\t\t\tstate = waitingCodec\n\n\t\t\tcase \"minf\", \"stbl\", \"stsd\":\n\t\t\t\treturn h.Expand()\n\n\t\t\tcase \"avc1\":\n\t\t\t\tif state != waitingCodec {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t}\n\t\t\t\tstate = waitingAvcC\n\t\t\t\treturn h.Expand()\n\n\t\t\tcase \"avcC\":\n\t\t\t\tif state != waitingAvcC {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t}\n\n\t\t\t\tbox, _, err := h.ReadPayload()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tavcc := box.(*mp4.AVCDecoderConfiguration)\n\n\t\t\t\tsps, pps, err := h264FindParams(avcc)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tcurTrack.Codec = &CodecH264{\n\t\t\t\t\tSPS: sps,\n\t\t\t\t\tPPS: pps,\n\t\t\t\t}\n\t\t\t\tstate = waitingTrak\n\n\t\t\t/*\n\t\t\t\tcase \"vp09\":\n\t\t\t\t\tif state != waitingCodec {\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t\t}\n\n\t\t\t\t\tbox, _, err := h.ReadPayload()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\tvp09 := box.(*mp4.VisualSampleEntry)\n\n\t\t\t\t\twidth = int(vp09.Width)\n\t\t\t\t\theight = int(vp09.Height)\n\t\t\t\t\tstate = waitingVpcC\n\t\t\t\t\treturn h.Expand()\n\t\t\t*/\n\n\t\t\t/*\n\t\t\t\tcase \"vpcC\":\n\t\t\t\t\tif state != waitingVpcC {\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t\t}\n\n\t\t\t\t\tbox, _, err := h.ReadPayload()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\tvpcc := box.(*mp4.VpcC)\n\n\t\t\t\t\tcurTrack.Codec = &CodecVP9{\n\t\t\t\t\t\tWidth:             width,\n\t\t\t\t\t\tHeight:            height,\n\t\t\t\t\t\tProfile:           vpcc.Profile,\n\t\t\t\t\t\tBitDepth:          vpcc.BitDepth,\n\t\t\t\t\t\tChromaSubsampling: vpcc.ChromaSubsampling,\n\t\t\t\t\t\tColorRange:        vpcc.VideoFullRangeFlag != 0,\n\t\t\t\t\t}\n\t\t\t\t\tstate = waitingTrak\n\t\t\t*/\n\n\t\t\tcase \"vp08\": // VP8, not supported yet\n\t\t\t\treturn nil, nil\n\n\t\t\tcase \"hev1\", \"hvc1\":\n\t\t\t\tif state != waitingCodec {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t}\n\t\t\t\tstate = waitingHvcC\n\t\t\t\treturn h.Expand()\n\n\t\t\tcase \"hvcC\":\n\t\t\t\tif state != waitingHvcC {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t}\n\n\t\t\t\tbox, _, err := h.ReadPayload()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\thvcc := box.(*mp4.HvcC)\n\n\t\t\t\tvps, sps, pps, err := h265FindParams(hvcc.NaluArrays)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tcurTrack.Codec = &CodecH265{\n\t\t\t\t\tVPS: vps,\n\t\t\t\t\tSPS: sps,\n\t\t\t\t\tPPS: pps,\n\t\t\t\t}\n\t\t\t\tstate = waitingTrak\n\n\t\t\tcase \"av01\":\n\t\t\t\tif state != waitingCodec {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t}\n\t\t\t\tstate = waitingAv1C\n\t\t\t\treturn h.Expand()\n\n\t\t\tcase \"Opus\":\n\t\t\t\tif state != waitingCodec {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t}\n\t\t\t\tstate = waitingDOps\n\t\t\t\treturn h.Expand()\n\n\t\t\tcase \"dOps\":\n\t\t\t\tif state != waitingDOps {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t}\n\n\t\t\t\tbox, _, err := h.ReadPayload()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tdops := box.(*mp4.DOps)\n\n\t\t\t\tcurTrack.Codec = &CodecOpus{\n\t\t\t\t\tChannelCount: int(dops.OutputChannelCount),\n\t\t\t\t}\n\t\t\t\tstate = waitingTrak\n\n\t\t\tcase \"mp4v\":\n\t\t\t\tif state != waitingCodec {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t}\n\n\t\t\t\tbox, _, err := h.ReadPayload()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tmp4v := box.(*mp4.VisualSampleEntry)\n\n\t\t\t\twidth := int(mp4v.Width)\n\t\t\t\theight := int(mp4v.Height)\n\n\t\t\t\tLog.Info(\"width:\", width, \" height:\", height)\n\t\t\t\tstate = waitingVideoEsds\n\t\t\t\treturn h.Expand()\n\n\t\t\tcase \"mp4a\":\n\t\t\t\tif state != waitingCodec {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t}\n\n\t\t\t\tbox, _, err := h.ReadPayload()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tmp4a := box.(*mp4.AudioSampleEntry)\n\n\t\t\t\tsampleRate := int(mp4a.SampleRate / 65536)\n\t\t\t\tchannelCount := int(mp4a.ChannelCount)\n\n\t\t\t\tLog.Info(\"sampleRate:\", sampleRate, \" channelCount:\", channelCount)\n\t\t\t\tstate = waitingAudioEsds\n\t\t\t\treturn h.Expand()\n\n\t\t\tcase \"esds\":\n\t\t\t\tbox, _, err := h.ReadPayload()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tesds := box.(*mp4.Esds)\n\n\t\t\t\tconf := esdsFindDecoderConf(esds.Descriptors)\n\t\t\t\tif conf == nil {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unable to find decoder config\")\n\t\t\t\t}\n\n\t\t\t\tswitch state {\n\t\t\t\tcase waitingVideoEsds:\n\t\t\t\t\tswitch conf.ObjectTypeIndication {\n\t\t\t\t\tcase objectTypeIndicationVisualISO14496part2:\n\t\t\t\t\t\tspec := esdsFindDecoderSpecificInfo(esds.Descriptors)\n\t\t\t\t\t\tif spec == nil {\n\t\t\t\t\t\t\treturn nil, fmt.Errorf(\"unable to find decoder specific info\")\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t/*\n\t\t\t\t\t\t\tcurTrack.Codec = &CodecMPEG4Video{\n\t\t\t\t\t\t\t\tConfig: spec,\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t*/\n\n\t\t\t\t\tcase objectTypeIndicationVisualISO1318part2Main:\n\t\t\t\t\t\tspec := esdsFindDecoderSpecificInfo(esds.Descriptors)\n\t\t\t\t\t\tif spec == nil {\n\t\t\t\t\t\t\treturn nil, fmt.Errorf(\"unable to find decoder specific info\")\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t/*\n\t\t\t\t\t\t\tcurTrack.Codec = &CodecMPEG1Video{\n\t\t\t\t\t\t\t\tConfig: spec,\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t*/\n\n\t\t\t\t\tcase objectTypeIndicationVisualISO10918part1:\n\t\t\t\t\t\t/*\n\t\t\t\t\t\t\tcurTrack.Codec = &CodecMJPEG{\n\t\t\t\t\t\t\t\tWidth:  width,\n\t\t\t\t\t\t\t\tHeight: height,\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t*/\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"unsupported object type indication: 0x%.2x\", conf.ObjectTypeIndication)\n\t\t\t\t\t}\n\n\t\t\t\t\tstate = waitingTrak\n\n\t\t\t\tcase waitingAudioEsds:\n\t\t\t\t\tswitch conf.ObjectTypeIndication {\n\t\t\t\t\tcase objectTypeIndicationAudioISO14496part3:\n\t\t\t\t\t\tspec := esdsFindDecoderSpecificInfo(esds.Descriptors)\n\t\t\t\t\t\tif spec == nil {\n\t\t\t\t\t\t\treturn nil, fmt.Errorf(\"unable to find decoder specific info\")\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tascCtx, err := aac.NewAscContext(spec)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn nil, fmt.Errorf(\"NewAscContext failed, err: %w\", err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tcurTrack.Codec = &CodecAAC{\n\t\t\t\t\t\t\tCtx:     ascCtx,\n\t\t\t\t\t\t\tAscData: spec,\n\t\t\t\t\t\t}\n\n\t\t\t\t\tcase objectTypeIndicationAudioISO11172part3:\n\t\t\t\t\t\t/*\n\t\t\t\t\t\t\tcurTrack.Codec = &CodecMPEG1Audio{\n\t\t\t\t\t\t\t\tSampleRate:   sampleRate,\n\t\t\t\t\t\t\t\tChannelCount: channelCount,\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t*/\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"unsupported object type indication: 0x%.2x\", conf.ObjectTypeIndication)\n\t\t\t\t\t}\n\n\t\t\t\tdefault:\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t}\n\n\t\t\t\tstate = waitingTrak\n\n\t\t\tcase \"ac-3\":\n\t\t\t\tif state != waitingCodec {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t}\n\n\t\t\t\tbox, _, err := h.ReadPayload()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tac3 := box.(*mp4.AudioSampleEntry)\n\n\t\t\t\tsampleRate := int(ac3.SampleRate / 65536)\n\t\t\t\tchannelCount := int(ac3.ChannelCount)\n\n\t\t\t\tLog.Info(\"sampleRate:\", sampleRate, \" channelCount:\", channelCount)\n\t\t\t\tstate = waitingDac3\n\t\t\t\treturn h.Expand()\n\n\t\t\tcase \"dac3\":\n\t\t\t\tif state != waitingDac3 {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t}\n\n\t\t\t\t/*\n\n\t\t\t\t\tbox, _, err := h.ReadPayload()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t}\n\t\t\t\t\tdac3 := box.(*mp4.Dac3)\n\n\n\t\t\t\t\tcurTrack.Codec = &C{\n\t\t\t\t\t\tSampleRate:   sampleRate,\n\t\t\t\t\t\tChannelCount: channelCount,\n\t\t\t\t\t\tFscod:        dac3.Fscod,\n\t\t\t\t\t\tBsid:         dac3.Bsid,\n\t\t\t\t\t\tBsmod:        dac3.Bsmod,\n\t\t\t\t\t\tAcmod:        dac3.Acmod,\n\t\t\t\t\t\tLfeOn:        dac3.LfeOn != 0,\n\t\t\t\t\t\tBitRateCode:  dac3.BitRateCode,\n\t\t\t\t\t}\n\t\t\t\t*/\n\t\t\t\tstate = waitingTrak\n\n\t\t\tcase \"ipcm\":\n\t\t\t\tif state != waitingCodec {\n\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t}\n\n\t\t\t\tbox, _, err := h.ReadPayload()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tac3 := box.(*mp4.AudioSampleEntry)\n\n\t\t\t\tsampleRate := int(ac3.SampleRate / 65536)\n\t\t\t\tchannelCount := int(ac3.ChannelCount)\n\n\t\t\t\tLog.Info(\"sampleRate:\", sampleRate, \" channelCount:\", channelCount)\n\n\t\t\t\tstate = waitingPcmC\n\t\t\t\treturn h.Expand()\n\n\t\t\t\t/*\n\t\t\t\t\tcase \"pcmC\":\n\t\t\t\t\t\tif state != waitingPcmC {\n\t\t\t\t\t\t\treturn nil, fmt.Errorf(\"unexpected box '%v'\", h.BoxInfo.Type)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tbox, _, err := h.ReadPayload()\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn nil, err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpcmc := box.(*mp4.PcmC)\n\n\t\t\t\t\t\tcurTrack.Codec = &CodecLPCM{\n\t\t\t\t\t\t\tLittleEndian: (pcmc.FormatFlags & 0x01) != 0,\n\t\t\t\t\t\t\tBitDepth:     int(pcmc.PCMSampleSize),\n\t\t\t\t\t\t\tSampleRate:   sampleRate,\n\t\t\t\t\t\t\tChannelCount: channelCount,\n\t\t\t\t\t\t}\n\t\t\t\t\t\tstate = waitingTrak\n\n\t\t\t\t*/\n\t\t\t}\n\t\t}\n\n\t\treturn nil, nil\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif state != waitingTrak {\n\t\treturn fmt.Errorf(\"parse error\")\n\t}\n\n\tif len(i.Tracks) == 0 {\n\t\treturn fmt.Errorf(\"no tracks found\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "fmp4/muxer/init_track.go",
    "content": "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.com/q191201771/lal/pkg/avc\"\n\t\"github.com/q191201771/lal/pkg/hevc\"\n)\n\nfunc boolToUint8(v bool) uint8 {\n\tif v {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\n// InitTrack is a track of Init.\ntype InitTrack struct {\n\t// ID, starts from 1.\n\tID int\n\n\t// time scale.\n\tTimeScale uint32\n\n\t// maximum bitrate.\n\t// it defaults to 1MB for video tracks, 128k for audio tracks.\n\tMaxBitrate uint32\n\n\t// average bitrate.\n\t// it defaults to 1MB for video tracks, 128k for audio tracks.\n\tAvgBitrate uint32\n\n\t// codec.\n\tCodec Codec\n}\n\nfunc (it *InitTrack) marshal(w *mp4Writer) error {\n\t/*\n\t\t|trak|\n\t\t|    |tkhd|\n\t\t|    |mdia|\n\t\t|    |    |mdhd|\n\t\t|    |    |hdlr|\n\t\t|    |    |minf|\n\t\t|    |    |    |vmhd| (video)\n\t\t|    |    |    |smhd| (audio)\n\t\t|    |    |    |dinf|\n\t\t|    |    |    |    |dref|\n\t\t|    |    |    |    |    |url|\n\t\t|    |    |    |stbl|\n\t\t|    |    |    |    |stsd|\n\t\t|    |    |    |    |    |av01| (AV1)\n\t\t|    |    |    |    |    |    |av1C|\n\t\t|    |    |    |    |    |    |btrt|\n\t\t|    |    |    |    |    |vp09| (VP9)\n\t\t|    |    |    |    |    |    |vpcC|\n\t\t|    |    |    |    |    |    |btrt|\n\t\t|    |    |    |    |    |hev1| (H265)\n\t\t|    |    |    |    |    |    |hvcC|\n\t\t|    |    |    |    |    |    |btrt|\n\t\t|    |    |    |    |    |avc1| (H264)\n\t\t|    |    |    |    |    |    |avcC|\n\t\t|    |    |    |    |    |    |btrt|\n\t\t|    |    |    |    |    |mp4v| (MPEG-4/2/1 video, MJPEG)\n\t\t|    |    |    |    |    |    |esds|\n\t\t|    |    |    |    |    |    |btrt|\n\t\t|    |    |    |    |    |Opus| (Opus)\n\t\t|    |    |    |    |    |    |dOps|\n\t\t|    |    |    |    |    |    |btrt|\n\t\t|    |    |    |    |    |mp4a| (MPEG-4/1 audio)\n\t\t|    |    |    |    |    |    |esds|\n\t\t|    |    |    |    |    |    |btrt|\n\t\t|    |    |    |    |    |ac-3| (AC-3)\n\t\t|    |    |    |    |    |    |dac3|\n\t\t|    |    |    |    |    |    |btrt|\n\t\t|    |    |    |    |    |ipcm| (LPCM)\n\t\t|    |    |    |    |    |    |pcmC|\n\t\t|    |    |    |    |    |    |btrt|\n\t\t|\t |    |    |    |    |fLaC| (FLAC)\n\t\t|    |    |    |    |stts|\n\t\t|    |    |    |    |stsc|\n\t\t|    |    |    |    |stsz|\n\t\t|    |    |    |    |stco|\n\t*/\n\n\tvar width int\n\tvar height int\n\n\t_, err := w.writeBoxStart(&mp4.Trak{}) // <trak>\n\tif err != nil {\n\t\treturn err\n\t}\n\tif it.Codec == nil {\n\t\treturn fmt.Errorf(\"codec is not for track\")\n\t}\n\tswitch codec := it.Codec.(type) {\n\tcase *CodecH264:\n\t\tif len(codec.SPS) == 0 || len(codec.PPS) == 0 {\n\t\t\treturn fmt.Errorf(\"H264 parameters not provided\")\n\t\t}\n\n\t\tvar ctx avc.Context\n\t\terr = avc.ParseSps(codec.SPS, &ctx)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"h264 parse sps failed\")\n\t\t}\n\n\t\twidth = int(ctx.Width)\n\t\theight = int(ctx.Height)\n\n\tcase *CodecH265:\n\t\tif len(codec.SPS) == 0 || len(codec.PPS) == 0 || len(codec.VPS) == 0 {\n\t\t\treturn fmt.Errorf(\"H265 parameters not provided\")\n\t\t}\n\n\t\tvar ctx hevc.Context\n\t\terr = hevc.ParseSps(codec.SPS, &ctx)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"hevc parse sps failed\")\n\t\t}\n\n\t\twidth = int(ctx.PicWidthInLumaSamples)\n\t\theight = int(ctx.PicHeightInLumaSamples)\n\n\t}\n\tif it.Codec == nil {\n\t\treturn nil\n\t}\n\tif it.Codec.IsVideo() {\n\t\t_, err = w.writeBox(&mp4.Tkhd{ // <tkhd/>\n\t\t\tFullBox: mp4.FullBox{\n\t\t\t\tFlags: [3]byte{0, 0, 3},\n\t\t\t},\n\t\t\tTrackID: uint32(it.ID),\n\t\t\tWidth:   uint32(width * 65536),\n\t\t\tHeight:  uint32(height * 65536),\n\t\t\tMatrix:  [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\t_, err = w.writeBox(&mp4.Tkhd{ // <tkhd/>\n\t\t\tFullBox: mp4.FullBox{\n\t\t\t\tFlags: [3]byte{0, 0, 3},\n\t\t\t},\n\t\t\tTrackID:        uint32(it.ID),\n\t\t\tAlternateGroup: 1,\n\t\t\tVolume:         256,\n\t\t\tMatrix:         [9]int32{0x10000, 0, 0, 0, 0x10000, 0, 0, 0, 0x40000000},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t_, err = w.writeBoxStart(&mp4.Mdia{}) // <mdia>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = w.writeBox(&mp4.Mdhd{ // <mdhd/>\n\t\tTimescale: it.TimeScale,\n\t\tLanguage:  [3]byte{'u', 'n', 'd'},\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif it.Codec.IsVideo() {\n\t\t_, err = w.writeBox(&mp4.Hdlr{ // <hdlr/>\n\t\t\tHandlerType: [4]byte{'v', 'i', 'd', 'e'},\n\t\t\tName:        \"VideoHandler\",\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\t_, err = w.writeBox(&mp4.Hdlr{ // <hdlr/>\n\t\t\tHandlerType: [4]byte{'s', 'o', 'u', 'n'},\n\t\t\tName:        \"SoundHandler\",\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t_, err = w.writeBoxStart(&mp4.Minf{}) // <minf>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif it.Codec.IsVideo() {\n\t\t_, err = w.writeBox(&mp4.Vmhd{ // <vmhd/>\n\t\t\tFullBox: mp4.FullBox{\n\t\t\t\tFlags: [3]byte{0, 0, 1},\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\t_, err = w.writeBox(&mp4.Smhd{}) // <smhd/>\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t_, err = w.writeBoxStart(&mp4.Dinf{}) // <dinf>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = w.writeBoxStart(&mp4.Dref{ // <dref>\n\t\tEntryCount: 1,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = w.writeBox(&mp4.Url{ // <url/>\n\t\tFullBox: mp4.FullBox{\n\t\t\tFlags: [3]byte{0, 0, 1},\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = w.writeBoxEnd() // </dref>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = w.writeBoxEnd() // </dinf>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = w.writeBoxStart(&mp4.Stbl{}) // <stbl>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = w.writeBoxStart(&mp4.Stsd{ // <stsd>\n\t\tEntryCount: 1,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmaxBitrate := it.MaxBitrate\n\tif maxBitrate == 0 {\n\t\tif it.Codec.IsVideo() {\n\t\t\tmaxBitrate = 1000000\n\t\t} else {\n\t\t\tmaxBitrate = 128825\n\t\t}\n\t}\n\n\tavgBitrate := it.AvgBitrate\n\tif avgBitrate == 0 {\n\t\tif it.Codec.IsVideo() {\n\t\t\tavgBitrate = 1000000\n\t\t} else {\n\t\t\tavgBitrate = 128825\n\t\t}\n\t}\n\n\tswitch codec := it.Codec.(type) {\n\tcase *CodecH264:\n\t\t_, err = w.writeBoxStart(&mp4.VisualSampleEntry{ // <avc1>\n\t\t\tSampleEntry: mp4.SampleEntry{\n\t\t\t\tAnyTypeBox: mp4.AnyTypeBox{\n\t\t\t\t\tType: mp4.BoxTypeAvc1(),\n\t\t\t\t},\n\t\t\t\tDataReferenceIndex: 1,\n\t\t\t},\n\t\t\tWidth:           uint16(width),\n\t\t\tHeight:          uint16(height),\n\t\t\tHorizresolution: 4718592,\n\t\t\tVertresolution:  4718592,\n\t\t\tFrameCount:      1,\n\t\t\tDepth:           24,\n\t\t\tPreDefined3:     -1,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar ctx avc.Context\n\t\terr = avc.ParseSps(codec.SPS, &ctx)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"h264 parse sps failed\")\n\t\t}\n\n\t\t_, err = w.writeBox(&mp4.AVCDecoderConfiguration{ // <avcc/>\n\t\t\tAnyTypeBox: mp4.AnyTypeBox{\n\t\t\t\tType: mp4.BoxTypeAvcC(),\n\t\t\t},\n\t\t\tConfigurationVersion:       1,\n\t\t\tProfile:                    ctx.Profile,\n\t\t\tProfileCompatibility:       codec.SPS[2],\n\t\t\tLevel:                      ctx.Level,\n\t\t\tLengthSizeMinusOne:         3,\n\t\t\tNumOfSequenceParameterSets: 1,\n\t\t\tSequenceParameterSets: []mp4.AVCParameterSet{\n\t\t\t\t{\n\t\t\t\t\tLength:  uint16(len(codec.SPS)),\n\t\t\t\t\tNALUnit: codec.SPS,\n\t\t\t\t},\n\t\t\t},\n\t\t\tNumOfPictureParameterSets: 1,\n\t\t\tPictureParameterSets: []mp4.AVCParameterSet{\n\t\t\t\t{\n\t\t\t\t\tLength:  uint16(len(codec.PPS)),\n\t\t\t\t\tNALUnit: codec.PPS,\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase *CodecH265:\n\t\t_, err = w.writeBoxStart(&mp4.VisualSampleEntry{ // <hev1>\n\t\t\tSampleEntry: mp4.SampleEntry{\n\t\t\t\tAnyTypeBox: mp4.AnyTypeBox{\n\t\t\t\t\tType: mp4.BoxTypeHev1(),\n\t\t\t\t},\n\t\t\t\tDataReferenceIndex: 1,\n\t\t\t},\n\t\t\tWidth:           uint16(width),\n\t\t\tHeight:          uint16(height),\n\t\t\tHorizresolution: 4718592,\n\t\t\tVertresolution:  4718592,\n\t\t\tFrameCount:      1,\n\t\t\tDepth:           24,\n\t\t\tPreDefined3:     -1,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar ctx hevc.Context\n\t\terr = hevc.ParseSps(codec.SPS, &ctx)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"hevc parse sps failed\")\n\t\t}\n\n\t\t_, err = w.writeBox(&mp4.HvcC{ // <hvcC/>\n\t\t\tConfigurationVersion:        1,\n\t\t\tGeneralProfileIdc:           ctx.GeneralProfileIdc,\n\t\t\tGeneralProfileCompatibility: Uint32ToBoolSlice(ctx.GeneralProfileCompatibilityFlags),\n\t\t\tGeneralConstraintIndicator: [6]uint8{\n\t\t\t\tcodec.SPS[7], codec.SPS[8], codec.SPS[9],\n\t\t\t\tcodec.SPS[10], codec.SPS[11], codec.SPS[12],\n\t\t\t},\n\t\t\tGeneralLevelIdc: ctx.GeneralLevelIdc,\n\t\t\t// MinSpatialSegmentationIdc\n\t\t\t// ParallelismType\n\t\t\tChromaFormatIdc:      uint8(ctx.ChromaFormat),\n\t\t\tBitDepthLumaMinus8:   uint8(ctx.BitDepthLumaMinus8),\n\t\t\tBitDepthChromaMinus8: uint8(ctx.BitDepthChromaMinus8),\n\t\t\t// AvgFrameRate\n\t\t\t// ConstantFrameRate\n\t\t\tNumTemporalLayers: 1,\n\t\t\t// TemporalIdNested\n\t\t\tLengthSizeMinusOne: 3,\n\t\t\tNumOfNaluArrays:    3,\n\t\t\tNaluArrays: []mp4.HEVCNaluArray{\n\t\t\t\t{\n\t\t\t\t\tNaluType: byte(h265.NALUType_VPS_NUT),\n\t\t\t\t\tNumNalus: 1,\n\t\t\t\t\tNalus: []mp4.HEVCNalu{{\n\t\t\t\t\t\tLength:  uint16(len(codec.VPS)),\n\t\t\t\t\t\tNALUnit: codec.VPS,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tNaluType: byte(h265.NALUType_SPS_NUT),\n\t\t\t\t\tNumNalus: 1,\n\t\t\t\t\tNalus: []mp4.HEVCNalu{{\n\t\t\t\t\t\tLength:  uint16(len(codec.SPS)),\n\t\t\t\t\t\tNALUnit: codec.SPS,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tNaluType: byte(h265.NALUType_PPS_NUT),\n\t\t\t\t\tNumNalus: 1,\n\t\t\t\t\tNalus: []mp4.HEVCNalu{{\n\t\t\t\t\t\tLength:  uint16(len(codec.PPS)),\n\t\t\t\t\t\tNALUnit: codec.PPS,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase *CodecAAC:\n\t\tsampleRate, _ := codec.Ctx.GetSamplingFrequency()\n\t\t_, err = w.writeBoxStart(&mp4.AudioSampleEntry{ // <mp4a>\n\t\t\tSampleEntry: mp4.SampleEntry{\n\t\t\t\tAnyTypeBox: mp4.AnyTypeBox{\n\t\t\t\t\tType: mp4.BoxTypeMp4a(),\n\t\t\t\t},\n\t\t\t\tDataReferenceIndex: 1,\n\t\t\t},\n\t\t\tChannelCount: uint16(codec.Ctx.ChannelConfiguration),\n\t\t\tSampleSize:   16,\n\t\t\tSampleRate:   uint32(sampleRate * 65536),\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t_, err = w.writeBox(&mp4.Esds{ // <esds/>\n\t\t\tDescriptors: []mp4.Descriptor{\n\t\t\t\t{\n\t\t\t\t\tTag:  mp4.ESDescrTag,\n\t\t\t\t\tSize: 32 + uint32(len(codec.AscData)),\n\t\t\t\t\tESDescriptor: &mp4.ESDescriptor{\n\t\t\t\t\t\tESID: uint16(it.ID),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tTag:  mp4.DecoderConfigDescrTag,\n\t\t\t\t\tSize: 18 + uint32(len(codec.Ctx.Pack())),\n\t\t\t\t\tDecoderConfigDescriptor: &mp4.DecoderConfigDescriptor{\n\t\t\t\t\t\tObjectTypeIndication: objectTypeIndicationAudioISO14496part3,\n\t\t\t\t\t\tStreamType:           streamTypeAudioStream,\n\t\t\t\t\t\tReserved:             true,\n\t\t\t\t\t\tMaxBitrate:           maxBitrate,\n\t\t\t\t\t\tAvgBitrate:           avgBitrate,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tTag:  mp4.DecSpecificInfoTag,\n\t\t\t\t\tSize: uint32(len(codec.AscData)),\n\t\t\t\t\tData: codec.AscData,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tTag:  mp4.SLConfigDescrTag,\n\t\t\t\t\tSize: 1,\n\t\t\t\t\tData: []byte{0x02},\n\t\t\t\t},\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase *CodecOpus:\n\t\t_, err = w.writeBoxStart(&mp4.AudioSampleEntry{ // <Opus>\n\t\t\tSampleEntry: mp4.SampleEntry{\n\t\t\t\tAnyTypeBox: mp4.AnyTypeBox{\n\t\t\t\t\tType: mp4.BoxTypeOpus(),\n\t\t\t\t},\n\t\t\t\tDataReferenceIndex: 1,\n\t\t\t},\n\t\t\tChannelCount: uint16(codec.ChannelCount),\n\t\t\tSampleSize:   16,\n\t\t\tSampleRate:   48000 * 65536,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t_, err = w.writeBox(&mp4.DOps{ // <dOps/>\n\t\t\tOutputChannelCount: uint8(codec.ChannelCount),\n\t\t\tPreSkip:            312,\n\t\t\tInputSampleRate:    48000,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t_, err = w.writeBox(&mp4.Btrt{ // <btrt/>\n\t\tMaxBitrate: maxBitrate,\n\t\tAvgBitrate: avgBitrate,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = w.writeBoxEnd() // </*>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = w.writeBoxEnd() // </stsd>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = w.writeBox(&mp4.Stts{ // <stts/>\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = w.writeBox(&mp4.Stsc{ // <stsc/>\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = w.writeBox(&mp4.Stsz{ // <stsz/>\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = w.writeBox(&mp4.Stco{ // <stco/>\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = w.writeBoxEnd() // </stbl>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = w.writeBoxEnd() // </minf>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = w.writeBoxEnd() // </mdia>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = w.writeBoxEnd() // </trak>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc Uint32ToBoolSlice(num uint32) [32]bool {\n\tvar boolSlice [32]bool\n\n\tfor i := 0; i < 32; i++ {\n\t\tboolSlice[i] = num&(1<<i) != 0\n\t}\n\n\treturn boolSlice\n}\n"
  },
  {
    "path": "fmp4/muxer/mp4_writer.go",
    "content": "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(w io.WriteSeeker) *mp4Writer {\n\treturn &mp4Writer{\n\t\tw: mp4.NewWriter(w),\n\t}\n}\n\nfunc (w *mp4Writer) writeBoxStart(box mp4.IImmutableBox) (int, error) {\n\tbi := &mp4.BoxInfo{\n\t\tType: box.GetType(),\n\t}\n\tvar err error\n\tbi, err = w.w.StartBox(bi)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\t_, err = mp4.Marshal(w.w, box, mp4.Context{})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn int(bi.Offset), nil\n}\n\nfunc (w *mp4Writer) writeBoxEnd() error {\n\t_, err := w.w.EndBox()\n\treturn err\n}\n\nfunc (w *mp4Writer) writeBox(box mp4.IImmutableBox) (int, error) {\n\toff, err := w.writeBoxStart(box)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\terr = w.writeBoxEnd()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn off, nil\n}\n\nfunc (w *mp4Writer) rewriteBox(off int, box mp4.IImmutableBox) error {\n\tprevOff, err := w.w.Seek(0, io.SeekCurrent)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = w.w.Seek(int64(off), io.SeekStart)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = w.writeBoxStart(box)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = w.writeBoxEnd()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = w.w.Seek(prevOff, io.SeekStart)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "fmp4/muxer/muxer.go",
    "content": "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\"github.com/q191201771/lalmax/utils\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\nfunc AudioTimeScale(c Codec) uint32 {\n\tswitch codec := c.(type) {\n\tcase *CodecAAC:\n\t\tsamplerate, _ := codec.Ctx.GetSamplingFrequency()\n\t\treturn uint32(samplerate)\n\tcase *CodecOpus:\n\t\treturn 48000\n\t}\n\n\treturn 0\n}\n\nfunc TsToTime(ts uint32) time.Duration {\n\treturn time.Millisecond * time.Duration(ts)\n}\n\ntype Muxer struct {\n\tVideoTrack      *Track\n\tAudioTrack      *Track\n\tnextTrackId     uint32\n\tinitFmp4        []byte\n\thasinitVideo    bool\n\thasinitAudio    bool\n\tvps, sps, pps   []byte\n\tauidoTimeScale  uint32\n\tlastVideoDts    time.Duration\n\tlastAudioDts    time.Duration\n\tlog             nazalog.Logger\n\tVideoDtsDecoder *utils.DtsDecoder\n\tAudioDtsDecoder *utils.DtsDecoder\n}\n\nfunc NewMuxer() *Muxer {\n\treturn &Muxer{\n\t\tnextTrackId: 1,\n\t\tlog:         Log,\n\t}\n}\n\nfunc (m *Muxer) WithLog(log nazalog.Logger) {\n\tm.log = log\n}\n\nfunc (m *Muxer) AddVideoTrack(c Codec) {\n\tswitch codec := c.(type) {\n\tcase *CodecH264:\n\t\tm.sps = codec.SPS\n\t\tm.pps = codec.PPS\n\tcase *CodecH265:\n\t\tm.vps = codec.VPS\n\t\tm.sps = codec.SPS\n\t\tm.pps = codec.PPS\n\n\tdefault:\n\t\tm.log.Errorf(\"invalid video codec\")\n\t\treturn\n\t}\n\n\tm.VideoTrack = NewTrack(c, m.nextTrackId, 90000)\n\tm.nextTrackId++\n}\n\nfunc (m *Muxer) AddAudioTrack(c Codec) {\n\tm.auidoTimeScale = AudioTimeScale(c)\n\tm.AudioTrack = NewTrack(c, m.nextTrackId, m.auidoTimeScale)\n\tm.nextTrackId++\n}\n\nfunc (m *Muxer) AudioTimeScale() uint32 {\n\treturn m.auidoTimeScale\n}\n\nfunc (m *Muxer) GetInitMp4() []byte {\n\tif m.initFmp4 == nil {\n\t\tinit := &Init{}\n\t\tif m.VideoTrack != nil {\n\t\t\tinit.Tracks = append(init.Tracks, &InitTrack{\n\t\t\t\tID:        int(m.VideoTrack.TrackId),\n\t\t\t\tTimeScale: 90000,\n\t\t\t\tCodec:     m.VideoTrack.Codec,\n\t\t\t})\n\t\t}\n\n\t\tif m.AudioTrack != nil {\n\t\t\tinit.Tracks = append(init.Tracks, &InitTrack{\n\t\t\t\tID:        int(m.AudioTrack.TrackId),\n\t\t\t\tTimeScale: m.auidoTimeScale,\n\t\t\t\tCodec:     m.AudioTrack.Codec,\n\t\t\t})\n\t\t}\n\n\t\tvar w Buffer\n\t\terr := init.Marshal(&w)\n\t\tif err != nil {\n\t\t\tm.log.Errorf(\"marshal init fmp4 failed: %v\", err)\n\t\t\treturn nil\n\t\t}\n\n\t\tm.initFmp4 = w.Bytes()\n\t}\n\n\treturn m.initFmp4\n}\n\nfunc (m *Muxer) Pack(msg base.RtmpMsg) (*PartSample, error) {\n\tif msg.Header.MsgTypeId == base.RtmpTypeIdVideo && !msg.IsVideoKeySeqHeader() {\n\t\treturn m.FeedVideo(msg)\n\t} else if msg.Header.MsgTypeId == base.RtmpTypeIdAudio && !msg.IsAacSeqHeader() {\n\t\treturn m.FeedAudio(msg)\n\t}\n\n\treturn nil, fmt.Errorf(\"invalid msg type\")\n}\n\nfunc (m *Muxer) FeedVideo(msg base.RtmpMsg) (*PartSample, error) {\n\tif m.VideoTrack == nil {\n\t\treturn nil, fmt.Errorf(\"no video track\")\n\t}\n\trandomAccess := false\n\tvar nalus [][]byte\n\n\tif msg.IsVideoKeySeqHeader() {\n\t\treturn nil, fmt.Errorf(\"msg is video key seq header\")\n\t}\n\n\tif !m.hasinitVideo {\n\t\tif !msg.IsVideoKeyNalu() {\n\t\t\treturn nil, fmt.Errorf(\"first video require key frame\")\n\t\t}\n\t}\n\n\tvar sample *PartSample\n\tif m.VideoDtsDecoder == nil {\n\t\tm.VideoDtsDecoder = utils.NewDtsDecoder(0, 90000, msg.Dts())\n\t}\n\n\tdts := m.VideoDtsDecoder.Decode(msg.Dts())\n\tif !m.hasinitVideo {\n\t\tm.lastVideoDts = dts\n\t\tm.hasinitVideo = true\n\t}\n\n\tsample_duration := uint32(durationGoToMp4(dts-m.lastVideoDts, 90000))\n\n\tswitch msg.VideoCodecId() {\n\tcase base.RtmpCodecIdAvc:\n\t\tnals, _ := avc.SplitNaluAvcc(msg.Payload[5:])\n\t\tif msg.IsAvcKeyNalu() {\n\t\t\trandomAccess = true\n\t\t\tnalus = append(nalus, m.sps)\n\t\t\tnalus = append(nalus, m.pps)\n\t\t\tnalus = append(nalus, nals...)\n\t\t} else {\n\t\t\tnalus = append(nalus, nals...)\n\t\t}\n\n\t\tsample = NewPartSampleH26x(int32(durationGoToMp4(TsToTime(msg.Cts()), 90000)), randomAccess, nalus, sample_duration, dts)\n\n\tcase base.RtmpCodecIdHevc:\n\t\tvar nals [][]byte\n\t\tif msg.IsEnchanedHevcNalu() {\n\t\t\tindex := msg.GetEnchanedHevcNaluIndex()\n\t\t\tnals, _ = avc.SplitNaluAvcc(msg.Payload[index:])\n\t\t} else {\n\t\t\tnals, _ = avc.SplitNaluAvcc(msg.Payload[5:])\n\t\t}\n\n\t\tif msg.IsHevcKeyNalu() {\n\t\t\trandomAccess = true\n\t\t\tnalus = append(nalus, m.vps)\n\t\t\tnalus = append(nalus, m.sps)\n\t\t\tnalus = append(nalus, m.pps)\n\t\t\tnalus = append(nalus, nals...)\n\t\t} else {\n\t\t\tnalus = append(nalus, nals...)\n\t\t}\n\n\t\tsample = NewPartSampleH26x(int32(durationGoToMp4(TsToTime(msg.Cts()), 90000)), randomAccess, nalus, sample_duration, dts)\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"invalid video codec id: %d\", msg.VideoCodecId())\n\t}\n\n\tm.lastVideoDts = dts\n\n\treturn sample, nil\n}\n\nfunc (m *Muxer) FeedAudio(msg base.RtmpMsg) (*PartSample, error) {\n\tif m.AudioTrack == nil {\n\t\treturn nil, fmt.Errorf(\"no audio track\")\n\t}\n\n\tif m.AudioDtsDecoder == nil {\n\t\tm.AudioDtsDecoder = utils.NewDtsDecoder(0, time.Duration(m.auidoTimeScale), msg.Dts())\n\t}\n\n\tdts := m.AudioDtsDecoder.Decode(msg.Dts())\n\tif !m.hasinitAudio {\n\t\tm.lastAudioDts = dts\n\t\tm.hasinitAudio = true\n\t}\n\tsample_duration := uint32(durationGoToMp4(dts-m.lastAudioDts, m.auidoTimeScale))\n\tvar payload []byte\n\tswitch msg.AudioCodecId() {\n\tcase base.RtmpSoundFormatAac:\n\t\tpayload = msg.Payload[2:]\n\tcase base.RtmpSoundFormatOpus:\n\t\tpayload = msg.Payload[5:]\n\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"invalid audio codec id: %d\", msg.AudioCodecId())\n\t}\n\n\tsample := &PartSample{\n\t\tDts:             dts,\n\t\tDuration:        sample_duration,\n\t\tIsNonSyncSample: true,\n\t\tPTSOffset:       0,\n\t\tPayload:         payload,\n\t}\n\n\tm.lastAudioDts = dts\n\n\treturn sample, nil\n}\n"
  },
  {
    "path": "fmp4/muxer/muxer_part.go",
    "content": "package muxer\n\nimport \"time\"\n\nfunc durationGoToMp4(v time.Duration, timeScale uint32) uint64 {\n\ttimeScale64 := uint64(timeScale)\n\tsecs := v / time.Second\n\tdec := v % time.Second\n\treturn uint64(secs)*timeScale64 + uint64(dec)*timeScale64/uint64(time.Second)\n}\n\nfunc durationMp4ToGo(v uint64, timeScale uint32) time.Duration {\n\ttimeScale64 := uint64(timeScale)\n\tsecs := v / timeScale64\n\tdec := v % timeScale64\n\treturn time.Duration(secs)*time.Second + time.Duration(dec)*time.Second/time.Duration(timeScale64)\n}\n\ntype MuxerPart struct {\n\tVideoSamples   []*PartSample\n\tAudioSamples   []*PartSample\n\taudioTimeScale uint32\n\n\tvideoStartDTSFilled bool\n\tvideoStartDTS       time.Duration\n\taudioStartDTSFilled bool\n\taudioStartDTS       time.Duration\n\n\tbuffer            *Buffer\n\tpartId            uint64\n\tpartDuration      time.Duration\n\tvideoPartDuration time.Duration\n\taudioPartDuration time.Duration\n}\n\nfunc NewMuxerPart(partId uint64, audioTimeScale uint32) *MuxerPart {\n\treturn &MuxerPart{\n\t\tbuffer:         &Buffer{},\n\t\tpartId:         partId,\n\t\taudioTimeScale: audioTimeScale,\n\t}\n}\n\nfunc (p *MuxerPart) Bytes() []byte {\n\treturn p.buffer.Bytes()\n}\n\nfunc (p *MuxerPart) Duration() time.Duration {\n\treturn p.partDuration\n}\n\nfunc (p *MuxerPart) AudioTimeScale() uint32 {\n\treturn p.audioTimeScale\n}\n\nfunc (p *MuxerPart) Encode(lastSampleDuration time.Duration, end bool) error {\n\tpart := Part{\n\t\tSequenceNumber: uint32(p.partId),\n\t}\n\n\tif p.VideoSamples != nil {\n\t\tpart.Tracks = append(part.Tracks, &PartTrack{\n\t\t\tID:       1,\n\t\t\tBaseTime: durationGoToMp4(p.videoStartDTS, 90000),\n\t\t\tSamples:  p.VideoSamples,\n\t\t})\n\t}\n\n\tif p.AudioSamples != nil {\n\t\tpart.Tracks = append(part.Tracks, &PartTrack{\n\t\t\tID:       1 + len(part.Tracks),\n\t\t\tBaseTime: durationGoToMp4(p.audioStartDTS, p.audioTimeScale),\n\t\t\tSamples:  p.AudioSamples,\n\t\t})\n\t}\n\n\terr := part.Marshal(p.buffer)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !end {\n\t\tif p.VideoSamples != nil {\n\t\t\tp.partDuration = lastSampleDuration - p.videoStartDTS\n\t\t} else {\n\t\t\tp.partDuration = lastSampleDuration - p.audioStartDTS\n\t\t}\n\t} else {\n\t\tif p.VideoSamples != nil {\n\t\t\tp.partDuration = p.videoPartDuration\n\t\t} else {\n\t\t\tp.partDuration = p.audioPartDuration\n\t\t}\n\t}\n\n\tp.VideoSamples = nil\n\tp.AudioSamples = nil\n\n\treturn nil\n}\n\nfunc (p *MuxerPart) WriteVideo(sample *PartSample) {\n\tif !p.videoStartDTSFilled {\n\t\tp.videoStartDTSFilled = true\n\t\tp.videoStartDTS = sample.Dts\n\t}\n\n\tp.videoPartDuration = sample.Dts - p.videoStartDTS\n\tp.VideoSamples = append(p.VideoSamples, sample)\n}\n\nfunc (p *MuxerPart) WriteAudio(sample *PartSample) {\n\tif !p.audioStartDTSFilled {\n\t\tp.audioStartDTSFilled = true\n\t\tp.audioStartDTS = sample.Dts\n\t}\n\n\tp.audioPartDuration = sample.Dts - p.audioStartDTS\n\tp.AudioSamples = append(p.AudioSamples, sample)\n}\n\nfunc (p *MuxerPart) StartVideoDts() time.Duration {\n\treturn p.videoStartDTS\n}\n\nfunc (p *MuxerPart) StartAudioDts() time.Duration {\n\treturn p.audioStartDTS\n}\n\nfunc (p *MuxerPart) ResetStartVideoDts() {\n\tp.videoStartDTS = 0\n}\n\nfunc (p *MuxerPart) ResetStartAudioDts() {\n\tp.audioStartDTS = 0\n}\n\nfunc (p *MuxerPart) Clone() *MuxerPart {\n\tclone := *p\n\tclone.buffer = &Buffer{}\n\treturn &clone\n}\n\nfunc (p *MuxerPart) SetPartId(partId uint64) {\n\tp.partId = partId\n}\n\nfunc (p *MuxerPart) CalcDuration(newPartStartDts time.Duration, end bool) (partDuration time.Duration) {\n\tif !end {\n\t\tif p.VideoSamples != nil {\n\t\t\tpartDuration = newPartStartDts - p.videoStartDTS\n\t\t} else {\n\t\t\tpartDuration = newPartStartDts - p.audioStartDTS\n\t\t}\n\t} else {\n\t\tif p.VideoSamples != nil {\n\t\t\tpartDuration = p.videoPartDuration\n\t\t} else {\n\t\t\tpartDuration = p.audioPartDuration\n\t\t}\n\t}\n\n\treturn partDuration\n}\n\nfunc (p *MuxerPart) SetVideoStartDts(videoStartDTS time.Duration) {\n\tp.videoStartDTS = videoStartDTS\n}\n\nfunc (p *MuxerPart) SetAudioStartDts(audioStartDTS time.Duration) {\n\tp.audioStartDTS = audioStartDTS\n}\n"
  },
  {
    "path": "fmp4/muxer/part.go",
    "content": "package muxer\n\nimport (\n\t\"io\"\n\n\t\"github.com/abema/go-mp4\"\n)\n\nconst (\n\ttrunFlagDataOffsetPreset                       = 0x01\n\ttrunFlagSampleDurationPresent                  = 0x100\n\ttrunFlagSampleSizePresent                      = 0x200\n\ttrunFlagSampleFlagsPresent                     = 0x400\n\ttrunFlagSampleCompositionTimeOffsetPresentOrV1 = 0x800\n\n\tsampleFlagIsNonSyncSample = 1 << 16\n)\n\n// Part is a fMP4 part.\ntype Part struct {\n\tSequenceNumber uint32\n\tTracks         []*PartTrack\n}\n\n// Marshal encodes a fMP4 part.\nfunc (p *Part) Marshal(w io.WriteSeeker) error {\n\t/*\n\t\t|moof|\n\t\t|    |mfhd|\n\t\t|    |traf|\n\t\t|    |traf|\n\t\t|    |....|\n\t\t|mdat|\n\t*/\n\n\tmw := newMP4Writer(w)\n\n\tmoofOffset, err := mw.writeBoxStart(&mp4.Moof{}) // <moof>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = mw.writeBox(&mp4.Mfhd{ // <mfhd/>\n\t\tSequenceNumber: p.SequenceNumber,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttrackLen := len(p.Tracks)\n\ttruns := make([]*mp4.Trun, trackLen)\n\ttrunOffsets := make([]int, trackLen)\n\tdataOffsets := make([]int, trackLen)\n\tdataSize := 0\n\n\tfor i, track := range p.Tracks {\n\t\tvar trun *mp4.Trun\n\t\tvar trunOffset int\n\t\ttrun, trunOffset, err = track.marshal(mw)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdataOffsets[i] = dataSize\n\n\t\tfor _, sample := range track.Samples {\n\t\t\tdataSize += len(sample.Payload)\n\t\t}\n\n\t\ttruns[i] = trun\n\t\ttrunOffsets[i] = trunOffset\n\t}\n\n\terr = mw.writeBoxEnd() // </moof>\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmdat := &mp4.Mdat{} // <mdat/>\n\tmdat.Data = make([]byte, dataSize)\n\tpos := 0\n\n\tfor _, track := range p.Tracks {\n\t\tfor _, sample := range track.Samples {\n\t\t\tpos += copy(mdat.Data[pos:], sample.Payload)\n\t\t}\n\t}\n\n\tmdatOffset, err := mw.writeBox(mdat)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor i := range p.Tracks {\n\t\ttruns[i].DataOffset = int32(dataOffsets[i] + mdatOffset - moofOffset + 8)\n\t\terr = mw.rewriteBox(trunOffsets[i], truns[i])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "fmp4/muxer/part_sample.go",
    "content": "package muxer\n\nimport (\n\t\"time\"\n)\n\n// PartSample is a sample of a PartTrack.\ntype PartSample struct {\n\tDts             time.Duration\n\tDuration        uint32\n\tPTSOffset       int32\n\tIsNonSyncSample bool\n\tPayload         []byte\n}\n\nfunc avccMarshalSize(au [][]byte) int {\n\tn := 0\n\tfor _, nalu := range au {\n\t\tn += 4 + len(nalu)\n\t}\n\treturn n\n}\n\n// AVCCMarshal encodes an access unit into the AVCC stream format.\n// Specification: ISO 14496-15, section 5.3.4.2.1\nfunc AVCCMarshal(au [][]byte) ([]byte, error) {\n\tbuf := make([]byte, avccMarshalSize(au))\n\tpos := 0\n\n\tfor _, nalu := range au {\n\t\tnaluLen := len(nalu)\n\t\tbuf[pos] = byte(naluLen >> 24)\n\t\tbuf[pos+1] = byte(naluLen >> 16)\n\t\tbuf[pos+2] = byte(naluLen >> 8)\n\t\tbuf[pos+3] = byte(naluLen)\n\t\tpos += 4\n\n\t\tpos += copy(buf[pos:], nalu)\n\t}\n\n\treturn buf, nil\n}\n\n// NewPartSampleH26x creates a sample with H26x data.\nfunc NewPartSampleH26x(ptsOffset int32, randomAccessPresent bool, au [][]byte, duration uint32, dts time.Duration) *PartSample {\n\tavcc, err := AVCCMarshal(au)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\treturn &PartSample{\n\t\tDts:             dts,\n\t\tPTSOffset:       ptsOffset,\n\t\tIsNonSyncSample: !randomAccessPresent,\n\t\tPayload:         avcc,\n\t\tDuration:        duration,\n\t}\n}\n"
  },
  {
    "path": "fmp4/muxer/part_track.go",
    "content": "package muxer\n\nimport \"github.com/abema/go-mp4\"\n\n// PartTrack is a track of Part.\ntype PartTrack struct {\n\tID       int\n\tBaseTime uint64\n\tSamples  []*PartSample\n}\n\nfunc (pt *PartTrack) marshal(w *mp4Writer) (*mp4.Trun, int, error) {\n\t/*\n\t\t|traf|\n\t\t|    |tfhd|\n\t\t|    |tfdt|\n\t\t|    |trun|\n\t*/\n\n\t_, err := w.writeBoxStart(&mp4.Traf{}) // <traf>\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tflags := 0\n\n\t_, err = w.writeBox(&mp4.Tfhd{ // <tfhd/>\n\t\tFullBox: mp4.FullBox{\n\t\t\tFlags: [3]byte{2, byte(flags >> 8), byte(flags)},\n\t\t},\n\t\tTrackID: uint32(pt.ID),\n\t})\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\t_, err = w.writeBox(&mp4.Tfdt{ // <tfdt/>\n\t\tFullBox: mp4.FullBox{\n\t\t\tVersion: 1,\n\t\t},\n\t\t// sum of decode durations of all earlier samples\n\t\tBaseMediaDecodeTimeV1: pt.BaseTime,\n\t})\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tflags = trunFlagDataOffsetPreset |\n\t\ttrunFlagSampleDurationPresent |\n\t\ttrunFlagSampleSizePresent\n\n\tfor _, sample := range pt.Samples {\n\t\tif sample.IsNonSyncSample {\n\t\t\tflags |= trunFlagSampleFlagsPresent\n\t\t}\n\t\tif sample.PTSOffset != 0 {\n\t\t\tflags |= trunFlagSampleCompositionTimeOffsetPresentOrV1\n\t\t}\n\t}\n\n\ttrun := &mp4.Trun{ // <trun/>\n\t\tFullBox: mp4.FullBox{\n\t\t\tVersion: 1,\n\t\t\tFlags:   [3]byte{0, byte(flags >> 8), byte(flags)},\n\t\t},\n\t\tSampleCount: uint32(len(pt.Samples)),\n\t}\n\n\tfor _, sample := range pt.Samples {\n\t\tvar flags uint32\n\t\tif sample.IsNonSyncSample {\n\t\t\tflags |= sampleFlagIsNonSyncSample\n\t\t}\n\n\t\ttrun.Entries = append(trun.Entries, mp4.TrunEntry{\n\t\t\tSampleDuration:                sample.Duration,\n\t\t\tSampleSize:                    uint32(len(sample.Payload)),\n\t\t\tSampleFlags:                   flags,\n\t\t\tSampleCompositionTimeOffsetV1: sample.PTSOffset,\n\t\t})\n\t}\n\n\ttrunOffset, err := w.writeBox(trun)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\terr = w.writeBoxEnd() // </traf>\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn trun, trunOffset, nil\n}\n"
  },
  {
    "path": "fmp4/muxer/rtmp2fmp4.go",
    "content": "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\"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/lal/pkg/hevc\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\ntype IRtmp2Fmp4muxerObserver interface {\n\tOnInitFmp4(init []byte)\n\tOnFmp4Packets(currentPart *MuxerPart, lastSampleDuration time.Duration, end bool, isVideo bool)\n}\n\nvar waitHeaderQueueSize = 16\n\ntype Rtmp2Fmp4Remuxer struct {\n\tdata        []base.RtmpMsg\n\tdone        bool\n\tmaxMsgSize  int\n\tvCodec      Codec\n\taCodec      Codec\n\tmux         *Muxer\n\tobserver    IRtmp2Fmp4muxerObserver\n\tlog         nazalog.Logger\n\tnextPartId  uint64\n\tcurrentPart *MuxerPart\n}\n\nfunc NewRtmp2Fmp4Remuxer(observer IRtmp2Fmp4muxerObserver) *Rtmp2Fmp4Remuxer {\n\tm := &Rtmp2Fmp4Remuxer{\n\t\tmaxMsgSize: waitHeaderQueueSize,\n\t\tdata:       make([]base.RtmpMsg, waitHeaderQueueSize)[0:0],\n\t\tdone:       false,\n\t\tobserver:   observer,\n\t\tlog:        Log,\n\t}\n\n\tm.mux = NewMuxer()\n\tm.mux.WithLog(m.log)\n\n\treturn m\n}\n\nfunc (m *Rtmp2Fmp4Remuxer) WithLog(log nazalog.Logger) *Rtmp2Fmp4Remuxer {\n\tm.log = log\n\tm.mux.WithLog(m.log)\n\treturn m\n}\n\nfunc (m *Rtmp2Fmp4Remuxer) FeedRtmpMessage(msg base.RtmpMsg) {\n\tm.Push(msg)\n}\n\nfunc (m *Rtmp2Fmp4Remuxer) Push(msg base.RtmpMsg) {\n\tif msg.Header.MsgTypeId == base.RtmpTypeIdMetadata {\n\t\treturn\n\t}\n\n\tif m.done {\n\t\tm.pack(msg)\n\t\treturn\n\t}\n\n\tif msg.IsVideoKeySeqHeader() {\n\t\tswitch msg.VideoCodecId() {\n\t\tcase base.RtmpCodecIdAvc:\n\t\t\tif sps, pps, err := avc.ParseSpsPpsFromSeqHeader(msg.Payload); err != nil {\n\t\t\t\tm.log.Errorf(\"parse sps pps from seq header failed: %v\", err)\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\tm.vCodec = &CodecH264{\n\t\t\t\t\tSPS: sps,\n\t\t\t\t\tPPS: pps,\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase base.RtmpCodecIdHevc:\n\t\t\tvar vps, sps, pps []byte\n\t\t\tvar err error\n\n\t\t\tif msg.IsEnhanced() {\n\t\t\t\tvps, sps, pps, err = hevc.ParseVpsSpsPpsFromEnhancedSeqHeader(msg.Payload)\n\t\t\t\tif err != nil {\n\t\t\t\t\tnazalog.Error(\"ParseVpsSpsPpsFromEnhancedSeqHeader failed, err:\", err)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t} else {\n\t\t\t\tvps, sps, pps, err = hevc.ParseVpsSpsPpsFromSeqHeader(msg.Payload)\n\t\t\t\tif err != nil {\n\t\t\t\t\tnazalog.Error(\"ParseVpsSpsPpsFromSeqHeader failed, err:\", err)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tm.vCodec = &CodecH265{\n\t\t\t\tVPS: vps,\n\t\t\t\tSPS: sps,\n\t\t\t\tPPS: pps,\n\t\t\t}\n\n\t\tdefault:\n\t\t\tm.log.Errorf(\"unknown video codec id: %d\", msg.VideoCodecId())\n\t\t\treturn\n\t\t}\n\t}\n\n\tif msg.Header.MsgTypeId == base.RtmpTypeIdAudio {\n\t\tswitch msg.AudioCodecId() {\n\t\tcase base.RtmpSoundFormatAac:\n\t\t\tif msg.IsAacSeqHeader() {\n\t\t\t\tif ascCtx, err := aac.NewAscContext(msg.Payload[2:]); err != nil {\n\t\t\t\t\tm.log.Errorf(\"new asc context failed: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t} else {\n\t\t\t\t\tm.aCodec = &CodecAAC{\n\t\t\t\t\t\tCtx:     ascCtx,\n\t\t\t\t\t\tAscData: msg.Payload[2:],\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\tdefault:\n\t\t\treturn\n\t\t}\n\t}\n\n\tm.data = append(m.data, msg.Clone())\n\n\tif m.vCodec != nil && m.aCodec != nil {\n\t\tm.drain()\n\t\treturn\n\t}\n\n\tif len(m.data) >= m.maxMsgSize {\n\t\tm.drain()\n\t\treturn\n\t}\n}\n\nfunc (m *Rtmp2Fmp4Remuxer) drain() {\n\tif m.vCodec != nil {\n\t\tm.mux.AddVideoTrack(m.vCodec)\n\t}\n\n\tif m.aCodec != nil {\n\t\tm.mux.AddAudioTrack(m.aCodec)\n\t}\n\n\tinit := m.mux.GetInitMp4()\n\tif m.observer != nil {\n\t\tm.observer.OnInitFmp4(init)\n\t}\n\n\tfor i := range m.data {\n\t\tm.pack(m.data[i])\n\t}\n\n\tm.data = nil\n\tm.done = true\n}\n\nfunc (m *Rtmp2Fmp4Remuxer) FlushLastSegment() {\n\tif m.currentPart != nil {\n\t\tif err := m.currentPart.Encode(0, true); err == nil && m.observer != nil {\n\t\t\tm.observer.OnFmp4Packets(m.currentPart, 0, true, false)\n\t\t}\n\t}\n}\n\nfunc (m *Rtmp2Fmp4Remuxer) Dispose() {\n}\n\nfunc (m *Rtmp2Fmp4Remuxer) pack(msg base.RtmpMsg) {\n\tparamsChanged := false\n\tif m.done {\n\t\tif msg.IsVideoKeySeqHeader() {\n\t\t\tswitch msg.VideoCodecId() {\n\t\t\tcase base.RtmpCodecIdAvc:\n\t\t\t\tif sps, pps, err := avc.ParseSpsPpsFromSeqHeader(msg.Payload); err != nil {\n\t\t\t\t\tm.log.Errorf(\"parse sps pps from seq header failed: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t} else {\n\t\t\t\t\tcodec, ok := m.vCodec.(*CodecH264)\n\t\t\t\t\tif !ok || !bytes.Equal(codec.SPS, sps) || !bytes.Equal(codec.PPS, pps) {\n\t\t\t\t\t\told := m.vCodec\n\t\t\t\t\t\tm.vCodec = &CodecH264{\n\t\t\t\t\t\t\tSPS: sps,\n\t\t\t\t\t\t\tPPS: pps,\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tparamsChanged = true\n\t\t\t\t\t\tif old != nil && m.vCodec != nil {\n\t\t\t\t\t\t\tm.log.Infof(\"video codec changed, old:%s, new:%s\", old.String(), m.vCodec.String())\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\tcase base.RtmpCodecIdHevc:\n\t\t\t\tvar vps, sps, pps []byte\n\t\t\t\tvar err error\n\n\t\t\t\tif msg.IsEnhanced() {\n\t\t\t\t\tvps, sps, pps, err = hevc.ParseVpsSpsPpsFromEnhancedSeqHeader(msg.Payload)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tnazalog.Error(\"ParseVpsSpsPpsFromEnhancedSeqHeader failed, err:\", err)\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\n\t\t\t\t} else {\n\t\t\t\t\tvps, sps, pps, err = hevc.ParseVpsSpsPpsFromSeqHeader(msg.Payload)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tnazalog.Error(\"ParseVpsSpsPpsFromSeqHeader failed, err:\", err)\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tcodec, ok := m.vCodec.(*CodecH265)\n\t\t\t\tif !ok || !bytes.Equal(codec.VPS, vps) || !bytes.Equal(codec.SPS, sps) || !bytes.Equal(codec.PPS, pps) {\n\t\t\t\t\told := m.vCodec\n\t\t\t\t\tm.vCodec = &CodecH265{\n\t\t\t\t\t\tVPS: vps,\n\t\t\t\t\t\tSPS: sps,\n\t\t\t\t\t\tPPS: pps,\n\t\t\t\t\t}\n\n\t\t\t\t\tparamsChanged = true\n\t\t\t\t\tif old != nil && m.vCodec != nil {\n\t\t\t\t\t\tm.log.Infof(\"video codec changed, old:%s, new:%s\", old.String(), m.vCodec.String())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else if msg.Header.MsgTypeId == base.RtmpTypeIdAudio {\n\t\t\tif msg.IsAacSeqHeader() {\n\t\t\t\tif ascCtx, err := aac.NewAscContext(msg.Payload[2:]); err != nil {\n\t\t\t\t\tm.log.Errorf(\"new asc context failed: %v\", err)\n\t\t\t\t\treturn\n\t\t\t\t} else {\n\t\t\t\t\tcodec, ok := m.aCodec.(*CodecAAC)\n\t\t\t\t\tif !ok || !bytes.Equal(codec.AscData, msg.Payload[2:]) {\n\t\t\t\t\t\told := m.aCodec\n\n\t\t\t\t\t\tm.aCodec = &CodecAAC{\n\t\t\t\t\t\t\tCtx:     ascCtx,\n\t\t\t\t\t\t\tAscData: msg.Payload[2:],\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tparamsChanged = true\n\t\t\t\t\t\tif old != nil && m.aCodec != nil {\n\t\t\t\t\t\t\tm.log.Infof(\"audio codec changed, old:%s, new:%s\", old.String(), m.aCodec.String())\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif paramsChanged {\n\t\t\t// 编码格式发生变化，需要更新init和强制生成当前这个文件\n\t\t\tif m.currentPart != nil {\n\t\t\t\tif err := m.currentPart.Encode(0, true); err == nil && m.observer != nil {\n\t\t\t\t\tm.observer.OnFmp4Packets(m.currentPart, 0, true, false)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tm.mux = NewMuxer()\n\t\t\tm.mux.WithLog(m.log)\n\n\t\t\tif m.vCodec != nil {\n\t\t\t\tm.mux.AddVideoTrack(m.vCodec)\n\t\t\t}\n\n\t\t\tif m.aCodec != nil {\n\t\t\t\tm.mux.AddAudioTrack(m.aCodec)\n\t\t\t}\n\n\t\t\tinit := m.mux.GetInitMp4()\n\t\t\tif m.observer != nil {\n\t\t\t\tm.observer.OnInitFmp4(init)\n\t\t\t}\n\n\t\t\tm.currentPart = nil\n\t\t}\n\t}\n\n\tsample, err := m.mux.Pack(msg)\n\tif err == nil {\n\t\tif m.vCodec != nil {\n\t\t\t// 视频存在的话，I帧作为分割点\n\t\t\tif msg.IsVideoKeyNalu() {\n\t\t\t\tif m.currentPart == nil {\n\t\t\t\t\tm.currentPart = NewMuxerPart(m.partId(), m.mux.AudioTimeScale())\n\t\t\t\t} else {\n\t\t\t\t\tif len(m.currentPart.VideoSamples) >= 15 {\n\t\t\t\t\t\tif m.observer != nil {\n\t\t\t\t\t\t\tm.observer.OnFmp4Packets(m.currentPart, sample.Dts, false, true)\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tm.currentPart = NewMuxerPart(m.partId(), m.mux.AudioTimeScale())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif msg.Header.MsgTypeId == base.RtmpTypeIdVideo {\n\t\t\t\tm.currentPart.WriteVideo(sample)\n\t\t\t} else {\n\t\t\t\t// 防止起始是音频\n\t\t\t\tif m.currentPart == nil {\n\t\t\t\t\tm.currentPart = NewMuxerPart(m.partId(), m.mux.AudioTimeScale())\n\t\t\t\t}\n\n\t\t\t\tm.currentPart.WriteAudio(sample)\n\t\t\t}\n\t\t} else {\n\t\t\tif m.currentPart == nil {\n\t\t\t\tm.currentPart = NewMuxerPart(m.partId(), m.mux.AudioTimeScale())\n\t\t\t} else {\n\t\t\t\t// 只有音频的话，2s分割\n\t\t\t\tif m.currentPart.Duration() >= 2*time.Second {\n\t\t\t\t\tif m.observer != nil {\n\t\t\t\t\t\tm.observer.OnFmp4Packets(m.currentPart, sample.Dts, false, false)\n\t\t\t\t\t}\n\n\t\t\t\t\tm.currentPart = NewMuxerPart(m.partId(), m.mux.AudioTimeScale())\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tm.currentPart.WriteAudio(sample)\n\t\t}\n\t}\n}\n\nfunc (m *Rtmp2Fmp4Remuxer) partId() uint64 {\n\tid := m.nextPartId\n\tm.nextPartId++\n\treturn id\n}\n"
  },
  {
    "path": "fmp4/muxer/seekablebuffer.go",
    "content": "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 Buffer struct {\n\tbytes.Buffer\n\tpos int64\n}\n\n// Write implements io.Writer.\nfunc (b *Buffer) Write(p []byte) (int, error) {\n\tn := 0\n\n\tif b.pos < int64(b.Len()) {\n\t\tn = copy(b.Bytes()[b.pos:], p)\n\t\tp = p[n:]\n\t}\n\n\tif len(p) > 0 {\n\t\t// Buffer.Write can't return an error.\n\t\tnn, _ := b.Buffer.Write(p) //nolint:errcheck\n\t\tn += nn\n\t}\n\n\tb.pos += int64(n)\n\treturn n, nil\n}\n\n// Read implements io.Reader.\nfunc (b *Buffer) Read(_ []byte) (int, error) {\n\treturn 0, fmt.Errorf(\"unimplemented\")\n}\n\n// Seek implements io.Seeker.\nfunc (b *Buffer) Seek(offset int64, whence int) (int64, error) {\n\tpos2 := int64(0)\n\n\tswitch whence {\n\tcase io.SeekStart:\n\t\tpos2 = offset\n\n\tcase io.SeekCurrent:\n\t\tpos2 = b.pos + offset\n\n\tcase io.SeekEnd:\n\t\tpos2 = int64(b.Len()) + offset\n\t}\n\n\tif pos2 < 0 {\n\t\treturn 0, fmt.Errorf(\"negative position\")\n\t}\n\n\tb.pos = pos2\n\n\tdiff := b.pos - int64(b.Len())\n\tif diff > 0 {\n\t\t// Buffer.Write can't return an error.\n\t\tb.Buffer.Write(make([]byte, diff)) //nolint:errcheck\n\t}\n\n\treturn pos2, nil\n}\n\n// Reset resets the buffer state.\nfunc (b *Buffer) Reset() {\n\tb.Buffer.Reset()\n\tb.pos = 0\n}\n"
  },
  {
    "path": "fmp4/muxer/track.go",
    "content": "package muxer\n\ntype Track struct {\n\tCodec\n\tTrackId   uint32\n\ttimeScale uint32\n\tfirstDTS  int64\n\tlastDTS   int64\n\tsamples   []PartSample\n}\n\nfunc NewTrack(codec Codec, trackId, timeSacle uint32) *Track {\n\treturn &Track{\n\t\tCodec:     codec,\n\t\tTrackId:   trackId,\n\t\ttimeScale: timeSacle,\n\t\tfirstDTS:  -1,\n\t}\n}\n"
  },
  {
    "path": "fmp4/muxer/var.go",
    "content": "package muxer\n\nimport \"github.com/q191201771/naza/pkg/nazalog\"\n\nvar Log = nazalog.GetGlobalLogger()\n"
  },
  {
    "path": "gb28181/auth.go",
    "content": "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/nazalog\"\n)\n\ntype Authorization struct {\n\t*sip.Authorization\n}\n\nfunc (a *Authorization) Verify(username, passwd, realm, nonce string) bool {\n\n\t//1、将 username,realm,password 依次组合获取 1 个字符串，并用算法加密的到密文 r1\n\ts1 := fmt.Sprintf(\"%s:%s:%s\", username, realm, passwd)\n\tr1 := a.getDigest(s1)\n\t//2、将 method，即REGISTER ,uri 依次组合获取 1 个字符串，并对这个字符串使用算法 加密得到密文 r2\n\ts2 := fmt.Sprintf(\"REGISTER:%s\", a.Uri())\n\tr2 := a.getDigest(s2)\n\n\tif r1 == \"\" || r2 == \"\" {\n\t\tnazalog.Error(\"Authorization algorithm wrong\")\n\t\treturn false\n\t}\n\t//3、将密文 1，nonce 和密文 2 依次组合获取 1 个字符串，并对这个字符串使用算法加密，获得密文 r3，即Response\n\ts3 := fmt.Sprintf(\"%s:%s:%s\", r1, nonce, r2)\n\tr3 := a.getDigest(s3)\n\n\t//4、计算服务端和客户端上报的是否相等\n\treturn r3 == a.Response()\n}\n\nfunc (a *Authorization) getDigest(raw string) string {\n\tswitch a.Algorithm() {\n\tcase \"MD5\":\n\t\treturn fmt.Sprintf(\"%x\", md5.Sum([]byte(raw)))\n\tdefault: //如果没有算法，默认使用MD5\n\t\treturn fmt.Sprintf(\"%x\", md5.Sum([]byte(raw)))\n\t}\n}\n"
  },
  {
    "path": "gb28181/avail_conn_pool.go",
    "content": "// Copyright 2020, Chef.  All rights reserved.\n// https://github.com/q191201771/naza\n//\n// Use of this source code is governed by a MIT-style license\n// that can be found in the License file.\n//\n// Author: Chef (191201771@qq.com)\n//根据naza修改，新增tcp\n\npackage gb28181\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"sync\"\n)\n\nvar ErrNazaNet = errors.New(\"gb28181: fxxk\")\n\ntype OnListenWithPort func(port uint16) (net.Listener, error)\n\n// 从指定的端口范围内，寻找可绑定监听的端口，绑定监听并返回\ntype AvailConnPool struct {\n\tminPort uint16\n\tmaxPort uint16\n\n\tm                sync.Mutex\n\tlastPort         uint16\n\tonListenWithPort OnListenWithPort\n}\n\nfunc NewAvailConnPool(minPort uint16, maxPort uint16) *AvailConnPool {\n\treturn &AvailConnPool{\n\t\tminPort:  minPort,\n\t\tmaxPort:  maxPort,\n\t\tlastPort: minPort,\n\t}\n}\nfunc (a *AvailConnPool) WithListenWithPort(listenWithPort OnListenWithPort) {\n\ta.onListenWithPort = listenWithPort\n}\nfunc (a *AvailConnPool) Acquire() (net.Listener, uint16, error) {\n\ta.m.Lock()\n\tdefer a.m.Unlock()\n\n\tloopFirstFlag := true\n\tp := a.lastPort\n\tfor {\n\t\t// 找了一轮也没有可用的，返回错误\n\t\tif !loopFirstFlag && p == a.lastPort {\n\t\t\treturn nil, 0, ErrNazaNet\n\t\t}\n\t\tloopFirstFlag = false\n\t\tif a.onListenWithPort == nil {\n\t\t\treturn nil, 0, ErrNazaNet\n\t\t}\n\t\tlistener, err := a.onListenWithPort(p)\n\n\t\t// 绑定失败，尝试下一个端口\n\t\tif err != nil {\n\t\t\tp = a.nextPort(p)\n\t\t\tcontinue\n\t\t}\n\n\t\t// 绑定成功，更新last，返回结果\n\t\ta.lastPort = a.nextPort(p)\n\t\treturn listener, p, nil\n\t}\n}\n\n// 通过Acquire获取到可用net.UDPConn对象后，将对象关闭，只返回可用的端口\nfunc (a *AvailConnPool) Peek() (uint16, error) {\n\tconn, port, err := a.Acquire()\n\tif err == nil {\n\t\terr = conn.Close()\n\t}\n\treturn port, err\n}\nfunc (a *AvailConnPool) ListenWithPort(port uint16) (net.Listener, error) {\n\tif a.onListenWithPort == nil {\n\t\treturn nil, ErrNazaNet\n\t}\n\treturn a.onListenWithPort(port)\n}\nfunc (a *AvailConnPool) nextPort(p uint16) uint16 {\n\tif p == a.maxPort {\n\t\treturn a.minPort\n\t}\n\n\treturn p + 1\n}\n"
  },
  {
    "path": "gb28181/channel.go",
    "content": "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/nazaatomic\"\n\n\tconfig \"github.com/q191201771/lalmax/config\"\n\t\"github.com/q191201771/lalmax/gb28181/mediaserver\"\n\n\t\"github.com/ghettovoice/gosip/sip\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\ntype Channel struct {\n\tdevice *Device // 所属设备\n\t//status  atomic.Int32 // 通道状态,0:空闲,1:正在invite,2:正在播放\n\tGpsTime time.Time // gps时间\n\tnumber  uint16\n\tackReq  sip.Request\n\n\tobserver IMediaOpObserver\n\tplayInfo *PlayInfo\n\n\tChannelInfo\n\tconf config.GB28181Config\n}\n\n// Channel 通道\ntype ChannelInfo struct {\n\tChannelId    string        `xml:\"DeviceID\"`     // 设备id\n\tParentId     string        `xml:\"ParentID\"`     //父目录Id\n\tName         string        `xml:\"Name\"`         //设备名称\n\tManufacturer string        `xml:\"Manufacturer\"` //制造厂商\n\tModel        string        `xml:\"Model\"`        //型号\n\tOwner        string        `xml:\"Owner\"`        //设备归属\n\tCivilCode    string        `xml:\"CivilCode\"`    //行政区划编码\n\tAddress      string        `xml:\"Address\"`      //地址\n\tPort         int           `xml:\"Port\"`         //端口\n\tParental     int           `xml:\"Parental\"`     //存在子设备，这里表明有子目录存在 1代表有子目录，0表示没有\n\tSafetyWay    int           `xml:\"SafetyWay\"`    //信令安全模式（可选）缺省为 0；0：不采用；2：S/MIME 签名方式；3：S/MIME\t加密签名同时采用方式；4：数字摘要方式\n\tRegisterWay  int           `xml:\"RegisterWay\"`  //标准的认证注册模式\n\tSecrecy      int           `xml:\"Secrecy\"`      //0 表示不涉密\n\tStatus       ChannelStatus `xml:\"Status\"`       // 状态  on 在线 off离线\n\tLongitude    string        `xml:\"Longitude\"`    // 经度\n\tLatitude     string        `xml:\"Latitude\"`     // 纬度\n\tStreamName   string        `xml:\"-\"`\n\tserial       string\n\tmediaserver.MediaInfo\n\tsn nazaatomic.Uint32\n}\n\ntype ChannelStatus string\n\nconst (\n\tChannelOnStatus  = \"ON\"\n\tChannelOffStatus = \"OFF\"\n)\n\nfunc (channel *Channel) WithMediaServer(observer IMediaOpObserver) {\n\tchannel.observer = observer\n}\n\nfunc (channel *Channel) TryAutoInvite(opt *InviteOptions, streamName string, playInfo *PlayInfo) {\n\tif channel.CanInvite(streamName) {\n\t\tgo channel.Invite(opt, streamName, playInfo)\n\t}\n}\n\nfunc (channel *Channel) CanInvite(streamName string) bool {\n\tif len(channel.ChannelId) != 20 || channel.Status == ChannelOffStatus {\n\t\tnazalog.Info(\"return false,  channel.DeviceID:\", len(channel.ChannelId), \" channel.Status:\", channel.Status)\n\t\treturn false\n\t}\n\tif channel.Parental != 0 {\n\t\treturn false\n\t}\n\n\tif channel.MediaInfo.IsInvite {\n\t\treturn false\n\t}\n\n\t// 11～13位是设备类型编码\n\ttypeID := channel.ChannelId[10:13]\n\tif typeID == \"132\" || typeID == \"131\" {\n\t\treturn true\n\t}\n\n\tnazalog.Info(\"return false\")\n\n\treturn false\n}\n\n// Invite 发送Invite报文 invites a channel to play\n// 注意里面的锁保证不同时发送invite报文，该锁由channel持有\n/***\nf字段： f = v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率\n各项具体含义：\n    v：后续参数为视频的参数；各参数间以 “/”分割；\n编码格式：十进制整数字符串表示\n1 –MPEG-4 2 –H.264 3 – SVAC 4 –3GP\n    分辨率：十进制整数字符串表示\n1 – QCIF 2 – CIF 3 – 4CIF 4 – D1 5 –720P 6 –1080P/I\n帧率：十进制整数字符串表示 0～99\n码率类型：十进制整数字符串表示\n1 – 固定码率（CBR）     2 – 可变码率（VBR）\n码率大小：十进制整数字符串表示 0～100000（如 1表示1kbps）\n    a：后续参数为音频的参数；各参数间以 “/”分割；\n编码格式：十进制整数字符串表示\n1 – G.711    2 – G.723.1     3 – G.729      4 – G.722.1\n码率大小：十进制整数字符串\n音频编码码率： 1 — 5.3 kbps （注：G.723.1中使用）\n   2 — 6.3 kbps （注：G.723.1中使用）\n   3 — 8 kbps （注：G.729中使用）\n   4 — 16 kbps （注：G.722.1中使用）\n   5 — 24 kbps （注：G.722.1中使用）\n   6 — 32 kbps （注：G.722.1中使用）\n   7 — 48 kbps （注：G.722.1中使用）\n   8 — 64 kbps（注：G.711中使用）\n采样率：十进制整数字符串表示\n\t1 — 8 kHz（注：G.711/ G.723.1/ G.729中使用）\n\t2—14 kHz（注：G.722.1中使用）\n\t3—16 kHz（注：G.722.1中使用）\n\t4—32 kHz（注：G.722.1中使用）\n\t注1：字符串说明\n本节中使用的“十进制整数字符串”的含义为“0”～“4294967296” 之间的十进制数字字符串。\n注2：参数分割标识\n各参数间以“/”分割，参数间的分割符“/”不能省略；\n若两个分割符 “/”间的某参数为空时（即两个分割符 “/”直接将相连时）表示无该参数值；\n注3：f字段说明\n使用f字段时，应保证视频和音频参数的结构完整性，即在任何时候，f字段的结构都应是完整的结构：\nf = v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率\n若只有视频时，音频中的各参数项可以不填写，但应保持 “a///”的结构:\nf = v/编码格式/分辨率/帧率/码率类型/码率大小a///\n若只有音频时也类似处理，视频中的各参数项可以不填写，但应保持 “v/”的结构：\nf = v/a/编码格式/码率大小/采样率\nf字段中视、音频参数段之间不需空格分割。\n可使用f字段中的分辨率参数标识同一设备不同分辨率的码流。\n*/\n\nfunc (channel *Channel) Invite(opt *InviteOptions, streamName string, playInfo *PlayInfo) (code int, err error) {\n\td := channel.device\n\ts := \"Play\"\n\n\t//然后按顺序生成，一个channel最大999 方便排查问题,也能保证唯一性\n\tchannel.number++\n\tif channel.number > 999 {\n\t\tchannel.number = 1\n\t}\n\tif len(channel.serial) == 0 {\n\t\tchannel.serial = RandNumString(6)\n\t}\n\topt.CreateSSRC(channel.serial, channel.number)\n\n\tvar mediaserver *mediaserver.GB28181MediaServer\n\tif channel.observer != nil {\n\t\tmediaserver = channel.observer.OnStartMediaServer(playInfo.NetWork, playInfo.SinglePort, channel.device.ID, channel.ChannelId)\n\t}\n\tif mediaserver == nil {\n\t\treturn http.StatusNotFound, err\n\t}\n\n\tprotocol := \"\"\n\tif playInfo.NetWork == \"tcp\" {\n\t\topt.MediaPort = mediaserver.GetListenerPort()\n\t\tprotocol = \"TCP/\"\n\t} else {\n\t\topt.MediaPort = mediaserver.GetListenerPort()\n\t}\n\n\tsdpInfo := []string{\n\t\t\"v=0\",\n\t\tfmt.Sprintf(\"o=%s 0 0 IN IP4 %s\", channel.ChannelId, d.mediaIP),\n\t\t\"s=\" + s,\n\t\t\"c=IN IP4 \" + d.mediaIP,\n\t\topt.String(),\n\t\tfmt.Sprintf(\"m=video %d %sRTP/AVP 96\", opt.MediaPort, protocol),\n\t\t\"a=recvonly\",\n\t\t\"a=rtpmap:96 PS/90000\",\n\t\t\"y=\" + opt.ssrc,\n\t}\n\n\tif playInfo.NetWork == \"tcp\" {\n\t\tsdpInfo = append(sdpInfo, \"a=setup:passive\", \"a=connection:new\")\n\t}\n\n\tinvite := channel.CreateRequst(sip.INVITE, channel.conf)\n\tcontentType := sip.ContentType(\"application/sdp\")\n\tinvite.AppendHeader(&contentType)\n\n\tcontentLength := sip.ContentLength(len(sdpInfo))\n\tinvite.AppendHeader(&contentLength)\n\n\tinvite.SetBody(strings.Join(sdpInfo, \"\\r\\n\")+\"\\r\\n\", true)\n\n\tsubject := sip.GenericHeader{\n\t\tHeaderName: \"Subject\", Contents: fmt.Sprintf(\"%s:%s,%s:0\", channel.ChannelId, opt.ssrc, channel.conf.Serial),\n\t}\n\tinvite.AppendHeader(&subject)\n\tinviteRes, err := d.SipRequestForResponse(invite)\n\tif err != nil {\n\t\tnazalog.Error(\"invite failed, err:\", err, \" invite msg:\", invite.String())\n\n\t\t//jay 在media端口监听成功后，但是sip发送失败时\n\t\tif channel.observer != nil {\n\t\t\tif err = channel.observer.OnStopMediaServer(playInfo.NetWork, playInfo.SinglePort, channel.device.ID, channel.ChannelId, \"\"); err != nil {\n\t\t\t\tnazalog.Errorf(\"gb28181 MediaServer stop err:%s\", err.Error())\n\t\t\t}\n\t\t}\n\n\t\treturn http.StatusInternalServerError, err\n\t}\n\tcode = int(inviteRes.StatusCode())\n\tif code == http.StatusOK {\n\t\tds := strings.Split(inviteRes.Body(), \"\\r\\n\")\n\t\tfor _, l := range ds {\n\t\t\tif ls := strings.Split(l, \"=\"); len(ls) > 1 {\n\t\t\t\tif ls[0] == \"y\" && len(ls[1]) > 0 {\n\t\t\t\t\tif _ssrc, err := strconv.ParseInt(ls[1], 10, 0); err == nil {\n\t\t\t\t\t\topt.SSRC = uint32(_ssrc)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tnazalog.Error(\"parse invite response y failed, err:\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif ls[0] == \"m\" && len(ls[1]) > 0 {\n\t\t\t\t\tnetinfo := strings.Split(ls[1], \" \")\n\t\t\t\t\tif strings.ToUpper(netinfo[2]) == \"TCP/RTP/AVP\" {\n\t\t\t\t\t\tnazalog.Info(\"Device support tcp\")\n\t\t\t\t\t} else {\n\t\t\t\t\t\tnazalog.Info(\"Device not support tcp\")\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tchannel.MediaInfo.IsInvite = true\n\t\tchannel.MediaInfo.Ssrc = opt.SSRC\n\t\tchannel.MediaInfo.StreamName = streamName\n\t\tchannel.MediaInfo.MediaKey = fmt.Sprintf(\"%s%d\", playInfo.NetWork, mediaserver.GetListenerPort())\n\n\t\tackReq := sip.NewAckRequest(\"\", invite, inviteRes, \"\", nil)\n\t\t//保存一下播放信息\n\t\tchannel.ackReq = ackReq\n\t\tchannel.playInfo = playInfo\n\n\t\terr = channel.device.sipSvr.Send(ackReq)\n\t} else {\n\n\t\tif channel.observer != nil {\n\t\t\tif err = channel.observer.OnStopMediaServer(playInfo.NetWork, playInfo.SinglePort, channel.device.ID, channel.ChannelId, \"\"); err != nil {\n\t\t\t\tnazalog.Errorf(\"gb28181 MediaServer stop err:%s\", err.Error())\n\t\t\t}\n\t\t}\n\n\t}\n\treturn\n}\nfunc (channel *Channel) GetCallId() string {\n\tif channel.ackReq != nil {\n\t\tif callId, ok := channel.ackReq.CallID(); ok {\n\t\t\treturn callId.Value()\n\t\t}\n\t}\n\treturn \"\"\n}\nfunc (channel *Channel) stopMediaServer() (err error) {\n\tif channel.playInfo != nil {\n\t\tif channel.observer != nil {\n\t\t\tif err = channel.observer.OnStopMediaServer(channel.playInfo.NetWork, channel.playInfo.SinglePort, channel.device.ID, channel.ChannelId, channel.playInfo.StreamName); err != nil {\n\t\t\t\tnazalog.Errorf(\"gb28181 MediaServer stop err:%s\", err.Error())\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\nfunc (channel *Channel) byeClear() (err error) {\n\terr = channel.stopMediaServer()\n\tchannel.ackReq = nil\n\tchannel.MediaInfo.Clear()\n\treturn\n}\nfunc (channel *Channel) Bye(streamName string) (err error) {\n\tif channel.ackReq != nil {\n\t\tbyeReq := channel.ackReq\n\t\tchannel.ackReq = nil\n\t\tbyeReq.SetMethod(sip.BYE)\n\t\tseq, _ := byeReq.CSeq()\n\t\tseq.SeqNo += 1\n\t\tchannel.device.sipSvr.Send(byeReq)\n\t} else {\n\t\terr = errors.New(\"channel has been closed\")\n\t}\n\tchannel.stopMediaServer()\n\treturn err\n}\nfunc (channel *Channel) CreateRequst(Method sip.RequestMethod, conf config.GB28181Config) (req sip.Request) {\n\td := channel.device\n\td.sn++\n\n\tcallId := sip.CallID(RandNumString(10))\n\tuserAgent := sip.UserAgentHeader(\"LALMax\")\n\tmaxForwards := sip.MaxForwards(70) //增加max-forwards为默认值 70\n\tcseq := sip.CSeq{\n\t\tSeqNo:      uint32(d.sn),\n\t\tMethodName: Method,\n\t}\n\tport := sip.Port(conf.SipPort)\n\tserverAddr := sip.Address{\n\t\tUri: &sip.SipUri{\n\t\t\tFUser: sip.String{Str: conf.Serial},\n\t\t\tFHost: d.sipIP,\n\t\t\tFPort: &port,\n\t\t},\n\t\tParams: sip.NewParams().Add(\"tag\", sip.String{Str: RandNumString(9)}),\n\t}\n\t//非同一域的目标地址需要使用@host\n\thost := conf.Realm\n\tif channel.ChannelId[0:9] != host {\n\t\tif channel.Port != 0 {\n\t\t\tdeviceIp := d.NetAddr\n\t\t\tdeviceIp = deviceIp[0:strings.LastIndex(deviceIp, \":\")]\n\t\t\thost = fmt.Sprintf(\"%s:%d\", deviceIp, channel.Port)\n\t\t} else {\n\t\t\thost = d.NetAddr\n\t\t}\n\t}\n\n\tchannelAddr := sip.Address{\n\t\tUri: &sip.SipUri{FUser: sip.String{Str: channel.ChannelId}, FHost: host},\n\t}\n\treq = sip.NewRequest(\n\t\t\"\",\n\t\tMethod,\n\t\tchannelAddr.Uri,\n\t\t\"SIP/2.0\",\n\t\t[]sip.Header{\n\t\t\tserverAddr.AsFromHeader(),\n\t\t\tchannelAddr.AsToHeader(),\n\t\t\t&callId,\n\t\t\t&userAgent,\n\t\t\t&cseq,\n\t\t\t&maxForwards,\n\t\t\tserverAddr.AsContactHeader(),\n\t\t},\n\t\t\"\",\n\t\tnil,\n\t)\n\n\treq.SetTransport(channel.device.network)\n\treq.SetDestination(d.NetAddr)\n\treturn req\n}\nfunc (channel *Channel) PtzDirection(direction *PtzDirection) error {\n\tptz := Ptz{\n\t\tZoomOut: false,\n\t\tZoomIn:  false,\n\t\tUp:      direction.Up,\n\t\tDown:    direction.Down,\n\t\tLeft:    direction.Left,\n\t\tRight:   direction.Right,\n\t\tSpeed:   direction.Speed,\n\t}\n\tmsgPtz := &MessagePtz{\n\t\tCmdType:  DeviceControl,\n\t\tDeviceID: direction.ChannelId,\n\t\tSN:       int(channel.sn.Add(1)),\n\t\tPTZCmd:   ptz.Pack(),\n\t}\n\txml, err := XmlEncode(msgPtz)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn channel.sipMessage(xml)\n}\nfunc (channel *Channel) PtzZoom(zoom *PtzZoom) error {\n\tptz := Ptz{\n\t\tZoomOut: zoom.ZoomOut,\n\t\tZoomIn:  zoom.ZoomIn,\n\t\tSpeed:   zoom.Speed,\n\t}\n\tmsgPtz := &MessagePtz{\n\t\tCmdType:  DeviceControl,\n\t\tDeviceID: zoom.ChannelId,\n\t\tSN:       int(channel.sn.Add(1)),\n\t\tPTZCmd:   ptz.Pack(),\n\t}\n\txml, err := XmlEncode(msgPtz)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn channel.sipMessage(xml)\n}\nfunc (channel *Channel) PtzFi(fi *PtzFi) error {\n\tptzFi := Fi{\n\t\tIrisIn:    fi.IrisIn,\n\t\tIrisOut:   fi.IrisOut,\n\t\tFocusNear: fi.FocusNear,\n\t\tFocusFar:  fi.FocusFar,\n\t\tSpeed:     fi.Speed,\n\t}\n\tmsgPtz := &MessagePtz{\n\t\tCmdType:  DeviceControl,\n\t\tDeviceID: fi.ChannelId,\n\t\tSN:       int(channel.sn.Add(1)),\n\t\tPTZCmd:   ptzFi.Pack(),\n\t}\n\txml, err := XmlEncode(msgPtz)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn channel.sipMessage(xml)\n}\nfunc (channel *Channel) PtzPreset(ptzPreset *PtzPreset) error {\n\tcmd := byte(PresetSet)\n\tswitch ptzPreset.Cmd {\n\tcase PresetEditPoint:\n\t\tcmd = PresetSet\n\tcase PresetDelPoint:\n\t\tcmd = PresetDel\n\tcase PresetCallPoint:\n\t\tcmd = PresetCall\n\tdefault:\n\t\treturn errors.New(fmt.Sprintf(\"ptz preset cmd error:%d\", ptzPreset.Cmd))\n\t}\n\tpreset := Preset{\n\t\tCMD:   cmd,\n\t\tPoint: ptzPreset.Point,\n\t}\n\tmsgPtz := &MessagePtz{\n\t\tCmdType:  DeviceControl,\n\t\tDeviceID: ptzPreset.ChannelId,\n\t\tSN:       int(channel.sn.Add(1)),\n\t\tPTZCmd:   preset.Pack(),\n\t}\n\txml, err := XmlEncode(msgPtz)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn channel.sipMessage(xml)\n}\nfunc (channel *Channel) PtzStop(stop *PtzStop) error {\n\tptz := Ptz{}\n\tmsgPtz := &MessagePtz{\n\t\tCmdType:  DeviceControl,\n\t\tDeviceID: stop.ChannelId,\n\t\tSN:       int(channel.sn.Add(1)),\n\t\tPTZCmd:   ptz.Pack(),\n\t}\n\txml, err := XmlEncode(msgPtz)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn channel.sipMessage(xml)\n}\nfunc (channel *Channel) sipMessage(xml string) error {\n\td := channel.device\n\tmsg := channel.CreateRequst(sip.MESSAGE, channel.conf)\n\tmsg.AppendHeader(&sip.GenericHeader{HeaderName: \"Content-Type\", Contents: \"Application/MANSCDP+xml\"})\n\tmsg.SetBody(xml, true)\n\tmsgRes, err := d.SipRequestForResponse(msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcode := int(msgRes.StatusCode())\n\tif code == http.StatusOK {\n\t\treturn nil\n\t} else {\n\t\treturn errors.New(fmt.Sprintf(\"sip message ptz fail,code:%d\", code))\n\t}\n}\n"
  },
  {
    "path": "gb28181/device.go",
    "content": "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 \"github.com/q191201771/lalmax/config\"\n\n\t\"github.com/ghettovoice/gosip/sip\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\nconst TIME_LAYOUT = \"2006-01-02T15:04:05\"\n\nvar (\n\tDevices             sync.Map\n\tDeviceNonce         sync.Map //保存nonce防止设备伪造\n\tDeviceRegisterCount sync.Map //设备注册次数\n)\n\ntype DeviceStatus string\n\nconst (\n\tDeviceRegisterStatus = \"REGISTER\"\n\tDeviceRecoverStatus  = \"RECOVER\"\n\tDeviceOnlineStatus   = \"ONLINE\"\n\tDeviceOfflineStatus  = \"OFFLINE\"\n\tDeviceAlarmedStatus  = \"ALARMED\"\n)\n\ntype Device struct {\n\tID              string\n\tName            string\n\tManufacturer    string\n\tModel           string\n\tOwner           string\n\tRegisterTime    time.Time\n\tUpdateTime      time.Time\n\tLastKeepaliveAt time.Time\n\tStatus          DeviceStatus\n\tsn              int\n\taddr            sip.Address\n\tsipIP           string //设备对应网卡的服务器ip\n\tmediaIP         string //设备对应网卡的服务器ip\n\tNetAddr         string\n\tchannelMap      sync.Map\n\tsubscriber      struct {\n\t\tCallID  string\n\t\tTimeout time.Time\n\t}\n\tlastSyncTime time.Time\n\tGpsTime      time.Time //gps时间\n\tLongitude    string    //经度\n\tLatitude     string    //纬度\n\n\tobserver IMediaOpObserver\n\tconf     config.GB28181Config\n\n\tnetwork string\n\tsipSvr  gosip.Server\n}\n\nfunc (d *Device) WithMediaServer(observer IMediaOpObserver) {\n\td.observer = observer\n}\n\nfunc (d *Device) WithSipSvr(sipSvr gosip.Server) *Device {\n\td.sipSvr = sipSvr\n\treturn d\n}\n\nfunc (d *Device) syncChannels() {\n\tif time.Since(d.lastSyncTime) > 2*time.Second {\n\t\td.lastSyncTime = time.Now()\n\t\td.Catalog(d.conf)\n\t\t//d.Subscribe(conf)\n\t\t//d.QueryDeviceInfo(conf)\n\t}\n}\n\nfunc (d *Device) UpdateChannels(list ...ChannelInfo) {\n\tfor _, c := range list {\n\t\t//当父设备非空且存在时、父设备节点增加通道\n\t\tif c.ParentId != \"\" {\n\t\t\tpath := strings.Split(c.ParentId, \"/\")\n\t\t\tparentId := path[len(path)-1]\n\t\t\t//如果父ID并非本身所属设备，一般情况下这是因为下级设备上传了目录信息，该信息通常不需要处理。\n\t\t\t// 暂时不考虑级联目录的实现\n\t\t\tif d.ID != parentId {\n\t\t\t\tif v, ok := Devices.Load(parentId); ok {\n\t\t\t\t\tparent := v.(*Device)\n\t\t\t\t\tparent.addOrUpdateChannel(c)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tc.ParentId = d.ID\n\t\t//本设备增加通道\n\t\td.addOrUpdateChannel(c)\n\t\t//channel.TryAutoInvite(&InviteOptions{}, conf)\n\t}\n}\n\nfunc (d *Device) addOrUpdateChannel(info ChannelInfo) (c *Channel) {\n\tif old, ok := d.channelMap.Load(info.ChannelId); ok {\n\t\tc = old.(*Channel)\n\t\tc.ChannelInfo = info\n\t} else {\n\t\tc = &Channel{\n\t\t\tdevice:      d,\n\t\t\tChannelInfo: info,\n\t\t\tconf:        d.conf,\n\t\t}\n\t\tc.WithMediaServer(d.observer)\n\t\td.channelMap.Store(info.ChannelId, c)\n\t}\n\treturn\n}\n\nfunc (d *Device) Catalog(conf config.GB28181Config) int {\n\trequest := d.CreateRequest(sip.MESSAGE, conf)\n\texpires := sip.Expires(3600)\n\td.subscriber.Timeout = time.Now().Add(time.Second * time.Duration(expires))\n\tcontentType := sip.ContentType(\"Application/MANSCDP+xml\")\n\n\trequest.AppendHeader(&contentType)\n\trequest.AppendHeader(&expires)\n\trequest.SetBody(BuildCatalogXML(d.sn, d.ID), true)\n\t// 输出Sip请求设备通道信息信令\n\tnazalog.Info(\"SIP->Catalog request:\", request.String())\n\n\tresp, err := d.SipRequestForResponse(request)\n\tif err == nil && resp != nil {\n\t\tnazalog.Info(\"SIP->Catalog Response:\", resp.String())\n\t\treturn int(resp.StatusCode())\n\t} else if err != nil {\n\t\tnazalog.Error(\"SIP<-Catalog error:\", err)\n\t}\n\treturn http.StatusRequestTimeout\n}\n\nfunc (d *Device) CreateRequest(Method sip.RequestMethod, conf config.GB28181Config) (req sip.Request) {\n\td.sn++\n\n\tcallId := sip.CallID(RandNumString(10))\n\tuserAgent := sip.UserAgentHeader(\"LALMax\")\n\tmaxForwards := sip.MaxForwards(70) //增加max-forwards为默认值 70\n\tcseq := sip.CSeq{\n\t\tSeqNo:      uint32(d.sn),\n\t\tMethodName: Method,\n\t}\n\tport := sip.Port(conf.SipPort)\n\tserverAddr := sip.Address{\n\t\tUri: &sip.SipUri{\n\t\t\tFUser: sip.String{Str: conf.Serial},\n\t\t\tFHost: d.sipIP,\n\t\t\tFPort: &port,\n\t\t},\n\t\tParams: sip.NewParams().Add(\"tag\", sip.String{Str: RandNumString(9)}),\n\t}\n\treq = sip.NewRequest(\n\t\t\"\",\n\t\tMethod,\n\t\td.addr.Uri,\n\t\t\"SIP/2.0\",\n\t\t[]sip.Header{\n\t\t\tserverAddr.AsFromHeader(),\n\t\t\td.addr.AsToHeader(),\n\t\t\t&callId,\n\t\t\t&userAgent,\n\t\t\t&cseq,\n\t\t\t&maxForwards,\n\t\t\tserverAddr.AsContactHeader(),\n\t\t},\n\t\t\"\",\n\t\tnil,\n\t)\n\n\treq.SetTransport(d.network)\n\treq.SetDestination(d.NetAddr)\n\treturn\n}\n\nfunc (d *Device) Subscribe(conf config.GB28181Config) int {\n\trequest := d.CreateRequest(sip.SUBSCRIBE, conf)\n\tif d.subscriber.CallID != \"\" {\n\t\tcallId := sip.CallID(RandNumString(10))\n\t\trequest.AppendHeader(&callId)\n\t}\n\texpires := sip.Expires(3600)\n\td.subscriber.Timeout = time.Now().Add(time.Second * time.Duration(expires))\n\tcontentType := sip.ContentType(\"Application/MANSCDP+xml\")\n\trequest.AppendHeader(&contentType)\n\trequest.AppendHeader(&expires)\n\n\trequest.SetBody(BuildCatalogXML(d.sn, d.ID), true)\n\n\tresponse, err := d.SipRequestForResponse(request)\n\tif err == nil && response != nil {\n\t\tif response.StatusCode() == http.StatusOK {\n\t\t\tcallId, _ := request.CallID()\n\t\t\td.subscriber.CallID = string(*callId)\n\t\t} else {\n\t\t\td.subscriber.CallID = \"\"\n\t\t}\n\t\treturn int(response.StatusCode())\n\t}\n\treturn http.StatusRequestTimeout\n}\n\nfunc (d *Device) QueryDeviceInfo(conf config.GB28181Config) {\n\tfor i := time.Duration(5); i < 100; i++ {\n\n\t\ttime.Sleep(time.Second * i)\n\t\trequest := d.CreateRequest(sip.MESSAGE, conf)\n\t\tcontentType := sip.ContentType(\"Application/MANSCDP+xml\")\n\t\trequest.AppendHeader(&contentType)\n\t\trequest.SetBody(BuildDeviceInfoXML(d.sn, d.ID), true)\n\n\t\tresponse, _ := d.SipRequestForResponse(request)\n\t\tif response != nil {\n\t\t\tif response.StatusCode() == http.StatusOK {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\n// UpdateChannelStatus 目录订阅消息处理：新增/移除/更新通道或者更改通道状态\nfunc (d *Device) UpdateChannelStatus(deviceList []*notifyMessage, conf config.GB28181Config) {\n\tfor _, v := range deviceList {\n\t\tswitch v.Event {\n\t\tcase \"ON\":\n\t\t\tnazalog.Debug(\"receive channel online notify\")\n\t\t\td.channelOnline(v.ChannelId)\n\t\tcase \"OFF\":\n\t\t\tnazalog.Debug(\"receive channel offline notify\")\n\t\t\td.channelOffline(v.ChannelId)\n\t\tcase \"VLOST\":\n\t\t\tnazalog.Debug(\"receive channel video lost notify\")\n\t\t\td.channelOffline(v.ChannelId)\n\t\tcase \"DEFECT\":\n\t\t\tnazalog.Debug(\"receive channel video defect notify\")\n\t\t\td.channelOffline(v.ChannelId)\n\t\tcase \"ADD\":\n\t\t\tnazalog.Debug(\"receive channel add notify\")\n\t\t\tchannel := ChannelInfo{\n\t\t\t\tChannelId:    v.ChannelId,\n\t\t\t\tParentId:     v.ParentId,\n\t\t\t\tName:         v.Name,\n\t\t\t\tManufacturer: v.Manufacturer,\n\t\t\t\tModel:        v.Model,\n\t\t\t\tOwner:        v.Owner,\n\t\t\t\tCivilCode:    v.CivilCode,\n\t\t\t\tAddress:      v.Address,\n\t\t\t\tPort:         v.Port,\n\t\t\t\tParental:     v.Parental,\n\t\t\t\tSafetyWay:    v.SafetyWay,\n\t\t\t\tRegisterWay:  v.RegisterWay,\n\t\t\t\tSecrecy:      v.Secrecy,\n\t\t\t\tStatus:       v.Status,\n\t\t\t\tLongitude:    v.Longitude,\n\t\t\t\tLatitude:     v.Latitude,\n\t\t\t}\n\t\t\td.addOrUpdateChannel(channel)\n\t\tcase \"DEL\":\n\t\t\t//删除\n\t\t\tnazalog.Debug(\"receive channel delete notify\")\n\t\t\td.deleteChannel(v.ChannelId)\n\t\tcase \"UPDATE\":\n\t\t\tnazalog.Debug(\"receive channel update notify\")\n\t\t\t// 更新通道\n\t\t\tchannel := ChannelInfo{\n\t\t\t\tChannelId:    v.ChannelId,\n\t\t\t\tParentId:     v.ParentId,\n\t\t\t\tName:         v.Name,\n\t\t\t\tManufacturer: v.Manufacturer,\n\t\t\t\tModel:        v.Model,\n\t\t\t\tOwner:        v.Owner,\n\t\t\t\tCivilCode:    v.CivilCode,\n\t\t\t\tAddress:      v.Address,\n\t\t\t\tPort:         v.Port,\n\t\t\t\tParental:     v.Parental,\n\t\t\t\tSafetyWay:    v.SafetyWay,\n\t\t\t\tRegisterWay:  v.RegisterWay,\n\t\t\t\tSecrecy:      v.Secrecy,\n\t\t\t\tStatus:       v.Status,\n\t\t\t\tLongitude:    v.Longitude,\n\t\t\t\tLatitude:     v.Latitude,\n\t\t\t}\n\t\t\td.UpdateChannels(channel)\n\t\t}\n\t}\n}\n\nfunc (d *Device) channelOnline(channelId string) {\n\tif v, ok := d.channelMap.Load(channelId); ok {\n\t\tc := v.(*Channel)\n\t\tc.Status = ChannelOnStatus\n\t\tnazalog.Debug(\"channel online, channelId: \", channelId)\n\t} else {\n\t\tnazalog.Debug(\"update channel status failed, not found, channelId: \", channelId)\n\t}\n}\n\nfunc (d *Device) channelOffline(channelId string) {\n\tif v, ok := d.channelMap.Load(channelId); ok {\n\t\tc := v.(*Channel)\n\t\tc.Status = ChannelOffStatus\n\t\tnazalog.Debug(\"channel offline, channelId: \", channelId)\n\t} else {\n\t\tnazalog.Debug(\"update channel status failed, not found, channelId: \", channelId)\n\t}\n}\n\nfunc (d *Device) deleteChannel(channelId string) {\n\td.channelMap.Delete(channelId)\n}\n\n// UpdateChannelPosition 更新通道GPS坐标\nfunc (d *Device) UpdateChannelPosition(channelId string, gpsTime string, lng string, lat string) {\n\tif v, ok := d.channelMap.Load(channelId); ok {\n\t\tc := v.(*Channel)\n\t\tc.GpsTime = time.Now() //时间取系统收到的时间，避免设备时间和格式问题\n\t\tc.Longitude = lng\n\t\tc.Latitude = lat\n\t\tnazalog.Debug(\"update channel position success\")\n\t} else {\n\t\t//如果未找到通道，则更新到设备上\n\t\td.GpsTime = time.Now() //时间取系统收到的时间，避免设备时间和格式问题\n\t\td.Longitude = lng\n\t\td.Latitude = lat\n\t\tnazalog.Debug(\"update device position success, channelId:\", channelId)\n\t}\n}\n\nfunc (d *Device) SipRequestForResponse(request sip.Request) (sip.Response, error) {\n\treturn d.sipSvr.RequestWithContext(context.Background(), request)\n}\n"
  },
  {
    "path": "gb28181/http_logic.go",
    "content": "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 *GbLogic\nvar once sync.Once\n\nfunc NewGbLogic(s *GB28181Server) *GbLogic {\n\tonce.Do(func() {\n\t\tgbLogic = &GbLogic{\n\t\t\ts: s,\n\t\t}\n\t})\n\treturn gbLogic\n}\n\nfunc (g *GbLogic) GetDeviceInfos(c *gin.Context) {\n\tdeviceInfos := g.s.getDeviceInfos()\n\tResponseSuccess(c, deviceInfos)\n}\n\nfunc (g *GbLogic) StartPlay(c *gin.Context) {\n\tvar reqPlay ReqPlay\n\tif err := c.ShouldBindJSON(&reqPlay); err != nil {\n\t\tResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg())\n\t} else {\n\t\tch := g.s.FindChannel(reqPlay.DeviceId, reqPlay.ChannelId)\n\t\tif ch == nil {\n\t\t\tResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg())\n\t\t} else {\n\t\t\tstreamName := reqPlay.StreamName\n\t\t\tif len(streamName) == 0 {\n\t\t\t\tstreamName = reqPlay.ChannelId\n\t\t\t}\n\t\t\tif len(reqPlay.NetWork) == 0 || !(reqPlay.NetWork == \"udp\" || reqPlay.NetWork == \"tcp\") {\n\t\t\t\treqPlay.NetWork = \"udp\"\n\t\t\t}\n\n\t\t\tch.TryAutoInvite(&InviteOptions{}, streamName, &reqPlay.PlayInfo)\n\t\t\trespPlay := &RespPlay{\n\t\t\t\tStreamName: streamName,\n\t\t\t}\n\t\t\tResponseSuccess(c, respPlay)\n\t\t}\n\t}\n\n}\nfunc (g *GbLogic) StopPlay(c *gin.Context) {\n\tvar reqStop ReqStop\n\tif err := c.ShouldBindJSON(&reqStop); err != nil {\n\t\tResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg())\n\t} else {\n\t\tch := g.s.FindChannel(reqStop.DeviceId, reqStop.ChannelId)\n\t\tif ch == nil {\n\t\t\tResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg())\n\t\t} else {\n\t\t\tstreamName := reqStop.StreamName\n\t\t\tif len(streamName) == 0 {\n\t\t\t\tstreamName = reqStop.ChannelId\n\t\t\t}\n\t\t\tif err = ch.Bye(streamName); err != nil {\n\t\t\t\tResponseErrorWithMsg(c, CodeDeviceStopError, err.Error())\n\t\t\t} else {\n\t\t\t\tResponseSuccess(c, nil)\n\t\t\t}\n\t\t}\n\t}\n}\nfunc (g *GbLogic) PtzDirection(c *gin.Context) {\n\tvar reqDirection PtzDirection\n\tif err := c.ShouldBindJSON(&reqDirection); err != nil {\n\t\tResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg())\n\t} else {\n\t\tif !(reqDirection.Speed > 0 && reqDirection.Speed <= 8) {\n\t\t\tResponseErrorWithMsg(c, CodeInvalidParam, SpeedParamError)\n\t\t}\n\t\tch := g.s.FindChannel(reqDirection.DeviceId, reqDirection.ChannelId)\n\t\tif ch == nil {\n\t\t\tResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg())\n\t\t} else {\n\t\t\treqDirection.Speed = reqDirection.Speed * 25\n\t\t\tif err = ch.PtzDirection(&reqDirection); err != nil {\n\t\t\t\tResponseErrorWithMsg(c, CodeDeviceStopError, err.Error())\n\t\t\t} else {\n\t\t\t\tResponseSuccess(c, nil)\n\t\t\t}\n\t\t}\n\t}\n}\nfunc (g *GbLogic) PtzZoom(c *gin.Context) {\n\tvar reqZoom PtzZoom\n\tif err := c.ShouldBindJSON(&reqZoom); err != nil {\n\t\tResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg())\n\t} else {\n\t\tif !(reqZoom.Speed > 0 && reqZoom.Speed <= 8) {\n\t\t\tResponseErrorWithMsg(c, CodeInvalidParam, SpeedParamError)\n\t\t}\n\t\tch := g.s.FindChannel(reqZoom.DeviceId, reqZoom.ChannelId)\n\t\tif ch == nil {\n\t\t\tResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg())\n\t\t} else {\n\t\t\treqZoom.Speed = reqZoom.Speed * 25\n\t\t\tif err = ch.PtzZoom(&reqZoom); err != nil {\n\t\t\t\tResponseErrorWithMsg(c, CodeDeviceStopError, err.Error())\n\t\t\t} else {\n\t\t\t\tResponseSuccess(c, nil)\n\t\t\t}\n\t\t}\n\t}\n}\nfunc (g *GbLogic) PtzFi(c *gin.Context) {\n\tvar reqFi PtzFi\n\tif err := c.ShouldBindJSON(&reqFi); err != nil {\n\t\tResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg())\n\t} else {\n\t\tif !(reqFi.Speed > 0 && reqFi.Speed <= 8) {\n\t\t\tResponseErrorWithMsg(c, CodeInvalidParam, SpeedParamError)\n\t\t}\n\t\tch := g.s.FindChannel(reqFi.DeviceId, reqFi.ChannelId)\n\t\tif ch == nil {\n\t\t\tResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg())\n\t\t} else {\n\t\t\treqFi.Speed = reqFi.Speed * 25\n\t\t\tif err = ch.PtzFi(&reqFi); err != nil {\n\t\t\t\tResponseErrorWithMsg(c, CodeDeviceStopError, err.Error())\n\t\t\t} else {\n\t\t\t\tResponseSuccess(c, nil)\n\t\t\t}\n\t\t}\n\t}\n}\nfunc (g *GbLogic) PtzPreset(c *gin.Context) {\n\tvar reqPreset PtzPreset\n\tif err := c.ShouldBindJSON(&reqPreset); err != nil {\n\t\tResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg())\n\t} else {\n\t\tif !(reqPreset.Point > 0 && reqPreset.Point <= 50) {\n\t\t\tResponseErrorWithMsg(c, CodeInvalidParam, PointParamError)\n\t\t}\n\t\tch := g.s.FindChannel(reqPreset.DeviceId, reqPreset.ChannelId)\n\t\tif ch == nil {\n\t\t\tResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg())\n\t\t} else {\n\t\t\tif err = ch.PtzPreset(&reqPreset); err != nil {\n\t\t\t\tResponseErrorWithMsg(c, CodeDeviceStopError, err.Error())\n\t\t\t} else {\n\t\t\t\tResponseSuccess(c, nil)\n\t\t\t}\n\t\t}\n\t}\n}\nfunc (g *GbLogic) PtzStop(c *gin.Context) {\n\tvar reqStop PtzStop\n\tif err := c.ShouldBindJSON(&reqStop); err != nil {\n\t\tResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg())\n\t} else {\n\t\tch := g.s.FindChannel(reqStop.DeviceId, reqStop.ChannelId)\n\t\tif ch == nil {\n\t\t\tResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg())\n\t\t} else {\n\t\t\tif err = ch.PtzStop(&reqStop); err != nil {\n\t\t\t\tResponseErrorWithMsg(c, CodeDeviceStopError, err.Error())\n\t\t\t} else {\n\t\t\t\tResponseSuccess(c, nil)\n\t\t\t}\n\t\t}\n\t}\n}\nfunc (g *GbLogic) UpdateAllNotify(c *gin.Context) {\n\tg.s.GetAllSyncChannels()\n\tResponseSuccess(c, nil)\n}\nfunc (g *GbLogic) UpdateNotify(c *gin.Context) {\n\tvar reqUpdateNotify ReqUpdateNotify\n\tif err := c.ShouldBindJSON(&reqUpdateNotify); err != nil {\n\t\tResponseErrorWithMsg(c, CodeInvalidParam, CodeInvalidParam.Msg())\n\t} else {\n\t\tif g.s.GetSyncChannels(reqUpdateNotify.DeviceId) {\n\t\t\tResponseSuccess(c, nil)\n\t\t} else {\n\t\t\tResponseErrorWithMsg(c, CodeDeviceNotRegister, CodeDeviceNotRegister.Msg())\n\t\t}\n\n\t}\n\n}\n"
  },
  {
    "path": "gb28181/inviteoption.go",
    "content": "package gb28181\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n)\n\ntype InviteOptions struct {\n\tStart     int\n\tEnd       int\n\tssrc      string\n\tSSRC      uint32\n\tMediaPort uint16\n}\n\nfunc (o InviteOptions) IsLive() bool {\n\treturn o.Start == 0 || o.End == 0\n}\n\nfunc (o InviteOptions) String() string {\n\treturn fmt.Sprintf(\"t=%d %d\", o.Start, o.End)\n}\n\nfunc (o *InviteOptions) CreateSSRC(serial string, number uint16) {\n\t//不按gb生成标准,取ID最后六位，然后按顺序生成，一个channel最大999\n\to.ssrc = fmt.Sprintf(\"%d%s%03d\", 0, serial, number)\n\t_ssrc, _ := strconv.ParseInt(o.ssrc, 10, 0)\n\to.SSRC = uint32(_ssrc)\n}\n"
  },
  {
    "path": "gb28181/mediaserver/conn.go",
    "content": "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/q191201771/lalmax/gb28181/mpegps\"\n\n\t\"github.com/pion/rtp\"\n\t\"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/lal/pkg/logic\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\nvar (\n\tErrInvalidPsData = errors.New(\"invalid mpegps data\")\n)\n\ntype Frame struct {\n\tbuffer  *bytes.Buffer\n\tpts     uint64\n\tdts     uint64\n\tinitPts uint64\n\tinitDts uint64\n}\n\ntype Conn struct {\n\tconn       net.Conn\n\tr          io.Reader\n\tcheck      bool\n\tdemuxer    *mpegps.PsDemuxer\n\tstreamName string\n\tlalServer  logic.ILalServer\n\tlalSession logic.ICustomizePubSessionContext\n\tvideoFrame Frame\n\taudioFrame Frame\n\n\tobserver IGbObserver\n\n\trtpPts         uint64\n\tpsPtsZeroTimes int64\n\n\tpsDumpFile *base.DumpFile\n\n\tbuffer *bytes.Buffer\n\tkey    string\n\n\tmediaServer          *GB28181MediaServer\n\tpreferMediaKeyLookup bool\n\treadTimeout          time.Duration\n\tone                  sync.Once\n\toneSaveConn          sync.Once\n}\n\nfunc NewConn(conn net.Conn, observer IGbObserver, lal logic.ILalServer) *Conn {\n\tc := &Conn{\n\t\tconn:      conn,\n\t\tr:         conn,\n\t\tdemuxer:   mpegps.NewPsDemuxer(),\n\t\tobserver:  observer,\n\t\tlalServer: lal,\n\t\tbuffer:    bytes.NewBuffer(nil),\n\t}\n\n\tc.demuxer.OnFrame = c.OnFrame\n\n\treturn c\n}\nfunc (c *Conn) SetMediaServer(mediaServer *GB28181MediaServer) {\n\tc.mediaServer = mediaServer\n}\nfunc (c *Conn) SetKey(key string) {\n\tc.key = key\n}\nfunc (c *Conn) SetPreferMediaKeyLookup(prefer bool) {\n\tc.preferMediaKeyLookup = prefer\n}\nfunc (c *Conn) SetReadTimeout(timeout time.Duration) {\n\tc.readTimeout = timeout\n}\nfunc (c *Conn) Serve() (err error) {\n\tdefer func() {\n\t\tnazalog.Info(\"conn close, err:\", err)\n\t\tc.Close()\n\n\t\tif c.observer != nil && c.streamName != \"\" {\n\t\t\tc.observer.NotifyClose(c.streamName)\n\t\t}\n\t\tif c.psDumpFile != nil {\n\t\t\tc.psDumpFile.Close()\n\t\t}\n\t\tif c.lalSession != nil {\n\t\t\tc.lalServer.DelCustomizePubSession(c.lalSession)\n\t\t}\n\t}()\n\n\tnazalog.Info(\"gb28181 conn, remoteaddr:\", c.conn.RemoteAddr().String(), \" localaddr:\", c.conn.LocalAddr().String())\n\n\tfor {\n\t\tif c.readTimeout > 0 {\n\t\t\tc.conn.SetReadDeadline(time.Now().Add(c.readTimeout))\n\t\t}\n\t\tpkt := &rtp.Packet{}\n\t\tif c.conn.RemoteAddr().Network() == \"udp\" {\n\t\t\tbuf := make([]byte, 1472*4)\n\t\t\tn, err := c.conn.Read(buf)\n\t\t\tif err != nil {\n\t\t\t\tnazalog.Error(\"conn read failed, err:\", err)\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\terr = pkt.Unmarshal(buf[:n])\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tlen := make([]byte, 2)\n\t\t\t_, err := io.ReadFull(c.r, len)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tsize := binary.BigEndian.Uint16(len)\n\t\t\tbuf := make([]byte, size)\n\t\t\t_, err = io.ReadFull(c.r, buf)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\terr = pkt.Unmarshal(buf)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif !c.check && c.observer != nil {\n\t\t\tvar mediaInfo *MediaInfo\n\t\t\tvar ok bool\n\t\t\tif c.preferMediaKeyLookup {\n\t\t\t\tmediaInfo, ok = c.observer.GetMediaInfoByKey(c.key)\n\t\t\t\tif !ok {\n\t\t\t\t\tnazalog.Error(\"get mediaInfo :\", c.key)\n\t\t\t\t\treturn fmt.Errorf(\"get mediaInfo:%s\", c.key)\n\t\t\t\t}\n\t\t\t} else if pkt.SSRC != 0 {\n\t\t\t\tmediaInfo, ok = c.observer.CheckSsrc(pkt.SSRC)\n\t\t\t\tif !ok {\n\t\t\t\t\tnazalog.Error(\"invalid ssrc:\", pkt.SSRC)\n\t\t\t\t\treturn fmt.Errorf(\"invalid ssrc:%d\", pkt.SSRC)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tmediaInfo, ok = c.observer.GetMediaInfoByKey(c.key)\n\t\t\t\tif !ok {\n\t\t\t\t\tnazalog.Error(\"get mediaInfo :\", c.key)\n\t\t\t\t\treturn fmt.Errorf(\"get mediaInfo:%s\", c.key)\n\t\t\t\t}\n\t\t\t}\n\t\t\tc.check = true\n\t\t\tc.streamName = mediaInfo.StreamName\n\t\t\tc.oneSaveConn.Do(func() {\n\t\t\t\tif c.mediaServer != nil {\n\t\t\t\t\tc.mediaServer.conns.Store(c.streamName, c)\n\t\t\t\t}\n\t\t\t})\n\t\t\tif len(mediaInfo.DumpFileName) > 0 {\n\t\t\t\tc.psDumpFile = base.NewDumpFile()\n\t\t\t\tif err = c.psDumpFile.OpenToWrite(mediaInfo.DumpFileName); err != nil {\n\t\t\t\t\tnazalog.Errorf(\"gb con dump file:%s\", err.Error())\n\t\t\t\t}\n\t\t\t}\n\t\t\tnazalog.Info(\"gb28181 ssrc check success, streamName:\", c.streamName)\n\t\t\tif c.observer != nil {\n\t\t\t\tc.observer.OnRtpPacket(c.streamName, c.key)\n\t\t\t}\n\n\t\t\tsession, err := c.lalServer.AddCustomizePubSession(mediaInfo.StreamName)\n\t\t\tif err != nil {\n\t\t\t\tnazalog.Error(\"lal server AddCustomizePubSession failed, err:\", err)\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tsession.WithOption(func(option *base.AvPacketStreamOption) {\n\t\t\t\toption.VideoFormat = base.AvPacketStreamVideoFormatAnnexb\n\t\t\t\toption.AudioFormat = base.AvPacketStreamAudioFormatAdtsAac\n\t\t\t})\n\n\t\t\tc.lalSession = session\n\t\t}\n\t\tc.rtpPts = uint64(pkt.Header.Timestamp)\n\t\tif c.observer != nil && c.streamName != \"\" {\n\t\t\tc.observer.OnRtpPacket(c.streamName, c.key)\n\t\t}\n\t\tif c.demuxer != nil {\n\t\t\tif c.psDumpFile != nil {\n\t\t\t\tc.psDumpFile.WriteWithType(pkt.Payload, base.DumpTypePsRtpData)\n\t\t\t}\n\t\t\tc.demuxer.Input(pkt.Payload)\n\t\t}\n\t}\n\treturn\n}\n\nfunc (c *Conn) Demuxer(data []byte) error {\n\tc.buffer.Write(data)\n\n\tbuf := c.buffer.Bytes()\n\tif len(buf) < 4 {\n\t\treturn nil\n\t}\n\n\tif buf[0] != 0x00 && buf[1] != 0x00 && buf[2] != 0x01 && buf[3] != 0xBA {\n\t\treturn ErrInvalidPsData\n\t}\n\n\tpackets := splitPsPackets(buf)\n\tif len(packets) <= 1 {\n\t\treturn nil\n\t}\n\n\tfor i, packet := range packets {\n\t\tif i == len(packets)-1 {\n\t\t\tc.buffer = bytes.NewBuffer(packet)\n\t\t\treturn nil\n\t\t}\n\n\t\tif c.demuxer != nil {\n\t\t\tc.demuxer.Input(packet)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *Conn) OnFrame(frame []byte, cid mpegps.PsStreamType, pts uint64, dts uint64) {\n\tpayloadType := getPayloadType(cid)\n\tif payloadType == base.AvPacketPtUnknown {\n\t\treturn\n\t}\n\t//当ps流解析出pts为0时，计数超过10则用rtp的时间戳\n\tif pts == 0 {\n\t\tif c.psPtsZeroTimes >= 0 {\n\t\t\tc.psPtsZeroTimes++\n\t\t}\n\t\tif c.psPtsZeroTimes > 10 {\n\t\t\tpts = c.rtpPts\n\t\t\tdts = c.rtpPts\n\t\t}\n\t} else {\n\t\tc.psPtsZeroTimes = -1\n\t}\n\tif payloadType == base.AvPacketPtAac || payloadType == base.AvPacketPtG711A || payloadType == base.AvPacketPtG711U {\n\t\tif c.audioFrame.initDts == 0 {\n\t\t\tc.audioFrame.initDts = dts\n\t\t}\n\n\t\tif c.audioFrame.initPts == 0 {\n\t\t\tc.audioFrame.initPts = pts\n\t\t}\n\n\t\tvar pkt base.AvPacket\n\t\tpkt.PayloadType = payloadType\n\t\tpkt.Timestamp = int64(dts - c.audioFrame.initDts)\n\t\tpkt.Pts = int64(pts - c.audioFrame.initPts)\n\t\tpkt.Payload = append(pkt.Payload, frame...)\n\t\tc.lalSession.FeedAvPacket(pkt)\n\n\t} else {\n\t\tif c.videoFrame.initPts == 0 {\n\t\t\tc.videoFrame.initPts = pts\n\t\t}\n\n\t\tif c.videoFrame.initDts == 0 {\n\t\t\tc.videoFrame.initDts = dts\n\t\t}\n\n\t\t// 塞入lal中\n\t\tc.videoFrame.pts = pts - c.videoFrame.initPts\n\t\tc.videoFrame.dts = dts - c.videoFrame.initDts\n\t\tvar pkt base.AvPacket\n\t\tpkt.PayloadType = payloadType\n\t\tpkt.Timestamp = int64(c.videoFrame.dts)\n\t\tpkt.Pts = int64(c.videoFrame.pts)\n\t\tpkt.Payload = frame\n\t\tc.lalSession.FeedAvPacket(pkt)\n\t}\n}\nfunc (c *Conn) Close() {\n\tc.one.Do(func() {\n\t\tc.conn.Close()\n\t})\n}\nfunc getPayloadType(cid mpegps.PsStreamType) base.AvPacketPt {\n\tswitch cid {\n\tcase mpegps.PsStreamAac:\n\t\treturn base.AvPacketPtAac\n\tcase mpegps.PsStreamG711A:\n\t\treturn base.AvPacketPtG711A\n\tcase mpegps.PsStreamG711U:\n\t\treturn base.AvPacketPtG711U\n\tcase mpegps.PsStreamH264:\n\t\treturn base.AvPacketPtAvc\n\tcase mpegps.PsStreamH265:\n\t\treturn base.AvPacketPtHevc\n\t}\n\n\treturn base.AvPacketPtUnknown\n}\n\nfunc splitPsPackets(data []byte) [][]byte {\n\tstartCode := []byte{0x00, 0x00, 0x01, 0xBA}\n\tstart := 0\n\tvar packets [][]byte\n\tfor i := 0; i < len(data); i++ {\n\t\tif i+len(startCode) <= len(data) && bytes.Equal(data[i:i+len(startCode)], startCode) {\n\t\t\tif i == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpackets = append(packets, data[start:i])\n\t\t\tstart = i\n\t\t}\n\t}\n\tpackets = append(packets, data[start:])\n\n\treturn packets\n}\n"
  },
  {
    "path": "gb28181/mediaserver/mediaserver_t.go",
    "content": "package mediaserver\n\ntype MediaInfo struct {\n\tIsInvite     bool\n\tSsrc         uint32\n\tStreamName   string\n\tSinglePort   bool\n\tDumpFileName string\n\tMediaKey     string\n}\n\nfunc (m *MediaInfo) Clear() (err error) {\n\tm.IsInvite = false\n\tm.Ssrc = 0\n\tm.StreamName = \"\"\n\tm.SinglePort = false\n\tm.DumpFileName = \"\"\n\n\treturn\n}\n"
  },
  {
    "path": "gb28181/mediaserver/server.go",
    "content": "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\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\nconst defaultReadTimeout = 10 * time.Second\n\ntype IGbObserver interface {\n\tCheckSsrc(ssrc uint32) (*MediaInfo, bool)\n\tGetMediaInfoByKey(key string) (*MediaInfo, bool)\n\tNotifyClose(streamName string)\n\tOnRtpPacket(streamName string, mediaKey string)\n}\n\ntype GB28181MediaServer struct {\n\tlistenPort int\n\tlalServer  logic.ILalServer\n\n\tlistener net.Listener\n\n\tdisposeOnce          sync.Once\n\tdisposed             atomic.Bool\n\tobserver             IGbObserver\n\tmediaKey             string\n\tpreferMediaKeyLookup bool\n\treadTimeout          time.Duration\n\n\tconns sync.Map //增加链接对象，目前只适用于多端口\n}\n\nfunc NewGB28181MediaServer(listenPort int, mediaKey string, observer IGbObserver, lal logic.ILalServer) *GB28181MediaServer {\n\treturn &GB28181MediaServer{\n\t\tlistenPort:  listenPort,\n\t\tlalServer:   lal,\n\t\tobserver:    observer,\n\t\tmediaKey:    mediaKey,\n\t\treadTimeout: defaultReadTimeout,\n\t}\n}\n\nfunc (s *GB28181MediaServer) WithPreferMediaKeyLookup(prefer bool) *GB28181MediaServer {\n\ts.preferMediaKeyLookup = prefer\n\treturn s\n}\n\nfunc (s *GB28181MediaServer) WithReadTimeout(timeout time.Duration) *GB28181MediaServer {\n\ts.readTimeout = timeout\n\treturn s\n}\n\nfunc (s *GB28181MediaServer) GetListenerPort() uint16 {\n\treturn uint16(s.listenPort)\n}\nfunc (s *GB28181MediaServer) Start(listener net.Listener) (err error) {\n\ts.listener = listener\n\tif listener != nil {\n\t\tgo func(listener net.Listener) {\n\t\t\tfor {\n\t\t\t\tif s.disposed.Load() {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconn, err := listener.Accept()\n\t\t\t\tif err != nil {\n\t\t\t\t\tvar ne net.Error\n\t\t\t\t\tif ok := errors.As(err, &ne); ok && ne.Timeout() {\n\t\t\t\t\t\tnazalog.Error(\"Accept failed: timeout error, retrying...\")\n\t\t\t\t\t\ttime.Sleep(time.Second / 20)\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif conn == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif s.disposed.Load() {\n\t\t\t\t\tconn.Close()\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tc := NewConn(conn, s.observer, s.lalServer)\n\t\t\t\tc.SetKey(s.mediaKey)\n\t\t\t\tc.SetMediaServer(s)\n\t\t\t\tc.SetPreferMediaKeyLookup(s.preferMediaKeyLookup)\n\t\t\t\tc.SetReadTimeout(s.readTimeout)\n\t\t\t\ts.conns.Store(c, c)\n\t\t\t\tgo func() {\n\t\t\t\t\tc.Serve()\n\t\t\t\t\ts.conns.Delete(c)\n\t\t\t\t\ts.conns.Delete(c.streamName)\n\t\t\t\t}()\n\t\t\t}\n\t\t}(listener)\n\t}\n\treturn\n}\nfunc (s *GB28181MediaServer) CloseConn(streamName string) {\n\tif v, ok := s.conns.Load(streamName); ok {\n\t\tconn := v.(*Conn)\n\t\tconn.Close()\n\t}\n}\nfunc (s *GB28181MediaServer) Dispose() {\n\ts.disposeOnce.Do(func() {\n\t\ts.disposed.Store(true)\n\t\ts.conns.Range(func(_, value any) bool {\n\t\t\tconn := value.(*Conn)\n\t\t\tconn.Close()\n\t\t\treturn true\n\t\t})\n\t\tif s.listener != nil {\n\t\t\ts.listener.Close()\n\t\t\ts.listener = nil\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "gb28181/mpegps/bitstream.go",
    "content": "package mpegps\n\nimport (\n\t\"encoding/binary\"\n)\n\nvar BitMask [8]byte = [8]byte{0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF}\n\ntype BitStream struct {\n\tbits        []byte\n\tbytesOffset int\n\tbitsOffset  int\n\tbitsmark    int\n\tbytemark    int\n}\n\nfunc NewBitStream(buf []byte) *BitStream {\n\treturn &BitStream{\n\t\tbits:        buf,\n\t\tbytesOffset: 0,\n\t\tbitsOffset:  0,\n\t\tbitsmark:    0,\n\t\tbytemark:    0,\n\t}\n}\n\nfunc (bs *BitStream) Uint8(n int) uint8 {\n\treturn uint8(bs.GetBits(n))\n}\n\nfunc (bs *BitStream) Uint16(n int) uint16 {\n\treturn uint16(bs.GetBits(n))\n}\n\nfunc (bs *BitStream) Uint32(n int) uint32 {\n\treturn uint32(bs.GetBits(n))\n}\n\nfunc (bs *BitStream) GetBytes(n int) []byte {\n\tif bs.bytesOffset+n > len(bs.bits) {\n\t\tpanic(\"OUT OF RANGE\")\n\t}\n\tif bs.bitsOffset != 0 {\n\t\tpanic(\"invaild operation\")\n\t}\n\tdata := make([]byte, n)\n\tcopy(data, bs.bits[bs.bytesOffset:bs.bytesOffset+n])\n\tbs.bytesOffset += n\n\treturn data\n}\n\n// n <= 64\nfunc (bs *BitStream) GetBits(n int) uint64 {\n\tif bs.bytesOffset >= len(bs.bits) {\n\t\tpanic(\"OUT OF RANGE\")\n\t}\n\tvar ret uint64 = 0\n\tif 8-bs.bitsOffset >= n {\n\t\tret = uint64((bs.bits[bs.bytesOffset] >> (8 - bs.bitsOffset - n)) & BitMask[n-1])\n\t\tbs.bitsOffset += n\n\t\tif bs.bitsOffset == 8 {\n\t\t\tbs.bytesOffset++\n\t\t\tbs.bitsOffset = 0\n\t\t}\n\t} else {\n\t\tret = uint64(bs.bits[bs.bytesOffset] & BitMask[8-bs.bitsOffset-1])\n\t\tbs.bytesOffset++\n\t\tn -= 8 - bs.bitsOffset\n\t\tbs.bitsOffset = 0\n\t\tfor n > 0 {\n\t\t\tif bs.bytesOffset >= len(bs.bits) {\n\t\t\t\tpanic(\"OUT OF RANGE\")\n\t\t\t}\n\t\t\tif n >= 8 {\n\t\t\t\tret = ret<<8 | uint64(bs.bits[bs.bytesOffset])\n\t\t\t\tbs.bytesOffset++\n\t\t\t\tn -= 8\n\t\t\t} else {\n\t\t\t\tret = (ret << n) | uint64((bs.bits[bs.bytesOffset]>>(8-n))&BitMask[n-1])\n\t\t\t\tbs.bitsOffset = n\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\treturn ret\n}\n\nfunc (bs *BitStream) GetBit() uint8 {\n\tif bs.bytesOffset >= len(bs.bits) {\n\t\tpanic(\"OUT OF RANGE\")\n\t}\n\tret := bs.bits[bs.bytesOffset] >> (7 - bs.bitsOffset) & 0x01\n\tbs.bitsOffset++\n\tif bs.bitsOffset >= 8 {\n\t\tbs.bytesOffset++\n\t\tbs.bitsOffset = 0\n\t}\n\treturn ret\n}\n\nfunc (bs *BitStream) SkipBits(n int) {\n\tbytecount := n / 8\n\tbitscount := n % 8\n\tbs.bytesOffset += bytecount\n\tif bs.bitsOffset+bitscount < 8 {\n\t\tbs.bitsOffset += bitscount\n\t} else {\n\t\tbs.bytesOffset += 1\n\t\tbs.bitsOffset += bitscount - 8\n\t}\n}\n\nfunc (bs *BitStream) Markdot() {\n\tbs.bitsmark = bs.bitsOffset\n\tbs.bytemark = bs.bytesOffset\n}\n\nfunc (bs *BitStream) DistanceFromMarkDot() int {\n\tbytecount := bs.bytesOffset - bs.bytemark - 1\n\tbitscount := bs.bitsOffset + (8 - bs.bitsmark)\n\treturn bytecount*8 + bitscount\n}\n\nfunc (bs *BitStream) RemainBytes() int {\n\tif bs.bitsOffset > 0 {\n\t\treturn len(bs.bits) - bs.bytesOffset - 1\n\t} else {\n\t\treturn len(bs.bits) - bs.bytesOffset\n\t}\n}\n\nfunc (bs *BitStream) RemainBits() int {\n\tif bs.bitsOffset > 0 {\n\t\treturn bs.RemainBytes()*8 + 8 - bs.bitsOffset\n\t} else {\n\t\treturn bs.RemainBytes() * 8\n\t}\n\n}\n\nfunc (bs *BitStream) Bits() []byte {\n\treturn bs.bits\n}\n\nfunc (bs *BitStream) RemainData() []byte {\n\treturn bs.bits[bs.bytesOffset:]\n}\n\n// 无符号哥伦布熵编码\nfunc (bs *BitStream) ReadUE() uint64 {\n\tleadingZeroBits := 0\n\tfor bs.GetBit() == 0 {\n\t\tleadingZeroBits++\n\t}\n\tif leadingZeroBits == 0 {\n\t\treturn 0\n\t}\n\tinfo := bs.GetBits(leadingZeroBits)\n\treturn uint64(1)<<leadingZeroBits - 1 + info\n}\n\n// 有符号哥伦布熵编码\nfunc (bs *BitStream) ReadSE() int64 {\n\tv := bs.ReadUE()\n\tif v%2 == 0 {\n\t\treturn -1 * int64(v/2)\n\t} else {\n\t\treturn int64(v+1) / 2\n\t}\n}\n\nfunc (bs *BitStream) ByteOffset() int {\n\treturn bs.bytesOffset\n}\n\nfunc (bs *BitStream) UnRead(n int) {\n\tif n-bs.bitsOffset <= 0 {\n\t\tbs.bitsOffset -= n\n\t} else {\n\t\tleast := n - bs.bitsOffset\n\t\tfor least >= 8 {\n\t\t\tbs.bytesOffset--\n\t\t\tleast -= 8\n\t\t}\n\t\tif least > 0 {\n\t\t\tbs.bytesOffset--\n\t\t\tbs.bitsOffset = 8 - least\n\t\t}\n\t}\n}\n\nfunc (bs *BitStream) NextBits(n int) uint64 {\n\tr := bs.GetBits(n)\n\tbs.UnRead(n)\n\treturn r\n}\n\nfunc (bs *BitStream) EOS() bool {\n\treturn bs.bytesOffset == len(bs.bits) && bs.bitsOffset == 0\n}\n\ntype BitStreamWriter struct {\n\tbits       []byte\n\tbyteoffset int\n\tbitsoffset int\n\tbitsmark   int\n\tbytemark   int\n}\n\nfunc NewBitStreamWriter(n int) *BitStreamWriter {\n\treturn &BitStreamWriter{\n\t\tbits:       make([]byte, n),\n\t\tbyteoffset: 0,\n\t\tbitsoffset: 0,\n\t\tbitsmark:   0,\n\t\tbytemark:   0,\n\t}\n}\n\nfunc (bsw *BitStreamWriter) expandSpace(n int) {\n\tif (len(bsw.bits)-bsw.byteoffset-1)*8+8-bsw.bitsoffset < n {\n\t\tnewlen := 0\n\t\tif len(bsw.bits)*8 < n {\n\t\t\tnewlen = len(bsw.bits) + n/8 + 1\n\t\t} else {\n\t\t\tnewlen = len(bsw.bits) * 2\n\t\t}\n\t\ttmp := make([]byte, newlen)\n\t\tcopy(tmp, bsw.bits)\n\t\tbsw.bits = tmp\n\t}\n}\n\nfunc (bsw *BitStreamWriter) ByteOffset() int {\n\treturn bsw.byteoffset\n}\n\nfunc (bsw *BitStreamWriter) BitOffset() int {\n\treturn bsw.bitsoffset\n}\n\nfunc (bsw *BitStreamWriter) Markdot() {\n\tbsw.bitsmark = bsw.bitsoffset\n\tbsw.bytemark = bsw.byteoffset\n}\n\nfunc (bsw *BitStreamWriter) DistanceFromMarkDot() int {\n\tbytecount := bsw.byteoffset - bsw.bytemark - 1\n\tbitscount := bsw.bitsoffset + (8 - bsw.bitsmark)\n\treturn bytecount*8 + bitscount\n}\n\nfunc (bsw *BitStreamWriter) PutByte(v byte) {\n\tbsw.expandSpace(8)\n\tif bsw.bitsoffset == 0 {\n\t\tbsw.bits[bsw.byteoffset] = v\n\t\tbsw.byteoffset++\n\t} else {\n\t\tbsw.bits[bsw.byteoffset] |= v >> byte(bsw.bitsoffset)\n\t\tbsw.byteoffset++\n\t\tbsw.bits[bsw.byteoffset] = v & BitMask[bsw.bitsoffset-1]\n\t}\n}\n\nfunc (bsw *BitStreamWriter) PutBytes(v []byte) {\n\tif bsw.bitsoffset != 0 {\n\t\tpanic(\"bsw.bitsoffset > 0\")\n\t}\n\tbsw.expandSpace(8 * len(v))\n\tcopy(bsw.bits[bsw.byteoffset:], v)\n\tbsw.byteoffset += len(v)\n}\n\nfunc (bsw *BitStreamWriter) PutRepetValue(v byte, n int) {\n\tif bsw.bitsoffset != 0 {\n\t\tpanic(\"bsw.bitsoffset > 0\")\n\t}\n\tbsw.expandSpace(8 * n)\n\tfor i := 0; i < n; i++ {\n\t\tbsw.bits[bsw.byteoffset] = v\n\t\tbsw.byteoffset++\n\t}\n}\n\nfunc (bsw *BitStreamWriter) PutUint8(v uint8, n int) {\n\tbsw.PutUint64(uint64(v), n)\n}\n\nfunc (bsw *BitStreamWriter) PutUint16(v uint16, n int) {\n\tbsw.PutUint64(uint64(v), n)\n}\n\nfunc (bsw *BitStreamWriter) PutUint32(v uint32, n int) {\n\tbsw.PutUint64(uint64(v), n)\n}\n\nfunc (bsw *BitStreamWriter) PutUint64(v uint64, n int) {\n\tbsw.expandSpace(n)\n\tif 8-bsw.bitsoffset >= n {\n\t\tbsw.bits[bsw.byteoffset] |= uint8(v) & BitMask[n-1] << (8 - bsw.bitsoffset - n)\n\t\tbsw.bitsoffset += n\n\t\tif bsw.bitsoffset == 8 {\n\t\t\tbsw.bitsoffset = 0\n\t\t\tbsw.byteoffset++\n\t\t}\n\t} else {\n\t\tbsw.bits[bsw.byteoffset] |= uint8(v>>(n-int(8-bsw.bitsoffset))) & BitMask[8-bsw.bitsoffset-1]\n\t\tbsw.byteoffset++\n\t\tn -= 8 - bsw.bitsoffset\n\t\tfor n-8 >= 0 {\n\t\t\tbsw.bits[bsw.byteoffset] = uint8(v>>(n-8)) & 0xFF\n\t\t\tbsw.byteoffset++\n\t\t\tn -= 8\n\t\t}\n\t\tbsw.bitsoffset = n\n\t\tif n > 0 {\n\t\t\tbsw.bits[bsw.byteoffset] |= (uint8(v) & BitMask[n-1]) << (8 - n)\n\t\t}\n\t}\n}\n\nfunc (bsw *BitStreamWriter) SetByte(v byte, where int) {\n\tbsw.bits[where] = v\n}\n\nfunc (bsw *BitStreamWriter) SetUint16(v uint16, where int) {\n\tbinary.BigEndian.PutUint16(bsw.bits[where:where+2], v)\n}\n\nfunc (bsw *BitStreamWriter) Bits() []byte {\n\tif bsw.byteoffset == len(bsw.bits) {\n\t\treturn bsw.bits\n\t}\n\tif bsw.bitsoffset > 0 {\n\t\treturn bsw.bits[0 : bsw.byteoffset+1]\n\t} else {\n\t\treturn bsw.bits[0:bsw.byteoffset]\n\t}\n}\n\n// 用v 填充剩余字节\nfunc (bsw *BitStreamWriter) FillRemainData(v byte) {\n\tfor i := bsw.byteoffset; i < len(bsw.bits); i++ {\n\t\tbsw.bits[i] = v\n\t}\n\tbsw.byteoffset = len(bsw.bits)\n\tbsw.bitsoffset = 0\n}\n\nfunc (bsw *BitStreamWriter) Reset() {\n\tfor i := 0; i < len(bsw.bits); i++ {\n\t\tbsw.bits[i] = 0\n\t}\n\tbsw.bitsmark = 0\n\tbsw.bytemark = 0\n\tbsw.bitsoffset = 0\n\tbsw.byteoffset = 0\n}\n"
  },
  {
    "path": "gb28181/mpegps/pes_proto.go",
    "content": "package mpegps\n\nimport (\n\t\"fmt\"\n\t\"os\"\n)\n\ntype TsStreamType int\n\nconst (\n\tTsStreamAudioMpeg1 TsStreamType = 0x03\n\tTsStreamAudioMpeg2 TsStreamType = 0x04\n\tTsStreamAac        TsStreamType = 0x0F\n\tTsStreamH264       TsStreamType = 0x1B\n\tTsStreamH265       TsStreamType = 0x24\n)\n\nvar H264AudNalu []byte = []byte{0x00, 0x00, 0x00, 0x01, 0x09, 0xF0} //ffmpeg mpegtsenc.c mpegts_write_packet_internal\nvar H265AudNalu []byte = []byte{0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x50}\n\ntype PesStreamId int\n\nconst (\n\tPesStreamEnd        PesStreamId = 0xB9\n\tPesStreamStart      PesStreamId = 0xBA\n\tPesStreamSystemHead PesStreamId = 0xBB\n\tPesStreamMap        PesStreamId = 0xBC\n\tPesStreamPrivate    PesStreamId = 0xBD\n\tPesStreamAudio      PesStreamId = 0xC0\n\tPesStreamVideo      PesStreamId = 0xE0\n)\n\ntype Display interface {\n\tPrettyPrint(file *os.File)\n}\n\nfunc findPesIdByStreamType(cid TsStreamType) PesStreamId {\n\tswitch cid {\n\tcase TsStreamAac, TsStreamAudioMpeg1, TsStreamAudioMpeg2:\n\t\treturn PesStreamAudio\n\tcase TsStreamH264, TsStreamH265:\n\t\treturn PesStreamVideo\n\tdefault:\n\t\treturn PesStreamPrivate\n\t}\n}\n\ntype PesPacket struct {\n\tStreamId               uint8\n\tPesPacketLength        uint16\n\tPesScramblingControl   uint8\n\tPesPriority            uint8\n\tDataAlignmentIndicator uint8\n\tCopyright              uint8\n\tOriginalOrCopy         uint8\n\tPtsDtsFlags            uint8\n\tEscrFlag               uint8\n\tEsRateFlag             uint8\n\tDsmTrickModeFlag       uint8\n\tAdditionalCopyInfoFlag uint8\n\tPesCrcFlag             uint8\n\tPesExtensionFlag       uint8\n\tPesHeaderDataLength    uint8\n\tPts                    uint64\n\tDts                    uint64\n\tEscrBase               uint64\n\tEscrExtension          uint16\n\tEsRate                 uint32\n\tTrickModeControl       uint8\n\tTrickValue             uint8\n\tAdditionalCopyInfo     uint8\n\tPreviousPesPacketCrc   uint16\n\tPesPayload             []byte\n\t//TODO\n\t//if ( PesExtensionFlag == '1')\n\t// PesPrivateDataFlag                 uint8\n\t// PackHeaderFieldFlag               uint8\n\t// ProgramPacketSequenceCounterFlag  uint8\n\t// PStdBufferFlag                     uint8\n\t// PesExtensionFlag2                 uint8\n\t// PesPrivateData                     [16]byte\n}\n\nfunc NewPesPacket() *PesPacket {\n\treturn new(PesPacket)\n}\n\nfunc (pkg *PesPacket) PrettyPrint(file *os.File) {\n\tfile.WriteString(fmt.Sprintf(\"stream id:%d\\n\", pkg.StreamId))\n\tfile.WriteString(fmt.Sprintf(\"pes packet length:%d\\n\", pkg.PesPacketLength))\n\tfile.WriteString(fmt.Sprintf(\"pes scrambling control:%d\\n\", pkg.PesScramblingControl))\n\tfile.WriteString(fmt.Sprintf(\"pes priority:%d\\n\", pkg.PesPriority))\n\tfile.WriteString(fmt.Sprintf(\"data alignment indicator:%d\\n\", pkg.DataAlignmentIndicator))\n\tfile.WriteString(fmt.Sprintf(\"copyright:%d\\n\", pkg.Copyright))\n\tfile.WriteString(fmt.Sprintf(\"original or copy:%d\\n\", pkg.OriginalOrCopy))\n\tfile.WriteString(fmt.Sprintf(\"pts dts flags:%d\\n\", pkg.PtsDtsFlags))\n\tfile.WriteString(fmt.Sprintf(\"escr flag:%d\\n\", pkg.EscrFlag))\n\tfile.WriteString(fmt.Sprintf(\"es rate flag:%d\\n\", pkg.EsRateFlag))\n\tfile.WriteString(fmt.Sprintf(\"dsm trick mode flag:%d\\n\", pkg.DsmTrickModeFlag))\n\tfile.WriteString(fmt.Sprintf(\"additional copy info flag:%d\\n\", pkg.AdditionalCopyInfoFlag))\n\tfile.WriteString(fmt.Sprintf(\"pes crc flag:%d\\n\", pkg.PesCrcFlag))\n\tfile.WriteString(fmt.Sprintf(\"pes extension flag:%d\\n\", pkg.PesExtensionFlag))\n\tfile.WriteString(fmt.Sprintf(\"pes header data length:%d\\n\", pkg.PesHeaderDataLength))\n\tif pkg.PtsDtsFlags&0x02 == 0x02 {\n\t\tfile.WriteString(fmt.Sprintf(\"PTS:%d\\n\", pkg.Pts))\n\t}\n\tif pkg.PtsDtsFlags&0x03 == 0x03 {\n\t\tfile.WriteString(fmt.Sprintf(\"DTS:%d\\n\", pkg.Dts))\n\t}\n\n\tif pkg.EscrFlag == 1 {\n\t\tfile.WriteString(fmt.Sprintf(\"escr base:%d\\n\", pkg.EscrBase))\n\t\tfile.WriteString(fmt.Sprintf(\"escr extension:%d\\n\", pkg.EscrExtension))\n\t}\n\n\tif pkg.EsRateFlag == 1 {\n\t\tfile.WriteString(fmt.Sprintf(\"es rate:%d\\n\", pkg.EsRate))\n\t}\n\n\tif pkg.DsmTrickModeFlag == 1 {\n\t\tfile.WriteString(fmt.Sprintf(\"trick mode control:%d\\n\", pkg.TrickModeControl))\n\t}\n\n\tif pkg.AdditionalCopyInfoFlag == 1 {\n\t\tfile.WriteString(fmt.Sprintf(\"additional copy info:%d\\n\", pkg.AdditionalCopyInfo))\n\t}\n\n\tif pkg.PesCrcFlag == 1 {\n\t\tfile.WriteString(fmt.Sprintf(\"previous pes packet crc:%d\\n\", pkg.PreviousPesPacketCrc))\n\t}\n\tfile.WriteString(\"pes packet data byte:\\n\")\n\tfile.WriteString(fmt.Sprintf(\"  size: %d\\n\", len(pkg.PesPayload)))\n\tfile.WriteString(\"  data:\")\n\tfor i := 0; i < 12 && i < len(pkg.PesPayload); i++ {\n\t\tif i%4 == 0 {\n\t\t\tfile.WriteString(\"\\n\")\n\t\t\tfile.WriteString(\"      \")\n\t\t}\n\t\tfile.WriteString(fmt.Sprintf(\"0x%02x \", pkg.PesPayload[i]))\n\t}\n\tfile.WriteString(\"\\n\")\n}\n\nfunc (pkg *PesPacket) Decode(bs *BitStream) error {\n\tif bs.RemainBytes() < 9 {\n\t\treturn errNeedMore\n\t}\n\tbs.SkipBits(24)            //packet_start_code_prefix\n\tpkg.StreamId = bs.Uint8(8) //stream_id\n\tpkg.PesPacketLength = bs.Uint16(16)\n\tbs.SkipBits(2) //'10'\n\tpkg.PesScramblingControl = bs.Uint8(2)\n\tpkg.PesPriority = bs.Uint8(1)\n\tpkg.DataAlignmentIndicator = bs.Uint8(1)\n\tpkg.Copyright = bs.Uint8(1)\n\tpkg.OriginalOrCopy = bs.Uint8(1)\n\tpkg.PtsDtsFlags = bs.Uint8(2)\n\tpkg.EscrFlag = bs.Uint8(1)\n\tpkg.EsRateFlag = bs.Uint8(1)\n\tpkg.DsmTrickModeFlag = bs.Uint8(1)\n\tpkg.AdditionalCopyInfoFlag = bs.Uint8(1)\n\tpkg.PesCrcFlag = bs.Uint8(1)\n\tpkg.PesExtensionFlag = bs.Uint8(1)\n\tpkg.PesHeaderDataLength = bs.Uint8(8)\n\tif bs.RemainBytes() < int(pkg.PesHeaderDataLength) {\n\t\tbs.UnRead(9 * 8)\n\t\treturn errNeedMore\n\t}\n\tbs.Markdot()\n\tif pkg.PtsDtsFlags&0x02 == 0x02 {\n\t\tbs.SkipBits(4)\n\t\tpkg.Pts = bs.GetBits(3)\n\t\tbs.SkipBits(1)\n\t\tpkg.Pts = (pkg.Pts << 15) | bs.GetBits(15)\n\t\tbs.SkipBits(1)\n\t\tpkg.Pts = (pkg.Pts << 15) | bs.GetBits(15)\n\t\tbs.SkipBits(1)\n\t}\n\tif pkg.PtsDtsFlags&0x03 == 0x03 {\n\t\tbs.SkipBits(4)\n\t\tpkg.Dts = bs.GetBits(3)\n\t\tbs.SkipBits(1)\n\t\tpkg.Dts = (pkg.Dts << 15) | bs.GetBits(15)\n\t\tbs.SkipBits(1)\n\t\tpkg.Dts = (pkg.Dts << 15) | bs.GetBits(15)\n\t\tbs.SkipBits(1)\n\t} else {\n\t\tpkg.Dts = pkg.Pts\n\t}\n\n\tif pkg.EscrFlag == 1 {\n\t\tbs.SkipBits(2)\n\t\tpkg.EscrBase = bs.GetBits(3)\n\t\tbs.SkipBits(1)\n\t\tpkg.EscrBase = (pkg.Pts << 15) | bs.GetBits(15)\n\t\tbs.SkipBits(1)\n\t\tpkg.EscrBase = (pkg.Pts << 15) | bs.GetBits(15)\n\t\tbs.SkipBits(1)\n\t\tpkg.EscrExtension = bs.Uint16(9)\n\t\tbs.SkipBits(1)\n\t}\n\n\tif pkg.EsRateFlag == 1 {\n\t\tbs.SkipBits(1)\n\t\tpkg.EsRate = bs.Uint32(22)\n\t\tbs.SkipBits(1)\n\t}\n\n\tif pkg.DsmTrickModeFlag == 1 {\n\t\tpkg.TrickModeControl = bs.Uint8(3)\n\t\tpkg.TrickValue = bs.Uint8(5)\n\t}\n\n\tif pkg.AdditionalCopyInfoFlag == 1 {\n\t\tpkg.AdditionalCopyInfo = bs.Uint8(7)\n\t}\n\n\tif pkg.PesCrcFlag == 1 {\n\t\tpkg.PreviousPesPacketCrc = bs.Uint16(16)\n\t}\n\n\tloc := bs.DistanceFromMarkDot()\n\tbs.SkipBits(int(pkg.PesHeaderDataLength)*8 - loc) // skip remaining header\n\n\t// the -3 bytes are the combined lengths\n\t// of all fields between PesHeaderDataLength and PesHeaderDataLength (2 bytes)\n\t// and the PesHeaderDataLength itself (1 byte)\n\tdataLen := int(pkg.PesPacketLength - 3 - uint16(pkg.PesHeaderDataLength))\n\n\tif bs.RemainBytes() < dataLen {\n\t\tpkg.PesPayload = bs.RemainData()\n\t\tbs.UnRead((9 + int(pkg.PesHeaderDataLength)) * 8)\n\t\treturn errNeedMore\n\t}\n\n\tif pkg.PesPacketLength == 0 || bs.RemainBytes() <= dataLen {\n\t\tpkg.PesPayload = bs.RemainData()\n\t\tbs.SkipBits(bs.RemainBits())\n\t} else {\n\t\tpkg.PesPayload = bs.RemainData()[:dataLen]\n\t\tbs.SkipBits(dataLen * 8)\n\t}\n\n\treturn nil\n}\n\nfunc (pkg *PesPacket) DecodeMpeg1(bs *BitStream) error {\n\tif bs.RemainBytes() < 6 {\n\t\treturn errNeedMore\n\t}\n\tbs.SkipBits(24)            //packet_start_code_prefix\n\tpkg.StreamId = bs.Uint8(8) //stream_id\n\tpkg.PesPacketLength = bs.Uint16(16)\n\tif pkg.PesPacketLength != 0 && bs.RemainBytes() < int(pkg.PesPacketLength) {\n\t\tbs.UnRead(6 * 8)\n\t\treturn errNeedMore\n\t}\n\tbs.Markdot()\n\tfor bs.NextBits(8) == 0xFF {\n\t\tbs.SkipBits(8)\n\t}\n\tif bs.NextBits(2) == 0x01 {\n\t\tbs.SkipBits(16)\n\t}\n\tif bs.NextBits(4) == 0x02 {\n\t\tbs.SkipBits(4)\n\t\tpkg.Pts = bs.GetBits(3)\n\t\tbs.SkipBits(1)\n\t\tpkg.Pts = pkg.Pts<<15 | bs.GetBits(15)\n\t\tbs.SkipBits(1)\n\t\tpkg.Pts = pkg.Pts<<15 | bs.GetBits(15)\n\t\tbs.SkipBits(1)\n\t} else if bs.NextBits(4) == 0x03 {\n\t\tbs.SkipBits(4)\n\t\tpkg.Pts = bs.GetBits(3)\n\t\tbs.SkipBits(1)\n\t\tpkg.Pts = pkg.Pts<<15 | bs.GetBits(15)\n\t\tbs.SkipBits(1)\n\t\tpkg.Pts = pkg.Pts<<15 | bs.GetBits(15)\n\t\tbs.SkipBits(1)\n\t\tpkg.Dts = bs.GetBits(3)\n\t\tbs.SkipBits(1)\n\t\tpkg.Dts = pkg.Pts<<15 | bs.GetBits(15)\n\t\tbs.SkipBits(1)\n\t\tpkg.Dts = pkg.Pts<<15 | bs.GetBits(15)\n\t\tbs.SkipBits(1)\n\t} else if bs.NextBits(8) == 0x0F {\n\t\tbs.SkipBits(8)\n\t} else {\n\t\treturn errParser\n\t}\n\tloc := bs.DistanceFromMarkDot() / 8\n\tif pkg.PesPacketLength < uint16(loc) {\n\t\treturn errParser\n\t}\n\tif pkg.PesPacketLength == 0 ||\n\t\tbs.RemainBits() <= int(pkg.PesPacketLength-uint16(loc))*8 {\n\t\tpkg.PesPayload = bs.RemainData()\n\t\tbs.SkipBits(bs.RemainBits())\n\t} else {\n\t\tpkg.PesPayload = bs.RemainData()[:pkg.PesPacketLength-uint16(loc)]\n\t\tbs.SkipBits(int(pkg.PesPacketLength-uint16(loc)) * 8)\n\t}\n\treturn nil\n}\n\nfunc (pkg *PesPacket) Encode(bsw *BitStreamWriter) {\n\tbsw.PutBytes([]byte{0x00, 0x00, 0x01})\n\tbsw.PutByte(pkg.StreamId)\n\tbsw.PutUint16(pkg.PesPacketLength, 16)\n\tbsw.PutUint8(0x02, 2)\n\tbsw.PutUint8(pkg.PesScramblingControl, 2)\n\tbsw.PutUint8(pkg.PesPriority, 1)\n\tbsw.PutUint8(pkg.DataAlignmentIndicator, 1)\n\tbsw.PutUint8(pkg.Copyright, 1)\n\tbsw.PutUint8(pkg.OriginalOrCopy, 1)\n\tbsw.PutUint8(pkg.PtsDtsFlags, 2)\n\tbsw.PutUint8(pkg.EscrFlag, 1)\n\tbsw.PutUint8(pkg.EsRateFlag, 1)\n\tbsw.PutUint8(pkg.DsmTrickModeFlag, 1)\n\tbsw.PutUint8(pkg.AdditionalCopyInfoFlag, 1)\n\tbsw.PutUint8(pkg.PesCrcFlag, 1)\n\tbsw.PutUint8(pkg.PesExtensionFlag, 1)\n\tbsw.PutByte(pkg.PesHeaderDataLength)\n\tif pkg.PtsDtsFlags == 0x02 {\n\t\tbsw.PutUint8(0x02, 4)\n\t\tbsw.PutUint64(pkg.Pts>>30, 3)\n\t\tbsw.PutUint8(0x01, 1)\n\t\tbsw.PutUint64(pkg.Pts>>15, 15)\n\t\tbsw.PutUint8(0x01, 1)\n\t\tbsw.PutUint64(pkg.Pts, 15)\n\t\tbsw.PutUint8(0x01, 1)\n\t}\n\n\tif pkg.PtsDtsFlags == 0x03 {\n\t\tbsw.PutUint8(0x03, 4)\n\t\tbsw.PutUint64(pkg.Pts>>30, 3)\n\t\tbsw.PutUint8(0x01, 1)\n\t\tbsw.PutUint64(pkg.Pts>>15, 15)\n\t\tbsw.PutUint8(0x01, 1)\n\t\tbsw.PutUint64(pkg.Pts, 15)\n\t\tbsw.PutUint8(0x01, 1)\n\t\tbsw.PutUint8(0x01, 4)\n\t\tbsw.PutUint64(pkg.Dts>>30, 3)\n\t\tbsw.PutUint8(0x01, 1)\n\t\tbsw.PutUint64(pkg.Dts>>15, 15)\n\t\tbsw.PutUint8(0x01, 1)\n\t\tbsw.PutUint64(pkg.Dts, 15)\n\t\tbsw.PutUint8(0x01, 1)\n\t}\n\n\tif pkg.EscrFlag == 1 {\n\t\tbsw.PutUint8(0x03, 2)\n\t\tbsw.PutUint64(pkg.EscrBase>>30, 3)\n\t\tbsw.PutUint8(0x01, 1)\n\t\tbsw.PutUint64(pkg.EscrBase>>15, 15)\n\t\tbsw.PutUint8(0x01, 1)\n\t\tbsw.PutUint64(pkg.EscrBase, 15)\n\t\tbsw.PutUint8(0x01, 1)\n\t}\n\tbsw.PutBytes(pkg.PesPayload)\n}\n"
  },
  {
    "path": "gb28181/mpegps/ps_demuxer.go",
    "content": "package mpegps\n\n//单元来源于https://github.com/yapingcat/gomedia\nimport (\n\t\"errors\"\n\t\"github.com/q191201771/lal/pkg/avc\"\n\t\"github.com/q191201771/lal/pkg/hevc\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\ntype psStream struct {\n\tsid       uint8\n\tcid       PsStreamType\n\tpts       uint64\n\tdts       uint64\n\tstreamBuf []byte\n}\n\nfunc newPsStream(sid uint8, cid PsStreamType) *psStream {\n\treturn &psStream{\n\t\tsid:       sid,\n\t\tcid:       cid,\n\t\tstreamBuf: make([]byte, 0, 4096),\n\t}\n}\nfunc (p *psStream) setCid(cid PsStreamType) {\n\tp.cid = cid\n}\n\nfunc (p *psStream) clearBuf() {\n\tp.streamBuf = p.streamBuf[:0]\n}\n\ntype PsDemuxer struct {\n\tstreamMap map[uint8]*psStream\n\tpkg       *PsPacket\n\tmpeg1     bool\n\tcache     []byte\n\tOnFrame   func(frame []byte, cid PsStreamType, pts uint64, dts uint64)\n\t//解ps包过程中，解码回调psm，system header，pes包等\n\t//decodeResult 解码ps包时的产生的错误\n\t//这个回调主要用于debug，查看是否ps包存在问题\n\tOnPacket func(pkg Display, decodeResult error)\n\n\tverifyBuf []byte\n\n\tlog nazalog.Logger\n}\n\nfunc NewPsDemuxer() *PsDemuxer {\n\tpsDemuxer := &PsDemuxer{\n\t\tstreamMap: make(map[uint8]*psStream),\n\t\tpkg:       new(PsPacket),\n\t\tcache:     make([]byte, 0, 256),\n\t\tOnFrame:   nil,\n\t\tOnPacket:  nil,\n\t}\n\treturn psDemuxer\n}\n\nfunc (psDemuxer *PsDemuxer) Input(data []byte) error {\n\tvar bs *BitStream\n\tif len(psDemuxer.cache) > 0 {\n\t\tpsDemuxer.cache = append(psDemuxer.cache, data...)\n\t\tbs = NewBitStream(psDemuxer.cache)\n\t} else {\n\t\tbs = NewBitStream(data)\n\t}\n\n\tsaveReseved := func() {\n\t\ttmpcache := make([]byte, bs.RemainBytes())\n\t\tcopy(tmpcache, bs.RemainData())\n\t\tpsDemuxer.cache = tmpcache\n\t}\n\n\tvar ret error = nil\n\tfor !bs.EOS() {\n\t\tif mpegerr, ok := ret.(Error); ok {\n\t\t\tif mpegerr.NeedMore() {\n\t\t\t\tsaveReseved()\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tif bs.RemainBits() < 32 {\n\t\t\tret = errNeedMore\n\t\t\tsaveReseved()\n\t\t\tbreak\n\t\t}\n\t\tprefix_code := bs.NextBits(32)\n\t\tswitch prefix_code {\n\t\tcase 0x000001BA: //pack header\n\t\t\tif psDemuxer.pkg.Header == nil {\n\t\t\t\tpsDemuxer.pkg.Header = new(PsPackHeader)\n\t\t\t}\n\t\t\tret = psDemuxer.pkg.Header.Decode(bs)\n\t\t\tpsDemuxer.mpeg1 = psDemuxer.pkg.Header.IsMpeg1\n\t\t\tif psDemuxer.OnPacket != nil {\n\t\t\t\tpsDemuxer.OnPacket(psDemuxer.pkg.Header, ret)\n\t\t\t}\n\t\tcase 0x000001BB: //system header\n\t\t\tif psDemuxer.pkg.Header == nil {\n\t\t\t\treturn errors.New(\"PsDemuxer.pkg.Header must not be nil\")\n\t\t\t}\n\t\t\tif psDemuxer.pkg.System == nil {\n\t\t\t\tpsDemuxer.pkg.System = new(SystemHeader)\n\t\t\t}\n\t\t\tret = psDemuxer.pkg.System.Decode(bs)\n\t\t\tif psDemuxer.OnPacket != nil {\n\t\t\t\tpsDemuxer.OnPacket(psDemuxer.pkg.System, ret)\n\t\t\t}\n\t\tcase 0x000001BC: //program stream map\n\t\t\tif psDemuxer.pkg.Psm == nil {\n\t\t\t\tpsDemuxer.pkg.Psm = new(ProgramStreamMap)\n\t\t\t}\n\t\t\tif ret = psDemuxer.pkg.Psm.Decode(bs); ret == nil {\n\t\t\t\tfor _, streaminfo := range psDemuxer.pkg.Psm.StreamMap {\n\t\t\t\t\tif _, found := psDemuxer.streamMap[streaminfo.ElementaryStreamId]; !found {\n\t\t\t\t\t\tstream := newPsStream(streaminfo.ElementaryStreamId, PsStreamType(streaminfo.StreamType))\n\t\t\t\t\t\tpsDemuxer.streamMap[stream.sid] = stream\n\t\t\t\t\t} else {\n\t\t\t\t\t\tstream := psDemuxer.streamMap[streaminfo.ElementaryStreamId]\n\t\t\t\t\t\tstream.setCid(PsStreamType(streaminfo.StreamType))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tif psDemuxer.OnPacket != nil {\n\t\t\t\tpsDemuxer.OnPacket(psDemuxer.pkg.Psm, ret)\n\t\t\t}\n\t\tcase 0x000001BD, 0x000001BE, 0x000001BF, 0x000001F0, 0x000001F1,\n\t\t\t0x000001F2, 0x000001F3, 0x000001F4, 0x000001F5, 0x000001F6,\n\t\t\t0x000001F7, 0x000001F8, 0x000001F9, 0x000001FA, 0x000001FB:\n\t\t\tif psDemuxer.pkg.CommPes == nil {\n\t\t\t\tpsDemuxer.pkg.CommPes = new(CommonPesPacket)\n\t\t\t}\n\t\t\tret = psDemuxer.pkg.CommPes.Decode(bs)\n\t\tcase 0x000001FF: //program stream directory\n\t\t\tif psDemuxer.pkg.Psd == nil {\n\t\t\t\tpsDemuxer.pkg.Psd = new(ProgramStreamDirectory)\n\t\t\t}\n\t\t\tret = psDemuxer.pkg.Psd.Decode(bs)\n\t\tcase 0x000001B9: //MPEG_program_end_code\n\t\t\tcontinue\n\t\tdefault:\n\t\t\tif prefix_code&0xFFFFFFE0 == 0x000001C0 || prefix_code&0xFFFFFFE0 == 0x000001E0 {\n\t\t\t\tif psDemuxer.pkg.Pes == nil {\n\t\t\t\t\tpsDemuxer.pkg.Pes = NewPesPacket()\n\t\t\t\t}\n\t\t\t\tif psDemuxer.mpeg1 {\n\t\t\t\t\tret = psDemuxer.pkg.Pes.DecodeMpeg1(bs)\n\t\t\t\t} else {\n\t\t\t\t\tret = psDemuxer.pkg.Pes.Decode(bs)\n\t\t\t\t}\n\t\t\t\tif psDemuxer.OnPacket != nil {\n\t\t\t\t\tpsDemuxer.OnPacket(psDemuxer.pkg.Pes, ret)\n\t\t\t\t}\n\t\t\t\tif ret == nil {\n\t\t\t\t\tif stream, found := psDemuxer.streamMap[psDemuxer.pkg.Pes.StreamId]; found {\n\t\t\t\t\t\tif psDemuxer.mpeg1 && stream.cid == PsStreamUnknow {\n\t\t\t\t\t\t\tpsDemuxer.guessCodecid(stream)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tpsDemuxer.demuxPespacket(stream, psDemuxer.pkg.Pes)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif psDemuxer.mpeg1 {\n\t\t\t\t\t\t\tstream := newPsStream(psDemuxer.pkg.Pes.StreamId, PsStreamUnknow)\n\t\t\t\t\t\t\tpsDemuxer.streamMap[stream.sid] = stream\n\t\t\t\t\t\t\tstream.streamBuf = append(stream.streamBuf, psDemuxer.pkg.Pes.PesPayload...)\n\t\t\t\t\t\t\tstream.pts = psDemuxer.pkg.Pes.Pts\n\t\t\t\t\t\t\tstream.dts = psDemuxer.pkg.Pes.Dts\n\t\t\t\t\t\t} else if psDemuxer.pkg.Pes.StreamId == uint8(PesStreamVideo) {\n\t\t\t\t\t\t\tif len(psDemuxer.verifyBuf) > 256 {\n\t\t\t\t\t\t\t\tpsDemuxer.verifyBuf = psDemuxer.verifyBuf[:0]\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tpsDemuxer.verifyBuf = append(psDemuxer.verifyBuf, psDemuxer.pkg.Pes.PesPayload...)\n\t\t\t\t\t\t\tif h26x, err := mpegH26xVerify(psDemuxer.verifyBuf); err == nil {\n\t\t\t\t\t\t\t\tswitch h26x {\n\t\t\t\t\t\t\t\tcase CodecUnknown:\n\t\t\t\t\t\t\t\tcase CodecH264:\n\t\t\t\t\t\t\t\t\tstreamH264 := newPsStream(uint8(PesStreamVideo), PsStreamH264)\n\t\t\t\t\t\t\t\t\tpsDemuxer.streamMap[streamH264.sid] = streamH264\n\t\t\t\t\t\t\t\t\tpsDemuxer.demuxPespacket(streamH264, psDemuxer.pkg.Pes)\n\t\t\t\t\t\t\t\tcase CodecH265:\n\t\t\t\t\t\t\t\t\tstreamH265 := newPsStream(uint8(PesStreamVideo), PsStreamH265)\n\t\t\t\t\t\t\t\t\tpsDemuxer.streamMap[streamH265.sid] = streamH265\n\t\t\t\t\t\t\t\t\tpsDemuxer.demuxPespacket(streamH265, psDemuxer.pkg.Pes)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} else if psDemuxer.pkg.Pes.StreamId == uint8(PesStreamAudio) {\n\t\t\t\t\t\t\tif _, found = psDemuxer.streamMap[uint8(PesStreamVideo)]; found {\n\t\t\t\t\t\t\t\tpsStreamType := audioVerify(psDemuxer.pkg.Pes.PesPayload)\n\t\t\t\t\t\t\t\tstreamAudio := newPsStream(uint8(PesStreamAudio), psStreamType)\n\t\t\t\t\t\t\t\tpsDemuxer.streamMap[streamAudio.sid] = streamAudio\n\t\t\t\t\t\t\t\tpsDemuxer.demuxPespacket(streamAudio, psDemuxer.pkg.Pes)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tbs.SkipBits(8)\n\t\t\t}\n\t\t}\n\t}\n\n\tif ret == nil && len(psDemuxer.cache) > 0 {\n\t\tpsDemuxer.cache = nil\n\t}\n\n\treturn ret\n}\n\nfunc (psDemuxer *PsDemuxer) Flush() {\n\tfor _, stream := range psDemuxer.streamMap {\n\t\tif len(stream.streamBuf) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif psDemuxer.OnFrame != nil {\n\t\t\tpsDemuxer.OnFrame(stream.streamBuf, stream.cid, stream.pts/90, stream.dts/90)\n\t\t}\n\t}\n}\n\nfunc (psDemuxer *PsDemuxer) guessCodecid(stream *psStream) {\n\tif stream.sid&0xE0 == uint8(PesStreamAudio) {\n\t\tpsStreamType := audioVerify(psDemuxer.pkg.Pes.PesPayload)\n\t\tstream.cid = psStreamType\n\t} else if stream.sid&0xE0 == uint8(PesStreamVideo) {\n\t\tif h26x, err := mpegH26xVerify(stream.streamBuf); err == nil {\n\t\t\tswitch h26x {\n\t\t\tcase CodecUnknown:\n\t\t\tcase CodecH264:\n\t\t\t\tstream.cid = PsStreamH264\n\t\t\tcase CodecH265:\n\t\t\t\tstream.cid = PsStreamH265\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (psDemuxer *PsDemuxer) demuxPespacket(stream *psStream, pes *PesPacket) error {\n\tswitch stream.cid {\n\tcase PsStreamAac, PsStreamG711A, PsStreamG711U:\n\t\treturn psDemuxer.demuxAudio(stream, pes)\n\tcase PsStreamH264, PsStreamH265:\n\t\treturn psDemuxer.demuxH26x(stream, pes)\n\tcase PsStreamUnknow:\n\t\tif stream.pts != pes.Pts {\n\t\t\tstream.streamBuf = nil\n\t\t}\n\t\tstream.streamBuf = append(stream.streamBuf, pes.PesPayload...)\n\t\tstream.pts = pes.Pts\n\t\tstream.dts = pes.Dts\n\t}\n\treturn nil\n}\n\nfunc (psDemuxer *PsDemuxer) demuxAudio(stream *psStream, pes *PesPacket) error {\n\tif psDemuxer.OnFrame != nil {\n\t\tpsDemuxer.OnFrame(pes.PesPayload, stream.cid, pes.Pts/90, pes.Dts/90)\n\t}\n\treturn nil\n}\n\nfunc (psDemuxer *PsDemuxer) demuxH26x(stream *psStream, pes *PesPacket) error {\n\tif stream.pts == 0 {\n\t\tstream.streamBuf = append(stream.streamBuf, pes.PesPayload...)\n\t\tstream.pts = pes.Pts\n\t\tstream.dts = pes.Dts\n\t} else if stream.pts == pes.Pts || pes.Pts == 0 {\n\t\tstream.streamBuf = append(stream.streamBuf, pes.PesPayload...)\n\t} else {\n\t\tstart, sc := FindStartCode(stream.streamBuf, 0)\n\t\tfor start >= 0 && start < len(stream.streamBuf) {\n\t\t\tend, sc2 := FindStartCode(stream.streamBuf, start+int(sc))\n\t\t\tif end < 0 {\n\t\t\t\tend = len(stream.streamBuf)\n\t\t\t}\n\t\t\tif stream.cid == PsStreamH264 {\n\t\t\t\tnaluType := H264NaluType(stream.streamBuf[start:])\n\t\t\t\tif naluType != avc.NaluTypeAud {\n\t\t\t\t\tif psDemuxer.OnFrame != nil {\n\t\t\t\t\t\tpsDemuxer.OnFrame(stream.streamBuf[start:end], stream.cid, stream.pts/90, stream.dts/90)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if stream.cid == PsStreamH265 {\n\t\t\t\tnaluType := H265NaluType(stream.streamBuf[start:])\n\t\t\t\tif naluType != hevc.NaluTypeAud {\n\t\t\t\t\tif psDemuxer.OnFrame != nil {\n\t\t\t\t\t\tpsDemuxer.OnFrame(stream.streamBuf[start:end], stream.cid, stream.pts/90, stream.dts/90)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tstart = end\n\t\t\tsc = sc2\n\t\t}\n\t\tstream.streamBuf = nil\n\t\tstream.streamBuf = append(stream.streamBuf, pes.PesPayload...)\n\t\tstream.pts = pes.Pts\n\t\tstream.dts = pes.Dts\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "gb28181/mpegps/ps_demuxer_test.go",
    "content": "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/rtprtcp\"\n\t\"github.com/q191201771/naza/pkg/nazabytes\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n\t\"io\"\n\t\"os\"\n\t\"testing\"\n)\n\nvar ps1 []byte = []byte{0x00, 0x00, 0x01, 0xBA}\nvar ps2 []byte = []byte{0x00, 0x00, 0x01, 0xBA, 0x40, 0x01, 0x00, 0x01, 0x33, 0x44, 0xFF, 0xFF, 0xFF, 0xF1, 0xFF}\n\nvar ps3 []byte = []byte{0x00, 0x00, 0x01, 0xBA, 0x40, 0x01, 0x00, 0x01, 0x33, 0x44, 0xFF, 0xFF, 0xFF, 0xF0, 0x00, 0x00, 0x01, 0xBB}\nvar ps4 []byte = []byte{0x00, 0x00, 0x01, 0xBA, 0x40, 0x01, 0x00, 0x01, 0x33, 0x44, 0xFF, 0xFF, 0xFF, 0xF1, 0x34, 0x00, 0x00, 0x01, 0xBB, 0x00, 0x01, 0x00, 0x01, 0x33, 0x44, 0xFF, 0x34}\nvar ps5 []byte = []byte{0x00, 0x00, 0x01, 0xBA, 0x40, 0x01, 0x00, 0x01, 0x33, 0x44, 0xFF, 0xFF, 0xFF, 0xF1, 0x34, 0x00, 0x00, 0x01, 0xBB, 0x00, 0x09, 0x00, 0x01, 0x33, 0x44, 0xFF, 0x34, 0x81, 0x00, 0x00}\nvar ps6 []byte = []byte{0x00, 0x00, 0x01, 0xBC, 0x40, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x34, 0x81, 0x00, 0x00}\nvar ps7 []byte = []byte{0x00, 0x00, 0x01, 0xBA, 0x20, 0x0a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03}\n\nfunc TestPSDemuxer_Input(t *testing.T) {\n\ttype fields struct {\n\t\tstreamMap map[uint8]*psStream\n\t\tpkg       *PsPacket\n\t\tcache     []byte\n\t\tOnPacket  func(pkg Display, decodeResult error)\n\t\tOnFrame   func(frame []byte, cid PsStreamType, pts uint64, dts uint64)\n\t}\n\ttype args struct {\n\t\tdata []byte\n\t}\n\ttests := []struct {\n\t\tname    string\n\t\tfields  fields\n\t\targs    args\n\t\twantErr bool\n\t}{\n\t\t{name: \"test1\", fields: fields{\n\t\t\tstreamMap: make(map[uint8]*psStream),\n\t\t\tpkg:       new(PsPacket),\n\t\t}, args: args{data: ps1}, wantErr: true},\n\n\t\t{name: \"test2\", fields: fields{\n\t\t\tstreamMap: make(map[uint8]*psStream),\n\t\t\tpkg:       new(PsPacket),\n\t\t}, args: args{data: ps2}, wantErr: false},\n\n\t\t{name: \"test3\", fields: fields{\n\t\t\tstreamMap: make(map[uint8]*psStream),\n\t\t\tpkg:       new(PsPacket),\n\t\t}, args: args{data: ps3}, wantErr: true},\n\n\t\t{name: \"test4\", fields: fields{\n\t\t\tstreamMap: make(map[uint8]*psStream),\n\t\t\tpkg:       new(PsPacket),\n\t\t}, args: args{data: ps4}, wantErr: true},\n\n\t\t{name: \"test5\", fields: fields{\n\t\t\tstreamMap: make(map[uint8]*psStream),\n\t\t\tpkg:       new(PsPacket),\n\t\t}, args: args{data: ps5}, wantErr: false},\n\t\t{name: \"test6\", fields: fields{\n\t\t\tstreamMap: make(map[uint8]*psStream),\n\t\t\tpkg:       new(PsPacket),\n\t\t}, args: args{data: ps6}, wantErr: false},\n\t\t{name: \"test-mpeg1\", fields: fields{\n\t\t\tstreamMap: make(map[uint8]*psStream),\n\t\t\tpkg:       new(PsPacket),\n\t\t}, args: args{data: ps7}, wantErr: false},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tpsdemuxer := &PsDemuxer{\n\t\t\t\tstreamMap: tt.fields.streamMap,\n\t\t\t\tpkg:       tt.fields.pkg,\n\t\t\t\tcache:     tt.fields.cache,\n\t\t\t\tOnPacket:  tt.fields.OnPacket,\n\t\t\t\tOnFrame:   tt.fields.OnFrame,\n\t\t\t}\n\t\t\tif err := psdemuxer.Input(tt.args.data); (err != nil) != tt.wantErr {\n\t\t\t\tt.Errorf(\"PSDemuxer.Input() error = %v, wantErr %v\", err, tt.wantErr)\n\t\t\t}\n\t\t})\n\t}\n}\nfunc TestPSDemuxer(t *testing.T) {\n\tvar psUnpacker *PsDemuxer\n\tos.Remove(\"h.ps\")\n\tos.Remove(\"h.h264\")\n\tos.Remove(\"ps_demux_result\")\n\tdumpFile := base.NewDumpFile()\n\terr := dumpFile.OpenToRead(\"C:\\\\Users\\\\Administrator\\\\Desktop\\\\dump_37060000001320000001001.raw\")\n\tif err != nil {\n\t\tfmt.Println(err)\n\t\treturn\n\t}\n\tpsUnpacker = NewPsDemuxer()\n\tpsUnpacker.OnFrame = func(frame []byte, cid PsStreamType, pts uint64, dts uint64) {\n\t\tif cid == PsStreamH264 || cid == PsStreamH265 {\n\t\t\twriteFile(\"h.h264\", frame)\n\t\t} else {\n\t\t\tif cid == PsStreamG711A {\n\t\t\t\tnazalog.Infof(\"存在音频g711A 大小：%d  dts:%d\", len(frame), dts)\n\t\t\t} else if cid == PsStreamG711U {\n\t\t\t\tnazalog.Infof(\"存在音频g711U 大小：%d  dts:%d\", len(frame), dts)\n\t\t\t} else {\n\t\t\t\tnazalog.Infof(\"存在音频aac 大小：%d dts:%d\", len(frame), dts)\n\t\t\t}\n\t\t}\n\n\t}\n\tfd3, err := os.OpenFile(\"ps_demux_result\", os.O_CREATE|os.O_RDWR, 0666)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t\treturn\n\t}\n\tdefer fd3.Close()\n\tpsUnpacker.OnPacket = func(pkg Display, decodeResult error) {\n\t\tswitch value := pkg.(type) {\n\t\tcase *PsPackHeader:\n\t\t\tfd3.WriteString(\"--------------PS Pack Header--------------\\n\")\n\t\t\tif decodeResult == nil {\n\t\t\t\tvalue.PrettyPrint(fd3)\n\t\t\t} else {\n\t\t\t\tfd3.WriteString(fmt.Sprintf(\"Decode Ps Packet Failed %s\\n\", decodeResult.Error()))\n\t\t\t}\n\t\tcase *SystemHeader:\n\t\t\tfd3.WriteString(\"--------------System Header--------------\\n\")\n\t\t\tif decodeResult == nil {\n\t\t\t\tvalue.PrettyPrint(fd3)\n\t\t\t} else {\n\t\t\t\tfd3.WriteString(fmt.Sprintf(\"Decode Ps Packet Failed %s\\n\", decodeResult.Error()))\n\t\t\t}\n\t\tcase *ProgramStreamMap:\n\t\t\tfd3.WriteString(\"--------------------PSM-------------------\\n\")\n\t\t\tif decodeResult == nil {\n\t\t\t\tvalue.PrettyPrint(fd3)\n\t\t\t} else {\n\t\t\t\tfd3.WriteString(fmt.Sprintf(\"Decode Ps Packet Failed %s\\n\", decodeResult.Error()))\n\t\t\t}\n\t\tcase *PesPacket:\n\t\t\tfd3.WriteString(\"-------------------PES--------------------\\n\")\n\t\t\tif decodeResult == nil {\n\t\t\t\tvalue.PrettyPrint(fd3)\n\t\t\t} else {\n\t\t\t\tfd3.WriteString(fmt.Sprintf(\"Decode Ps Packet Failed %s\\n\", decodeResult.Error()))\n\t\t\t}\n\t\t}\n\t}\n\n\tif err != nil {\n\t\treturn\n\t}\n\tpacke := 0\n\tSeq := 0\n\tfor {\n\t\tm, err := dumpFile.ReadOneMessage()\n\t\tif err == io.EOF {\n\t\t\tbreak\n\t\t}\n\t\tipkt, err := rtprtcp.ParseRtpPacket(m.Body)\n\t\tif err != nil {\n\t\t\tnazalog.Errorf(\"PsUnpacker ParseRtpPacket failed. b=%s, err=%+v\",\n\t\t\t\thex.Dump(nazabytes.Prefix(m.Body, 64)), err)\n\t\t\tcontinue\n\t\t}\n\t\tpacke++\n\t\tif ipkt.Header.Seq-uint16(Seq) != 1 {\n\t\t\tfmt.Printf(\"pkt Seq:%d ssrc:%d \\n\", ipkt.Header.Seq, ipkt.Header.Ssrc)\n\t\t}\n\t\tSeq = int(ipkt.Header.Seq)\n\t\tbody := ipkt.Body()\n\t\twriteFile(\"h.ps\", body)\n\t\tfmt.Println(psUnpacker.Input(body))\n\t}\n\n}\nfunc fileExists(fileName string) (bool, error) {\n\t_, err := os.Stat(fileName)\n\tif err == nil {\n\t\treturn true, nil\n\t}\n\tif os.IsNotExist(err) {\n\t\treturn false, nil\n\t}\n\treturn false, err\n}\nfunc writeFile(filename string, buffer []byte) (err error) {\n\tvar fp *os.File\n\tb, err := fileExists(filename)\n\tif err != nil {\n\t\treturn\n\t}\n\tif !b {\n\t\tfp, err = os.Create(filename)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tfp, err = os.OpenFile(filename, os.O_CREATE|os.O_APPEND, 6)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\tdefer fp.Close()\n\t_, err = fp.Write(buffer)\n\n\treturn\n}\n"
  },
  {
    "path": "gb28181/mpegps/ps_muxer.go",
    "content": "package mpegps\n\n//单元来源于https://github.com/yapingcat/gomedia\nimport (\n\t\"github.com/q191201771/lal/pkg/avc\"\n\t\"github.com/q191201771/lal/pkg/hevc\"\n)\n\ntype PsMuxer struct {\n\tsystem     *SystemHeader\n\tpsm        *ProgramStreamMap\n\tOnPacket   func(pkg []byte, pts uint64)\n\tfirstframe bool\n}\n\nfunc NewPsMuxer() *PsMuxer {\n\tmuxer := new(PsMuxer)\n\tmuxer.firstframe = true\n\tmuxer.system = new(SystemHeader)\n\tmuxer.system.RateBound = 26234\n\tmuxer.psm = new(ProgramStreamMap)\n\tmuxer.psm.CurrentNextIndicator = 1\n\tmuxer.psm.ProgramStreamMapVersion = 1\n\tmuxer.OnPacket = nil\n\treturn muxer\n}\n\nfunc (muxer *PsMuxer) AddStream(cid PsStreamType) uint8 {\n\tif cid == PsStreamH265 || cid == PsStreamH264 {\n\t\tes := NewElementaryStream(uint8(PesStreamVideo) + muxer.system.VideoBound)\n\t\tes.PStdBufferBoundScale = 1\n\t\tes.PStdBufferSizeBound = 400\n\t\tmuxer.system.Streams = append(muxer.system.Streams, es)\n\t\tmuxer.system.VideoBound++\n\t\tmuxer.psm.StreamMap = append(muxer.psm.StreamMap, NewElementaryStreamElem(uint8(cid), es.StreamId))\n\t\tmuxer.psm.ProgramStreamMapVersion++\n\t\treturn es.StreamId\n\t} else {\n\t\tes := NewElementaryStream(uint8(PesStreamAudio) + muxer.system.AudioBound)\n\t\tes.PStdBufferBoundScale = 0\n\t\tes.PStdBufferSizeBound = 32\n\t\tmuxer.system.Streams = append(muxer.system.Streams, es)\n\t\tmuxer.system.AudioBound++\n\t\tmuxer.psm.StreamMap = append(muxer.psm.StreamMap, NewElementaryStreamElem(uint8(cid), es.StreamId))\n\t\tmuxer.psm.ProgramStreamMapVersion++\n\t\treturn es.StreamId\n\t}\n}\n\nfunc (muxer *PsMuxer) Write(sid uint8, frame []byte, pts uint64, dts uint64) error {\n\tvar stream *ElementaryStreamElem = nil\n\tfor _, es := range muxer.psm.StreamMap {\n\t\tif es.ElementaryStreamId == sid {\n\t\t\tstream = es\n\t\t\tbreak\n\t\t}\n\t}\n\tif stream == nil {\n\t\treturn errNotFound\n\t}\n\tif len(frame) <= 0 {\n\t\treturn nil\n\t}\n\tvar withaud bool = false\n\tvar idrFlag bool = false\n\tvar first bool = true\n\tvar vcl bool = false\n\tif stream.StreamType == uint8(PsStreamH264) || stream.StreamType == uint8(PsStreamH265) {\n\t\tSplitFrame(frame, func(nalu []byte) bool {\n\t\t\tif stream.StreamType == uint8(PsStreamH264) {\n\t\t\t\tnaluType := avc.ParseNaluType(nalu[0])\n\t\t\t\tif naluType == avc.NaluTypeAud {\n\t\t\t\t\twithaud = true\n\t\t\t\t\treturn false\n\t\t\t\t} else if naluType >= avc.NaluTypeSlice && naluType <= avc.NaluTypeIdrSlice {\n\t\t\t\t\tif naluType == avc.NaluTypeIdrSlice {\n\t\t\t\t\t\tidrFlag = true\n\t\t\t\t\t}\n\t\t\t\t\tvcl = true\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t} else {\n\t\t\t\tnaluType := hevc.ParseNaluType(nalu[0])\n\t\t\t\tif naluType == hevc.NaluTypeAud {\n\t\t\t\t\twithaud = true\n\t\t\t\t\treturn false\n\t\t\t\t} else if naluType >= hevc.NaluTypeSliceBlaWlp && naluType <= hevc.NaluTypeSliceRsvIrapVcl23 ||\n\t\t\t\t\tnaluType >= hevc.NaluTypeSliceTrailN && naluType <= hevc.NaluTypeSliceRaslR {\n\t\t\t\t\tif naluType >= hevc.NaluTypeSliceBlaWlp && naluType <= hevc.NaluTypeSliceRsvIrapVcl23 {\n\t\t\t\t\t\tidrFlag = true\n\t\t\t\t\t}\n\t\t\t\t\tvcl = true\n\t\t\t\t\treturn false\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t}\n\t\t})\n\t}\n\n\tdts = dts * 90\n\tpts = pts * 90\n\tbsw := NewBitStreamWriter(1024)\n\tvar pack PsPackHeader\n\tpack.SystemClockReferenceBase = dts - 3600\n\tpack.SystemClockReferenceExtension = 0\n\tpack.ProgramMuxRate = 6106\n\tpack.Encode(bsw)\n\tif muxer.firstframe || idrFlag {\n\t\tmuxer.system.Encode(bsw)\n\t\tmuxer.psm.Encode(bsw)\n\t\tmuxer.firstframe = false\n\t}\n\tpespkg := NewPesPacket()\n\tfor len(frame) > 0 {\n\t\tpeshdrlen := 13\n\t\tpespkg.StreamId = sid\n\t\tpespkg.PtsDtsFlags = 0x03\n\t\tpespkg.PesHeaderDataLength = 10\n\t\tpespkg.Pts = pts\n\t\tpespkg.Dts = dts\n\t\tif idrFlag {\n\t\t\tpespkg.DataAlignmentIndicator = 1\n\t\t}\n\t\tif first && !withaud && vcl {\n\t\t\tif stream.StreamType == uint8(PsStreamH264) {\n\t\t\t\tpespkg.PesPayload = append(pespkg.PesPayload, H264AudNalu...)\n\t\t\t\tpeshdrlen += 6\n\t\t\t} else if stream.StreamType == uint8(PsStreamH265) {\n\t\t\t\tpespkg.PesPayload = append(pespkg.PesPayload, H265AudNalu...)\n\t\t\t\tpeshdrlen += 7\n\t\t\t}\n\t\t}\n\t\tif peshdrlen+len(frame) >= 0xFFFF {\n\t\t\tpespkg.PesPacketLength = 0xFFFF\n\t\t\tpespkg.PesPayload = append(pespkg.PesPayload, frame[0:0xFFFF-peshdrlen]...)\n\t\t\tframe = frame[0xFFFF-peshdrlen:]\n\t\t} else {\n\t\t\tpespkg.PesPacketLength = uint16(peshdrlen + len(frame))\n\t\t\tpespkg.PesPayload = append(pespkg.PesPayload, frame[0:]...)\n\t\t\tframe = frame[:0]\n\t\t}\n\t\tpespkg.Encode(bsw)\n\t\tpespkg.PesPayload = pespkg.PesPayload[:0]\n\t\tif muxer.OnPacket != nil {\n\t\t\tmuxer.OnPacket(bsw.Bits(), pts)\n\t\t}\n\t\tbsw.Reset()\n\t\tfirst = false\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "gb28181/mpegps/ps_proto.go",
    "content": "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\tParserError() bool\n\tStreamIdNotFound() bool\n}\n\nvar errNeedMore error = &needmoreError{}\n\ntype needmoreError struct{}\n\nfunc (e *needmoreError) Error() string          { return \"need more bytes\" }\nfunc (e *needmoreError) NeedMore() bool         { return true }\nfunc (e *needmoreError) ParserError() bool      { return false }\nfunc (e *needmoreError) StreamIdNotFound() bool { return false }\n\nvar errParser error = &parserError{}\n\ntype parserError struct{}\n\nfunc (e *parserError) Error() string          { return \"parser packet error\" }\nfunc (e *parserError) NeedMore() bool         { return false }\nfunc (e *parserError) ParserError() bool      { return true }\nfunc (e *parserError) StreamIdNotFound() bool { return false }\n\nvar errNotFound error = &sidNotFoundError{}\n\ntype sidNotFoundError struct{}\n\nfunc (e *sidNotFoundError) Error() string          { return \"stream id not found\" }\nfunc (e *sidNotFoundError) NeedMore() bool         { return false }\nfunc (e *sidNotFoundError) ParserError() bool      { return false }\nfunc (e *sidNotFoundError) StreamIdNotFound() bool { return true }\n\ntype PsStreamType int\n\nconst (\n\tPsStreamUnknow PsStreamType = 0xFF\n\tPsStreamAac    PsStreamType = 0x0F\n\tPsStreamH264   PsStreamType = 0x1B\n\tPsStreamH265   PsStreamType = 0x24\n\tPsStreamG711A  PsStreamType = 0x90\n\tPsStreamG711U  PsStreamType = 0x91\n)\n\n// Table 2-33 – Program Stream pack header\n// pack_header() {\n//     pack_start_code                                     32      bslbf\n//     '01'                                                2         bslbf\n//     system_clock_reference_base [32..30]                 3         bslbf\n//     marker_bit                                           1         bslbf\n//     system_clock_reference_base [29..15]                 15         bslbf\n//     marker_bit                                           1         bslbf\n//     system_clock_reference_base [14..0]                  15         bslbf\n//     marker_bit                                           1         bslbf\n//     system_clock_reference_extension                     9         uimsbf\n//     marker_bit                                           1         bslbf\n//     program_mux_rate                                     22        uimsbf\n//     marker_bit                                           1        bslbf\n//     marker_bit                                           1        bslbf\n//     reserved                                             5        bslbf\n//     pack_stuffing_length                                 3        uimsbf\n//     for (i = 0; i < pack_stuffing_length; i++) {\n//             stuffing_byte                               8       bslbf\n//     }\n//     if (nextbits() == SystemHeader_start_code) {\n//             SystemHeader ()\n//     }\n// }\n\ntype PsPackHeader struct {\n\tIsMpeg1                       bool\n\tSystemClockReferenceBase      uint64 //33 bits\n\tSystemClockReferenceExtension uint16 //9 bits\n\tProgramMuxRate                uint32 //22 bits\n\tPackStuffingLength            uint8  //3 bitss\n}\n\nfunc (psPackHeader *PsPackHeader) PrettyPrint(file *os.File) {\n\tfile.WriteString(fmt.Sprintf(\"IsMpeg1:%t\\n\", psPackHeader.IsMpeg1))\n\tfile.WriteString(fmt.Sprintf(\"system clock reference base:%d\\n\", psPackHeader.SystemClockReferenceBase))\n\tfile.WriteString(fmt.Sprintf(\"system clock reference extension:%d\\n\", psPackHeader.SystemClockReferenceExtension))\n\tfile.WriteString(fmt.Sprintf(\"program mux rate:%d\\n\", psPackHeader.ProgramMuxRate))\n\tfile.WriteString(fmt.Sprintf(\"pack stuffing length:%d\\n\", psPackHeader.PackStuffingLength))\n}\n\nfunc (psPackHeader *PsPackHeader) Decode(bs *BitStream) error {\n\tif bs.RemainBytes() < 5 {\n\t\treturn errNeedMore\n\t}\n\tif bs.Uint32(32) != 0x000001BA {\n\t\treturn errors.New(\"ps header must start with 000001BA\")\n\t}\n\n\tif bs.NextBits(2) == 0x01 { //mpeg2\n\t\tif bs.RemainBytes() < 10 {\n\t\t\treturn errNeedMore\n\t\t}\n\t\treturn psPackHeader.decodeMpeg2(bs)\n\t} else if bs.NextBits(4) == 0x02 { //mpeg1\n\t\tif bs.RemainBytes() < 8 {\n\t\t\treturn errNeedMore\n\t\t}\n\t\tpsPackHeader.IsMpeg1 = true\n\t\treturn psPackHeader.decodeMpeg1(bs)\n\t} else {\n\t\treturn errParser\n\t}\n}\n\nfunc (psPackHeader *PsPackHeader) decodeMpeg2(bs *BitStream) error {\n\tbs.SkipBits(2)\n\tpsPackHeader.SystemClockReferenceBase = bs.GetBits(3)\n\tbs.SkipBits(1)\n\tpsPackHeader.SystemClockReferenceBase = psPackHeader.SystemClockReferenceBase<<15 | bs.GetBits(15)\n\tbs.SkipBits(1)\n\tpsPackHeader.SystemClockReferenceBase = psPackHeader.SystemClockReferenceBase<<15 | bs.GetBits(15)\n\tbs.SkipBits(1)\n\tpsPackHeader.SystemClockReferenceExtension = bs.Uint16(9)\n\tbs.SkipBits(1)\n\tpsPackHeader.ProgramMuxRate = bs.Uint32(22)\n\tbs.SkipBits(1)\n\tbs.SkipBits(1)\n\tbs.SkipBits(5)\n\tpsPackHeader.PackStuffingLength = bs.Uint8(3)\n\tif bs.RemainBytes() < int(psPackHeader.PackStuffingLength) {\n\t\tbs.UnRead(10 * 8)\n\t\treturn errNeedMore\n\t}\n\tbs.SkipBits(int(psPackHeader.PackStuffingLength) * 8)\n\treturn nil\n}\n\nfunc (psPackHeader *PsPackHeader) decodeMpeg1(bs *BitStream) error {\n\tbs.SkipBits(4)\n\tpsPackHeader.SystemClockReferenceBase = bs.GetBits(3)\n\tbs.SkipBits(1)\n\tpsPackHeader.SystemClockReferenceBase = psPackHeader.SystemClockReferenceBase<<15 | bs.GetBits(15)\n\tbs.SkipBits(1)\n\tpsPackHeader.SystemClockReferenceBase = psPackHeader.SystemClockReferenceBase<<15 | bs.GetBits(15)\n\tbs.SkipBits(1)\n\tpsPackHeader.SystemClockReferenceExtension = 1\n\tpsPackHeader.ProgramMuxRate = bs.Uint32(7)\n\tbs.SkipBits(1)\n\tpsPackHeader.ProgramMuxRate = psPackHeader.ProgramMuxRate<<15 | bs.Uint32(15)\n\tbs.SkipBits(1)\n\treturn nil\n}\n\nfunc (psPackHeader *PsPackHeader) Encode(bsw *BitStreamWriter) {\n\tbsw.PutBytes([]byte{0x00, 0x00, 0x01, 0xBA})\n\tbsw.PutUint8(1, 2)\n\tbsw.PutUint64(psPackHeader.SystemClockReferenceBase>>30, 3)\n\tbsw.PutUint8(1, 1)\n\tbsw.PutUint64(psPackHeader.SystemClockReferenceBase>>15, 15)\n\tbsw.PutUint8(1, 1)\n\tbsw.PutUint64(psPackHeader.SystemClockReferenceBase, 15)\n\tbsw.PutUint8(1, 1)\n\tbsw.PutUint16(psPackHeader.SystemClockReferenceExtension, 9)\n\tbsw.PutUint8(1, 1)\n\tbsw.PutUint32(psPackHeader.ProgramMuxRate, 22)\n\tbsw.PutUint8(1, 1)\n\tbsw.PutUint8(1, 1)\n\tbsw.PutUint8(0x1F, 5)\n\tbsw.PutUint8(psPackHeader.PackStuffingLength, 3)\n\tbsw.PutRepetValue(0xFF, int(psPackHeader.PackStuffingLength))\n}\n\ntype ElementaryStream struct {\n\tStreamId             uint8\n\tPStdBufferBoundScale uint8\n\tPStdBufferSizeBound  uint16\n}\n\nfunc NewElementaryStream(sid uint8) *ElementaryStream {\n\treturn &ElementaryStream{\n\t\tStreamId: sid,\n\t}\n}\n\n// SystemHeader () {\n//     SystemHeader_start_code         32 bslbf\n//     header_length                     16 uimsbf\n//     marker_bit                         1  bslbf\n//     rate_bound                         22 uimsbf\n//     marker_bit                         1  bslbf\n//     audio_bound                     6  uimsbf\n//     fixed_flag                         1  bslbf\n//     CSPS_flag                         1  bslbf\n//     system_audio_lock_flag             1  bslbf\n//     system_video_lock_flag             1  bslbf\n//     marker_bit                      1  bslbf\n//     video_bound                     5  uimsbf\n//     packet_rate_restriction_flag    1  bslbf\n//     reserved_bits                     7  bslbf\n//     while (nextbits () == '1') {\n//         stream_id                     8  uimsbf\n//         '11'                         2  bslbf\n//         P-STD_buffer_bound_scale     1  bslbf\n//         P-STD_buffer_size_bound     13 uimsbf\n//     }\n// }\n\ntype SystemHeader struct {\n\tHeaderLength              uint16\n\tRateBound                 uint32\n\tAudioBound                uint8\n\tFixedFlag                 uint8\n\tCspsFlag                  uint8\n\tSystemAudioLockFlag       uint8\n\tSystemVideoLockFlag       uint8\n\tVideoBound                uint8\n\tPacketRateRestrictionFlag uint8\n\tStreams                   []*ElementaryStream\n}\n\nfunc (sh *SystemHeader) PrettyPrint(file *os.File) {\n\tfile.WriteString(fmt.Sprintf(\"header length:%d\\n\", sh.HeaderLength))\n\tfile.WriteString(fmt.Sprintf(\"rate bound:%d\\n\", sh.RateBound))\n\tfile.WriteString(fmt.Sprintf(\"audio bound:%d\\n\", sh.AudioBound))\n\tfile.WriteString(fmt.Sprintf(\"fixed flag:%d\\n\", sh.FixedFlag))\n\tfile.WriteString(fmt.Sprintf(\"csps flag:%d\\n\", sh.CspsFlag))\n\tfile.WriteString(fmt.Sprintf(\"system audio lock flag:%d\\n\", sh.SystemAudioLockFlag))\n\tfile.WriteString(fmt.Sprintf(\"system video lock flag:%d\\n\", sh.SystemVideoLockFlag))\n\tfile.WriteString(fmt.Sprintf(\"video bound:%d\\n\", sh.VideoBound))\n\tfile.WriteString(fmt.Sprintf(\"packet rate restriction flag:%d\\n\", sh.PacketRateRestrictionFlag))\n\tfor i, es := range sh.Streams {\n\t\tfile.WriteString(fmt.Sprintf(\"----streams %d\\n\", i))\n\t\tfile.WriteString(fmt.Sprintf(\"    stream id:%d\\n\", es.StreamId))\n\t\tfile.WriteString(fmt.Sprintf(\"    PStdBufferBoundScale:%d\\n\", es.PStdBufferBoundScale))\n\t\tfile.WriteString(fmt.Sprintf(\"    PStdBufferSizeBound:%d\\n\", es.PStdBufferSizeBound))\n\t}\n}\n\nfunc (sh *SystemHeader) Encode(bsw *BitStreamWriter) {\n\tbsw.PutBytes([]byte{0x00, 0x00, 0x01, 0xBB})\n\tloc := bsw.ByteOffset()\n\tbsw.PutUint16(0, 16)\n\tbsw.Markdot()\n\tbsw.PutUint8(1, 1)\n\tbsw.PutUint32(sh.RateBound, 22)\n\tbsw.PutUint8(1, 1)\n\tbsw.PutUint8(sh.AudioBound, 6)\n\tbsw.PutUint8(sh.FixedFlag, 1)\n\tbsw.PutUint8(sh.CspsFlag, 1)\n\tbsw.PutUint8(sh.SystemAudioLockFlag, 1)\n\tbsw.PutUint8(sh.SystemVideoLockFlag, 1)\n\tbsw.PutUint8(1, 1)\n\tbsw.PutUint8(sh.VideoBound, 5)\n\tbsw.PutUint8(sh.PacketRateRestrictionFlag, 1)\n\tbsw.PutUint8(0x7F, 7)\n\tfor _, stream := range sh.Streams {\n\t\tbsw.PutUint8(stream.StreamId, 8)\n\t\tbsw.PutUint8(3, 2)\n\t\tbsw.PutUint8(stream.PStdBufferBoundScale, 1)\n\t\tbsw.PutUint16(stream.PStdBufferSizeBound, 13)\n\t}\n\tlength := bsw.DistanceFromMarkDot() / 8\n\tbsw.SetUint16(uint16(length), loc)\n}\n\nfunc (sh *SystemHeader) Decode(bs *BitStream) error {\n\tif bs.RemainBytes() < 12 {\n\t\treturn errNeedMore\n\t}\n\tif bs.Uint32(32) != 0x000001BB {\n\t\treturn errors.New(\"system header must start with 000001BB\")\n\t}\n\tsh.HeaderLength = bs.Uint16(16)\n\tif bs.RemainBytes() < int(sh.HeaderLength) {\n\t\tbs.UnRead(6 * 8)\n\t\treturn errNeedMore\n\t}\n\tif sh.HeaderLength < 6 || (sh.HeaderLength-6)%3 != 0 {\n\t\treturn errParser\n\t}\n\tbs.SkipBits(1)\n\tsh.RateBound = bs.Uint32(22)\n\tbs.SkipBits(1)\n\tsh.AudioBound = bs.Uint8(6)\n\tsh.FixedFlag = bs.Uint8(1)\n\tsh.CspsFlag = bs.Uint8(1)\n\tsh.SystemAudioLockFlag = bs.Uint8(1)\n\tsh.SystemVideoLockFlag = bs.Uint8(1)\n\tbs.SkipBits(1)\n\tsh.VideoBound = bs.Uint8(5)\n\tsh.PacketRateRestrictionFlag = bs.Uint8(1)\n\tbs.SkipBits(7)\n\tsh.Streams = sh.Streams[:0]\n\tleast := sh.HeaderLength - 6\n\tfor least > 0 && bs.NextBits(1) == 0x01 {\n\t\tes := new(ElementaryStream)\n\t\tes.StreamId = bs.Uint8(8)\n\t\tbs.SkipBits(2)\n\t\tes.PStdBufferBoundScale = bs.GetBit()\n\t\tes.PStdBufferSizeBound = bs.Uint16(13)\n\t\tsh.Streams = append(sh.Streams, es)\n\t\tleast -= 3\n\t}\n\tif least > 0 {\n\t\treturn errParser\n\t}\n\treturn nil\n}\n\ntype ElementaryStreamElem struct {\n\tStreamType                 uint8\n\tElementaryStreamId         uint8\n\tElementaryStreamInfoLength uint16\n}\n\nfunc NewElementaryStreamElem(stype uint8, esid uint8) *ElementaryStreamElem {\n\treturn &ElementaryStreamElem{\n\t\tStreamType:         stype,\n\t\tElementaryStreamId: esid,\n\t}\n}\n\n// program_stream_map() {\n//     packet_start_code_prefix             24     bslbf\n//     map_stream_id                         8     uimsbf\n//     program_stream_map_length             16     uimsbf\n//     current_next_indicator                 1     bslbf\n//     reserved                             2     bslbf\n//     program_stream_map_version             5     uimsbf\n//     reserved                             7     bslbf\n//     marker_bit                             1     bslbf\n//     program_stream_info_length             16     uimsbf\n//     for (i = 0; i < N; i++) {\n//         descriptor()\n//     }\n//     elementary_stream_map_length         16     uimsbf\n//     for (i = 0; i < N1; i++) {\n//         stream_type                         8     uimsbf\n//         elementary_stream_id             8     uimsbf\n//         elementary_stream_info_length     16    uimsbf\n//         for (i = 0; i < N2; i++) {\n//             descriptor()\n//         }\n//     }\n//     CRC_32                                 32     rpchof\n// }\n\ntype ProgramStreamMap struct {\n\tMapStreamId               uint8\n\tProgramStreamMapLength    uint16\n\tCurrentNextIndicator      uint8\n\tProgramStreamMapVersion   uint8\n\tProgramStreamInfoLength   uint16\n\tElementaryStreamMapLength uint16\n\tStreamMap                 []*ElementaryStreamElem\n}\n\nfunc (psm *ProgramStreamMap) PrettyPrint(file *os.File) {\n\tfile.WriteString(fmt.Sprintf(\"map stream id:%d\\n\", psm.MapStreamId))\n\tfile.WriteString(fmt.Sprintf(\"program stream map length:%d\\n\", psm.ProgramStreamMapLength))\n\tfile.WriteString(fmt.Sprintf(\"current next indicator:%d\\n\", psm.CurrentNextIndicator))\n\tfile.WriteString(fmt.Sprintf(\"program stream map version:%d\\n\", psm.ProgramStreamMapVersion))\n\tfile.WriteString(fmt.Sprintf(\"program stream info length:%d\\n\", psm.ProgramStreamInfoLength))\n\tfile.WriteString(fmt.Sprintf(\"elementary stream map length:%d\\n\", psm.ElementaryStreamMapLength))\n\tfor i, es := range psm.StreamMap {\n\t\tfile.WriteString(fmt.Sprintf(\"----ES stream %d\\n\", i))\n\t\tif es.StreamType == uint8(PsStreamAac) {\n\t\t\tfile.WriteString(\"    streamType:AAC\\n\")\n\t\t} else if es.StreamType == uint8(PsStreamG711A) {\n\t\t\tfile.WriteString(\"    streamType:G711A\\n\")\n\t\t} else if es.StreamType == uint8(PsStreamG711U) {\n\t\t\tfile.WriteString(\"    streamType:G711U\\n\")\n\t\t} else if es.StreamType == uint8(PsStreamH264) {\n\t\t\tfile.WriteString(\"    streamType:H264\\n\")\n\t\t} else if es.StreamType == uint8(PsStreamH265) {\n\t\t\tfile.WriteString(\"    streamType:H265\\n\")\n\t\t}\n\t\tfile.WriteString(fmt.Sprintf(\"    elementary stream id:%d\\n\", es.ElementaryStreamId))\n\t\tfile.WriteString(fmt.Sprintf(\"    elementary stream info length:%d\\n\", es.ElementaryStreamInfoLength))\n\t}\n}\n\nfunc (psm *ProgramStreamMap) Encode(bsw *BitStreamWriter) {\n\tbsw.PutBytes([]byte{0x00, 0x00, 0x01, 0xBC})\n\tloc := bsw.ByteOffset()\n\tbsw.PutUint16(psm.ElementaryStreamMapLength, 16)\n\tbsw.Markdot()\n\tbsw.PutUint8(psm.CurrentNextIndicator, 1)\n\tbsw.PutUint8(3, 2)\n\tbsw.PutUint8(psm.ProgramStreamMapVersion, 5)\n\tbsw.PutUint8(0x7F, 7)\n\tbsw.PutUint8(1, 1)\n\tbsw.PutUint16(0, 16)\n\tpsm.ElementaryStreamMapLength = uint16(len(psm.StreamMap) * 4)\n\tbsw.PutUint16(psm.ElementaryStreamMapLength, 16)\n\tfor _, streaminfo := range psm.StreamMap {\n\t\tbsw.PutUint8(streaminfo.StreamType, 8)\n\t\tbsw.PutUint8(streaminfo.ElementaryStreamId, 8)\n\t\tbsw.PutUint16(0, 16)\n\t}\n\tlength := bsw.DistanceFromMarkDot()/8 + 4\n\tbsw.SetUint16(uint16(length), loc)\n\tcrc := CalcCrc32(0xffffffff, bsw.Bits()[bsw.ByteOffset()-int(length-4)-4:bsw.ByteOffset()])\n\ttmpcrc := make([]byte, 4)\n\tbinary.LittleEndian.PutUint32(tmpcrc, crc)\n\tbsw.PutBytes(tmpcrc)\n}\n\nfunc (psm *ProgramStreamMap) Decode(bs *BitStream) error {\n\tif bs.RemainBytes() < 16 {\n\t\treturn errNeedMore\n\t}\n\tif bs.Uint32(24) != 0x000001 {\n\t\treturn errors.New(\"program stream map must startwith 0x000001\")\n\t}\n\tpsm.MapStreamId = bs.Uint8(8)\n\tif psm.MapStreamId != 0xBC {\n\t\treturn errors.New(\"map stream id must be 0xBC\")\n\t}\n\tpsm.ProgramStreamMapLength = bs.Uint16(16)\n\tif bs.RemainBytes() < int(psm.ProgramStreamMapLength) {\n\t\tbs.UnRead(6 * 8)\n\t\treturn errNeedMore\n\t}\n\tpsm.CurrentNextIndicator = bs.Uint8(1)\n\tbs.SkipBits(2)\n\tpsm.ProgramStreamMapVersion = bs.Uint8(5)\n\tbs.SkipBits(8)\n\tpsm.ProgramStreamInfoLength = bs.Uint16(16)\n\tif bs.RemainBytes() < int(psm.ProgramStreamInfoLength)+2 {\n\t\tbs.UnRead(10 * 8)\n\t\treturn errNeedMore\n\t}\n\tbs.SkipBits(int(psm.ProgramStreamInfoLength) * 8)\n\tpsm.ElementaryStreamMapLength = bs.Uint16(16)\n\n\tpsm.ElementaryStreamMapLength = psm.ProgramStreamMapLength - psm.ProgramStreamInfoLength - 10\n\n\tif bs.RemainBytes() < int(psm.ElementaryStreamMapLength)+4 {\n\t\tbs.UnRead(12*8 + int(psm.ProgramStreamInfoLength)*8)\n\t\treturn errNeedMore\n\t}\n\n\ti := 0\n\tpsm.StreamMap = psm.StreamMap[:0]\n\tfor i < int(psm.ElementaryStreamMapLength) {\n\t\telem := new(ElementaryStreamElem)\n\t\telem.StreamType = bs.Uint8(8)\n\t\telem.ElementaryStreamId = bs.Uint8(8)\n\t\telem.ElementaryStreamInfoLength = bs.Uint16(16)\n\t\t//TODO Parser descriptor\n\t\tif bs.RemainBytes() < int(elem.ElementaryStreamInfoLength) {\n\t\t\treturn errParser\n\t\t}\n\t\tbs.SkipBits(int(elem.ElementaryStreamInfoLength) * 8)\n\t\ti += int(4 + elem.ElementaryStreamInfoLength)\n\t\tpsm.StreamMap = append(psm.StreamMap, elem)\n\t}\n\n\tif i != int(psm.ElementaryStreamMapLength) {\n\t\treturn errParser\n\t}\n\n\tbs.SkipBits(32)\n\treturn nil\n}\n\ntype ProgramStreamDirectory struct {\n\tPesPacketLength uint16\n}\n\nfunc (psd *ProgramStreamDirectory) Decode(bs *BitStream) error {\n\tif bs.RemainBytes() < 6 {\n\t\treturn errNeedMore\n\t}\n\tif bs.Uint32(32) != 0x000001FF {\n\t\treturn errors.New(\"program stream directory 000001FF\")\n\t}\n\tpsd.PesPacketLength = bs.Uint16(16)\n\tif bs.RemainBytes() < int(psd.PesPacketLength) {\n\t\tbs.UnRead(6 * 8)\n\t\treturn errNeedMore\n\t}\n\t//TODO Program Stream directory\n\tbs.SkipBits(int(psd.PesPacketLength) * 8)\n\treturn nil\n}\n\ntype CommonPesPacket struct {\n\tStreamId        uint8\n\tPesPacketLength uint16\n}\n\nfunc (compes *CommonPesPacket) Decode(bs *BitStream) error {\n\tif bs.RemainBytes() < 6 {\n\t\treturn errNeedMore\n\t}\n\tbs.SkipBits(24)\n\tcompes.StreamId = bs.Uint8(8)\n\tcompes.PesPacketLength = bs.Uint16(16)\n\tif bs.RemainBytes() < int(compes.PesPacketLength) {\n\t\tbs.UnRead(6 * 8)\n\t\treturn errNeedMore\n\t}\n\tbs.SkipBits(int(compes.PesPacketLength) * 8)\n\treturn nil\n}\n\ntype PsPacket struct {\n\tHeader  *PsPackHeader\n\tSystem  *SystemHeader\n\tPsm     *ProgramStreamMap\n\tPsd     *ProgramStreamDirectory\n\tCommPes *CommonPesPacket\n\tPes     *PesPacket\n}\n"
  },
  {
    "path": "gb28181/mpegps/util.go",
    "content": "package mpegps\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n)\n\nconst (\n\tCodecUnknown = iota\n\tCodecH264\n\tCodecH265\n\tCodecH266\n\tCodecMpeg4\n)\n\nvar crc32table [256]uint32 = [256]uint32{\n\t0x00000000, 0xB71DC104, 0x6E3B8209, 0xD926430D, 0xDC760413, 0x6B6BC517,\n\t0xB24D861A, 0x0550471E, 0xB8ED0826, 0x0FF0C922, 0xD6D68A2F, 0x61CB4B2B,\n\t0x649B0C35, 0xD386CD31, 0x0AA08E3C, 0xBDBD4F38, 0x70DB114C, 0xC7C6D048,\n\t0x1EE09345, 0xA9FD5241, 0xACAD155F, 0x1BB0D45B, 0xC2969756, 0x758B5652,\n\t0xC836196A, 0x7F2BD86E, 0xA60D9B63, 0x11105A67, 0x14401D79, 0xA35DDC7D,\n\t0x7A7B9F70, 0xCD665E74, 0xE0B62398, 0x57ABE29C, 0x8E8DA191, 0x39906095,\n\t0x3CC0278B, 0x8BDDE68F, 0x52FBA582, 0xE5E66486, 0x585B2BBE, 0xEF46EABA,\n\t0x3660A9B7, 0x817D68B3, 0x842D2FAD, 0x3330EEA9, 0xEA16ADA4, 0x5D0B6CA0,\n\t0x906D32D4, 0x2770F3D0, 0xFE56B0DD, 0x494B71D9, 0x4C1B36C7, 0xFB06F7C3,\n\t0x2220B4CE, 0x953D75CA, 0x28803AF2, 0x9F9DFBF6, 0x46BBB8FB, 0xF1A679FF,\n\t0xF4F63EE1, 0x43EBFFE5, 0x9ACDBCE8, 0x2DD07DEC, 0x77708634, 0xC06D4730,\n\t0x194B043D, 0xAE56C539, 0xAB068227, 0x1C1B4323, 0xC53D002E, 0x7220C12A,\n\t0xCF9D8E12, 0x78804F16, 0xA1A60C1B, 0x16BBCD1F, 0x13EB8A01, 0xA4F64B05,\n\t0x7DD00808, 0xCACDC90C, 0x07AB9778, 0xB0B6567C, 0x69901571, 0xDE8DD475,\n\t0xDBDD936B, 0x6CC0526F, 0xB5E61162, 0x02FBD066, 0xBF469F5E, 0x085B5E5A,\n\t0xD17D1D57, 0x6660DC53, 0x63309B4D, 0xD42D5A49, 0x0D0B1944, 0xBA16D840,\n\t0x97C6A5AC, 0x20DB64A8, 0xF9FD27A5, 0x4EE0E6A1, 0x4BB0A1BF, 0xFCAD60BB,\n\t0x258B23B6, 0x9296E2B2, 0x2F2BAD8A, 0x98366C8E, 0x41102F83, 0xF60DEE87,\n\t0xF35DA999, 0x4440689D, 0x9D662B90, 0x2A7BEA94, 0xE71DB4E0, 0x500075E4,\n\t0x892636E9, 0x3E3BF7ED, 0x3B6BB0F3, 0x8C7671F7, 0x555032FA, 0xE24DF3FE,\n\t0x5FF0BCC6, 0xE8ED7DC2, 0x31CB3ECF, 0x86D6FFCB, 0x8386B8D5, 0x349B79D1,\n\t0xEDBD3ADC, 0x5AA0FBD8, 0xEEE00C69, 0x59FDCD6D, 0x80DB8E60, 0x37C64F64,\n\t0x3296087A, 0x858BC97E, 0x5CAD8A73, 0xEBB04B77, 0x560D044F, 0xE110C54B,\n\t0x38368646, 0x8F2B4742, 0x8A7B005C, 0x3D66C158, 0xE4408255, 0x535D4351,\n\t0x9E3B1D25, 0x2926DC21, 0xF0009F2C, 0x471D5E28, 0x424D1936, 0xF550D832,\n\t0x2C769B3F, 0x9B6B5A3B, 0x26D61503, 0x91CBD407, 0x48ED970A, 0xFFF0560E,\n\t0xFAA01110, 0x4DBDD014, 0x949B9319, 0x2386521D, 0x0E562FF1, 0xB94BEEF5,\n\t0x606DADF8, 0xD7706CFC, 0xD2202BE2, 0x653DEAE6, 0xBC1BA9EB, 0x0B0668EF,\n\t0xB6BB27D7, 0x01A6E6D3, 0xD880A5DE, 0x6F9D64DA, 0x6ACD23C4, 0xDDD0E2C0,\n\t0x04F6A1CD, 0xB3EB60C9, 0x7E8D3EBD, 0xC990FFB9, 0x10B6BCB4, 0xA7AB7DB0,\n\t0xA2FB3AAE, 0x15E6FBAA, 0xCCC0B8A7, 0x7BDD79A3, 0xC660369B, 0x717DF79F,\n\t0xA85BB492, 0x1F467596, 0x1A163288, 0xAD0BF38C, 0x742DB081, 0xC3307185,\n\t0x99908A5D, 0x2E8D4B59, 0xF7AB0854, 0x40B6C950, 0x45E68E4E, 0xF2FB4F4A,\n\t0x2BDD0C47, 0x9CC0CD43, 0x217D827B, 0x9660437F, 0x4F460072, 0xF85BC176,\n\t0xFD0B8668, 0x4A16476C, 0x93300461, 0x242DC565, 0xE94B9B11, 0x5E565A15,\n\t0x87701918, 0x306DD81C, 0x353D9F02, 0x82205E06, 0x5B061D0B, 0xEC1BDC0F,\n\t0x51A69337, 0xE6BB5233, 0x3F9D113E, 0x8880D03A, 0x8DD09724, 0x3ACD5620,\n\t0xE3EB152D, 0x54F6D429, 0x7926A9C5, 0xCE3B68C1, 0x171D2BCC, 0xA000EAC8,\n\t0xA550ADD6, 0x124D6CD2, 0xCB6B2FDF, 0x7C76EEDB, 0xC1CBA1E3, 0x76D660E7,\n\t0xAFF023EA, 0x18EDE2EE, 0x1DBDA5F0, 0xAAA064F4, 0x738627F9, 0xC49BE6FD,\n\t0x09FDB889, 0xBEE0798D, 0x67C63A80, 0xD0DBFB84, 0xD58BBC9A, 0x62967D9E,\n\t0xBBB03E93, 0x0CADFF97, 0xB110B0AF, 0x060D71AB, 0xDF2B32A6, 0x6836F3A2,\n\t0x6D66B4BC, 0xDA7B75B8, 0x035D36B5, 0xB440F7B1,\n}\n\nfunc CalcCrc32(crc uint32, buffer []byte) uint32 {\n\tvar i int = 0\n\tfor i = 0; i < len(buffer); i++ {\n\t\tcrc = crc32table[(crc^uint32(buffer[i]))&0xff] ^ (crc >> 8)\n\t}\n\treturn crc\n}\n\ntype StartCodeType int\n\nconst (\n\tStartCode3 StartCodeType = 3\n\tSTartCode4 StartCodeType = 4\n)\n\nfunc FindStartCode(nalu []byte, offset int) (int, StartCodeType) {\n\tidx := bytes.Index(nalu[offset:], []byte{0x00, 0x00, 0x01})\n\tswitch {\n\tcase idx > 0:\n\t\tif nalu[offset+idx-1] == 0x00 {\n\t\t\treturn offset + idx - 1, STartCode4\n\t\t}\n\t\tfallthrough\n\tcase idx == 0:\n\t\treturn offset + idx, StartCode3\n\t}\n\treturn -1, StartCode3\n}\n\nfunc SplitFrame(frames []byte, onFrame func(nalu []byte) bool) {\n\tbeg, sc := FindStartCode(frames, 0)\n\tfor beg >= 0 {\n\t\tend, sc2 := FindStartCode(frames, beg+int(sc))\n\t\tif end == -1 {\n\t\t\tif onFrame != nil {\n\t\t\t\tonFrame(frames[beg+int(sc):])\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tif onFrame != nil && onFrame(frames[beg+int(sc):end]) == false {\n\t\t\tbreak\n\t\t}\n\t\tbeg = end\n\t\tsc = sc2\n\t}\n}\nfunc H264NaluType(h264 []byte) uint8 {\n\tloc, sc := FindStartCode(h264, 0)\n\treturn h264[loc+int(sc)] & 0x1F\n}\nfunc H265NaluType(h265 []byte) uint8 {\n\tloc, sc := FindStartCode(h265, 0)\n\treturn (h265[loc+int(sc)] >> 1) & 0x3F\n}\n\nfunc mpegH264FindNALU(data []byte) (int, int, error) {\n\tvar zeros, i int\n\n\tfor i = 0; i+2 < len(data); i++ {\n\t\tif data[i] == 0x01 && zeros >= 2 {\n\t\t\treturn i + 1, zeros + 1, nil // 返回 NALU 的长度和前导零的数量\n\t\t}\n\n\t\tif data[i] == 0 {\n\t\t\tzeros++\n\t\t} else {\n\t\t\tzeros = 0\n\t\t}\n\t}\n\n\treturn -1, 0, errors.New(\"no valid NALU found\")\n}\n\n// 来自media-server\nfunc mpegH26xVerify(data []byte) (int, error) {\n\th264Flags := uint32(0x01A0)      // SPS/PPS/IDR\n\th265Flags := uint64(0x700000000) // VPS/SPS/PPS\n\th266Flags := uint32(0xC000)      // VPS/SPS/PPS\n\tcount := 0\n\th26x := [5][10]int{}\n\n\tp := 0\n\tend := len(data)\n\n\tfor p < end && count < len(h26x[0]) {\n\t\tn, _, err := mpegH264FindNALU(data[p:])\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\t\tif p+n+1 > end {\n\t\t\tbreak\n\t\t}\n\n\t\th26x[0][count] = int(data[p+n]) & 0x1F          // H.264 NALU type\n\t\th26x[1][count] = (int(data[p+n]) >> 1) & 0x3F   // H.265 NALU type\n\t\th26x[2][count] = (int(data[p+n+1]) >> 3) & 0x1F // H.266 NALU type\n\t\th26x[3][count] = int(data[p+n])                 // MPEG-4 VOP start code\n\t\th26x[4][count] = int(data[p+n+1])               // MPEG-4 VOP coding type\n\t\tcount++\n\n\t\tp += n // 移动到下一个 NALU\n\t}\n\n\tfor n := 0; n < count; n++ {\n\t\th264Flags &= ^(1 << h26x[0][n])\n\t\th265Flags &= ^(1 << h26x[1][n])\n\t\th266Flags &= ^(1 << h26x[2][n])\n\t}\n\n\tif h264Flags == 0 && h265Flags != 0 && h266Flags != 0 {\n\t\t// match SPS/PPS/IDR\n\t\treturn CodecH264, nil\n\t} else if h265Flags == 0 && h264Flags != 0 && h266Flags != 0 {\n\t\t// match VPS/SPS/PPS\n\t\treturn CodecH265, nil\n\t} else if h266Flags == 0 && h264Flags != 0 && h265Flags != 0 {\n\t\t// match SPS/PPS\n\t\treturn CodecH266, nil\n\t} else if h26x[3][0] == 0xB0 && (h26x[4][0]&0x30) == 0 {\n\t\t// match VOP start code\n\t\treturn CodecMpeg4, nil\n\t}\n\n\treturn CodecUnknown, nil\n}\nfunc audioVerify(data []byte) PsStreamType {\n\tif data[0] == 0xFF && (data[1]&0xF0) == 0xF0 && len(data) > 7 {\n\t\taacLen := ((int(data[3]) & 0x03) << 11) |\n\t\t\t(int(data[4]) << 3) |\n\t\t\t(int(data[5]) >> 5 & 0x07)\n\t\tif len(data) == aacLen {\n\t\t\treturn PsStreamAac\n\t\t}\n\t}\n\treturn PsStreamG711A\n}\n"
  },
  {
    "path": "gb28181/ptz.go",
    "content": "package gb28181\n\nimport (\n\t\"encoding/hex\"\n\t\"encoding/xml\"\n)\n\ntype MessagePtz struct {\n\tXMLName  xml.Name `xml:\"Control\"`\n\tCmdType  string   `xml:\"CmdType\"`\n\tSN       int      `xml:\"SN\"`\n\tDeviceID string   `xml:\"DeviceID\"`\n\tPTZCmd   string   `xml:\"PTZCmd\"`\n}\n\nconst DeviceControl = \"DeviceControl\"\nconst PTZFirstByte = 0xA5\nconst (\n\tPresetSet  = 0x81\n\tPresetCall = 0x82\n\tPresetDel  = 0x83\n)\n\nconst (\n\tCruiseAdd      = 0x84\n\tCruiseDel      = 0x85\n\tCruiseSetSpeed = 0x86\n\tCruiseStopTime = 0x87\n\tCruiseStart    = 0x88\n)\nconst (\n\tScanningStart = 0x89\n\tScanningSpeed = 0x8A\n)\n\n/*\n表 A.3 指令格式\n字节 字节1 字节2 字节3 字节4 字节5 字节6 字节7 字节8\n含义 A5H 组合码1 地址 指令 数据1 数据2 组合码2 校验码\n各字节定义如下:\n字节1: 指令的首字节为 A5H。\n字节2: 组合码1, 高4 位是版本信息, 低4 位是校验位。 本标准的版本号是1.0, 版本信息为0H。\n校验位= (字节1 的高4 位+ 字节1 的低4 位+ 字节2 的高4 位) %16。\n字节3: 地址的低8 位。\n字节4: 指令码。\n字节5、6: 数据1 和数据2。\n字节7: 组合码2, 高4 位是数据3, 低4 位是地址的高4 位; 在后续叙述中, 没有特别指明的高4 位,\n表示该4 位与所指定的功能无关。\n字节8: 校验码, 为前面的第1~7 字节的算术和的低8 位, 即算术和对256 取模后的结果。\n字节8= (字节1+ 字节2+ 字节3+ 字节4+ 字节5+ 字节6+ 字节7) %256。\n地址范围000H~FFFH(即0~4095) , 其中000H 地址作为广播地址。\n注: 前端设备控制中, 不使用字节3 和字节7 的低4 位地址码, 使用前端设备控制消息体中的<DeviceID> 统一编码\n标识控制的前端设备\n*/\ntype PtzHead struct {\n\tFirstByte    uint8\n\tAssembleByte uint8\n\tAddr         uint8 //低地址码0-ff\n}\n\n// 获取组合码\nfunc getAssembleCode() uint8 {\n\treturn (PTZFirstByte>>4 + PTZFirstByte&0xF + 0) % 16\n}\nfunc getVerificationCode(ptz []byte) {\n\tsum := uint8(0)\n\tfor i := 0; i < len(ptz)-1; i++ {\n\t\tsum += ptz[i]\n\t}\n\tptz[len(ptz)-1] = sum\n}\n\n/*\n注1 : 字节4 中的 Bit5、Bit4 分别控制镜头变倍的缩小和放大, 字节4 中的 Bit3、Bit2、Bit1、Bit0 位分别控制云台\n上、 下、 左、 右方向的转动, 相应 Bit 位置1 时, 启动云台向相应方向转动, 相应 Bit 位清0 时, 停止云台相应\n方向的转动。 云台的转动方向以监视器显示图像的移动方向为准。\n注2: 字节5 控制水平方向速度, 速度范围由慢到快为00H~FFH; 字节6 控制垂直方向速度, 速度范围由慢到快\n为00H-FFH。\n注3: 字节7 的高4 位为变焦速度, 速度范围由慢到快为0H~FH; 低4 位为地址的高4 位。\n*/\ntype Ptz struct {\n\tZoomOut bool\n\tZoomIn  bool\n\tUp      bool\n\tDown    bool\n\tLeft    bool\n\tRight   bool\n\tSpeed   byte //0-8\n}\n\nfunc (p *Ptz) Pack() string {\n\tbuf := make([]byte, 8)\n\tbuf[0] = PTZFirstByte\n\tbuf[1] = getAssembleCode()\n\tbuf[2] = 1\n\tbuf[4] = 0\n\tbuf[5] = 0\n\tbuf[6] = 0\n\tif p.ZoomOut {\n\t\tbuf[3] |= 1 << 5\n\t\tbuf[6] = p.Speed << 4\n\t}\n\n\tif p.ZoomIn {\n\t\tbuf[3] |= 1 << 4\n\t\tbuf[6] = p.Speed << 4\n\t}\n\tif p.Up {\n\t\tbuf[3] |= 1 << 3\n\t\tbuf[5] = p.Speed\n\t}\n\tif p.Down {\n\t\tbuf[3] |= 1 << 2\n\t\tbuf[5] = p.Speed\n\t}\n\tif p.Left {\n\t\tbuf[3] |= 1 << 1\n\t\tbuf[4] = p.Speed\n\t}\n\tif p.Right {\n\t\tbuf[3] |= 1\n\t\tbuf[4] = p.Speed\n\t}\n\tgetVerificationCode(buf)\n\treturn hex.EncodeToString(buf)\n}\n\nfunc (p *Ptz) Stop() string {\n\tbuf := make([]byte, 8)\n\tbuf[0] = PTZFirstByte\n\tbuf[1] = getAssembleCode()\n\tbuf[2] = 1\n\tbuf[3] = 0\n\tbuf[4] = 0\n\tbuf[5] = 0\n\tbuf[6] = 0\n\tgetVerificationCode(buf)\n\treturn hex.EncodeToString(buf)\n}\n\n/*\n注1 : 字节4 中的 Bit3 为1 时, 光圈缩小;Bit2 为1 时, 光圈放大。 Bit1 为1 时, 聚焦近;Bit0 为1 时, 聚焦远。 Bit3~\nBit0 的相应位清0, 则相应控制操作停止动作。\n注2: 字节5 表示聚焦速度, 速度范围由慢到快为00H~FFH。\n注3: 字节6 表示光圈速度, 速度范围由慢到快为00H~FFH\n*/\ntype Fi struct {\n\tIrisIn    bool\n\tIrisOut   bool\n\tFocusNear bool\n\tFocusFar  bool\n\tSpeed     byte //0-8\n}\n\nfunc (f *Fi) Pack() string {\n\tbuf := make([]byte, 8)\n\tbuf[0] = PTZFirstByte\n\tbuf[1] = getAssembleCode()\n\tbuf[2] = 1\n\tbuf[3] |= 1 << 6\n\tbuf[4] = 0\n\tbuf[5] = 0\n\tbuf[6] = 0\n\n\tif f.IrisIn {\n\t\tbuf[3] |= 1 << 3\n\t\tbuf[5] = f.Speed\n\t}\n\tif f.IrisOut {\n\t\tbuf[3] |= 1 << 2\n\t\tbuf[5] = f.Speed\n\t}\n\tif f.FocusNear {\n\t\tbuf[3] |= 1 << 1\n\t\tbuf[4] = f.Speed\n\t}\n\tif f.FocusFar {\n\t\tbuf[3] |= 1\n\t\tbuf[4] = f.Speed\n\t}\n\tgetVerificationCode(buf)\n\treturn hex.EncodeToString(buf)\n}\n\ntype Preset struct {\n\tCMD   byte\n\tPoint byte\n}\n\nfunc (p *Preset) Pack() string {\n\tbuf := make([]byte, 8)\n\tbuf[0] = PTZFirstByte\n\tbuf[1] = getAssembleCode()\n\tbuf[2] = 1\n\n\tbuf[3] = p.CMD\n\n\tbuf[4] = 0\n\tbuf[5] = p.Point\n\tbuf[6] = 0\n\tgetVerificationCode(buf)\n\treturn hex.EncodeToString(buf)\n}\n\n/*\n注1 : 字节5 表示巡航组号, 字节6 表示预置位号。\n注2: 序号2 中, 字节6 为00H 时, 删除对应的整条巡航; 序号3、4 中字节6 表示数据的低8 位, 字节7 的高4 位\n表示数据的高4 位。\n注3: 巡航停留时间的单位是秒(s) 。\n注4: 停止巡航用 PTZ 指令中的字节4 的各 Bit 位均为0 的停止指令。\n*/\ntype Cruise struct {\n\tCMD      byte\n\tGroupNum byte\n\tValue    uint16\n}\n\nfunc (c *Cruise) Pack() string {\n\tbuf := make([]byte, 8)\n\tbuf[0] = PTZFirstByte\n\tbuf[1] = getAssembleCode()\n\tbuf[2] = 1\n\tbuf[3] = c.CMD\n\n\tbuf[4] = c.GroupNum\n\tbuf[5] = byte(c.Value & 0xFF)\n\tbuf[6] = byte(c.Value>>8) & 0x0F\n\tgetVerificationCode(buf)\n\treturn hex.EncodeToString(buf)\n}\n\n/*\n注1 : 字节5 表示扫描组号。\n注2: 序号4 中, 字节6 表示数据的低8 位, 字节7 的高4 位表示数据的高4 位。\n注3: 停止自动扫描用 PTZ 指令中的字节4 的各 Bit 位均为0 的停止指令。\n注4: 自动扫描开始时, 整体画面从右向左移动。\n*/\ntype Scanning struct {\n\tCMD      byte\n\tNo       byte\n\tValue    byte\n\tHighAddr byte // 0-f 后4位高地址码 0-f\n}\n"
  },
  {
    "path": "gb28181/rtppub/manager.go",
    "content": "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\"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/lal/pkg/logic\"\n\t\"github.com/q191201771/lalmax/config\"\n\t\"github.com/q191201771/lalmax/gb28181/mediaserver\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\nconst (\n\tdefaultPortMin          = 30000\n\tdefaultPortMaxIncrement = 3000\n)\n\nvar (\n\terrDuplicateStream = errors.New(\"rtp pub stream already exists\")\n\terrSessionNotFound = errors.New(\"rtp pub session not found\")\n)\n\ntype Manager struct {\n\tmu sync.Mutex\n\n\tlalServer logic.ILalServer\n\tportMin   int\n\tportMax   int\n\n\tsessionsByID     map[string]*Session\n\tsessionsByStream map[string]*Session\n\tsessionsByKey    map[string]*Session\n}\n\ntype Session struct {\n\tID         string\n\tStreamName string\n\tMediaKey   string\n\tNetwork    string\n\tPort       int\n\n\tmediaInfo  mediaserver.MediaInfo\n\tserver     *mediaserver.GB28181MediaServer\n\tlastActive time.Time\n\tdone       chan struct{}\n\tcloseOnce  sync.Once\n}\n\nfunc NewManager(lalServer logic.ILalServer, mediaConfig config.GB28181MediaConfig) *Manager {\n\tbasePort := int(mediaConfig.ListenPort)\n\tif basePort == 0 {\n\t\tbasePort = defaultPortMin\n\t}\n\tmaxIncrement := mediaConfig.MultiPortMaxIncrement\n\tif maxIncrement == 0 {\n\t\tmaxIncrement = defaultPortMaxIncrement\n\t}\n\n\tportMin := basePort\n\tif mediaConfig.ListenPort != 0 {\n\t\tportMin++\n\t}\n\n\treturn &Manager{\n\t\tlalServer:        lalServer,\n\t\tportMin:          portMin,\n\t\tportMax:          basePort + int(maxIncrement),\n\t\tsessionsByID:     make(map[string]*Session),\n\t\tsessionsByStream: make(map[string]*Session),\n\t\tsessionsByKey:    make(map[string]*Session),\n\t}\n}\n\nfunc (m *Manager) Start(req base.ApiCtrlStartRtpPubReq) (ret base.ApiCtrlStartRtpPubResp) {\n\tif req.StreamName == \"\" {\n\t\tret.ErrorCode = base.ErrorCodeParamMissing\n\t\tret.Desp = base.DespParamMissing\n\t\treturn\n\t}\n\n\tnetwork := \"udp\"\n\tif req.IsTcpFlag != 0 {\n\t\tnetwork = \"tcp\"\n\t}\n\n\tm.mu.Lock()\n\tif _, ok := m.sessionsByStream[req.StreamName]; ok {\n\t\tm.mu.Unlock()\n\t\tret.ErrorCode = base.ErrorCodeListenUdpPortFail\n\t\tret.Desp = errDuplicateStream.Error()\n\t\treturn\n\t}\n\tm.mu.Unlock()\n\n\tlistener, port, err := m.listen(req.Port, network)\n\tif err != nil {\n\t\tret.ErrorCode = base.ErrorCodeListenUdpPortFail\n\t\tret.Desp = err.Error()\n\t\treturn\n\t}\n\n\tsessionID := base.GenUkPsPubSession()\n\tmediaKey := fmt.Sprintf(\"%s%d\", network, port)\n\tsession := &Session{\n\t\tID:         sessionID,\n\t\tStreamName: req.StreamName,\n\t\tMediaKey:   mediaKey,\n\t\tNetwork:    network,\n\t\tPort:       port,\n\t\tmediaInfo: mediaserver.MediaInfo{\n\t\t\tStreamName:   req.StreamName,\n\t\t\tDumpFileName: req.DebugDumpPacket,\n\t\t\tMediaKey:     mediaKey,\n\t\t},\n\t\tlastActive: time.Now(),\n\t\tdone:       make(chan struct{}),\n\t}\n\treadTimeout := time.Duration(req.TimeoutMs) * time.Millisecond\n\tsession.server = mediaserver.NewGB28181MediaServer(port, mediaKey, m, m.lalServer).\n\t\tWithPreferMediaKeyLookup(true).\n\t\tWithReadTimeout(readTimeout)\n\n\tm.mu.Lock()\n\tif _, ok := m.sessionsByStream[req.StreamName]; ok {\n\t\tm.mu.Unlock()\n\t\t_ = listener.Close()\n\t\tret.ErrorCode = base.ErrorCodeListenUdpPortFail\n\t\tret.Desp = errDuplicateStream.Error()\n\t\treturn\n\t}\n\tm.sessionsByID[session.ID] = session\n\tm.sessionsByStream[session.StreamName] = session\n\tm.sessionsByKey[session.MediaKey] = session\n\tm.mu.Unlock()\n\n\tif err = session.server.Start(listener); err != nil {\n\t\tm.stopSession(session)\n\t\tret.ErrorCode = base.ErrorCodeListenUdpPortFail\n\t\tret.Desp = err.Error()\n\t\treturn\n\t}\n\n\tif req.TimeoutMs > 0 {\n\t\tgo m.watchTimeout(session, time.Duration(req.TimeoutMs)*time.Millisecond)\n\t}\n\n\tret.ErrorCode = base.ErrorCodeSucc\n\tret.Desp = base.DespSucc\n\tret.Data.SessionId = session.ID\n\tret.Data.StreamName = session.StreamName\n\tret.Data.Port = session.Port\n\treturn\n}\n\nfunc (m *Manager) Stop(streamName, sessionID string) (*Session, error) {\n\tm.mu.Lock()\n\tvar session *Session\n\tif sessionID != \"\" {\n\t\tsession = m.sessionsByID[sessionID]\n\t} else if streamName != \"\" {\n\t\tsession = m.sessionsByStream[streamName]\n\t}\n\tm.mu.Unlock()\n\n\tif session == nil {\n\t\treturn nil, errSessionNotFound\n\t}\n\n\tm.stopSession(session)\n\treturn session, nil\n}\n\nfunc (m *Manager) GetMediaInfoByKey(key string) (*mediaserver.MediaInfo, bool) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tsession, ok := m.sessionsByKey[key]\n\tif !ok {\n\t\treturn nil, false\n\t}\n\treturn &session.mediaInfo, true\n}\n\nfunc (m *Manager) CheckSsrc(ssrc uint32) (*mediaserver.MediaInfo, bool) {\n\treturn nil, false\n}\n\nfunc (m *Manager) NotifyClose(streamName string) {\n}\n\n// UpdatePortRange 动态更新端口范围，由 setServerConfig 接口调用\n// 为什么：owl 通过 setServerConfig 下发 rtp_proxy.port_range，需运行时生效\nfunc (m *Manager) UpdatePortRange(portMin, portMax int) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\tm.portMin = portMin\n\tm.portMax = portMax\n\tnazalog.Infof(\"rtp pub port range updated. min=%d, max=%d\", portMin, portMax)\n}\n\nfunc (m *Manager) OnRtpPacket(streamName string, mediaKey string) {\n\tm.mu.Lock()\n\tdefer m.mu.Unlock()\n\n\tif session, ok := m.sessionsByKey[mediaKey]; ok {\n\t\tsession.lastActive = time.Now()\n\t}\n}\n\nfunc (m *Manager) stopSession(session *Session) {\n\tm.mu.Lock()\n\tif current := m.sessionsByID[session.ID]; current != session {\n\t\tm.mu.Unlock()\n\t\treturn\n\t}\n\tdelete(m.sessionsByID, session.ID)\n\tdelete(m.sessionsByStream, session.StreamName)\n\tdelete(m.sessionsByKey, session.MediaKey)\n\tsession.closeOnce.Do(func() {\n\t\tclose(session.done)\n\t})\n\tm.mu.Unlock()\n\n\tsession.server.Dispose()\n}\n\nfunc (m *Manager) watchTimeout(session *Session, timeout time.Duration) {\n\tinterval := timeout / 2\n\tif interval < time.Second {\n\t\tinterval = time.Second\n\t}\n\n\tticker := time.NewTicker(interval)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-session.done:\n\t\t\treturn\n\t\tcase <-ticker.C:\n\t\t\tm.mu.Lock()\n\t\t\tcurrent := m.sessionsByID[session.ID]\n\t\t\texpired := current == session && time.Since(session.lastActive) >= timeout\n\t\t\tm.mu.Unlock()\n\t\t\tif expired {\n\t\t\t\tnazalog.Warnf(\"rtp pub timeout, streamName:%s, sessionId:%s\", session.StreamName, session.ID)\n\t\t\t\tm.stopSession(session)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (m *Manager) listen(port int, network string) (net.Listener, int, error) {\n\tif port > 0 {\n\t\tlistener, err := listenPort(port, network)\n\t\treturn listener, port, err\n\t}\n\n\tvar lastErr error\n\tfor p := m.portMin; p <= m.portMax; p++ {\n\t\tlistener, err := listenPort(p, network)\n\t\tif err == nil {\n\t\t\treturn listener, p, nil\n\t\t}\n\t\tlastErr = err\n\t}\n\tif lastErr == nil {\n\t\tlastErr = fmt.Errorf(\"no available %s port in range [%d,%d]\", network, m.portMin, m.portMax)\n\t}\n\treturn nil, 0, lastErr\n}\n\nfunc listenPort(port int, network string) (net.Listener, error) {\n\taddr := fmt.Sprintf(\":%d\", port)\n\tif network == \"tcp\" {\n\t\treturn net.Listen(\"tcp\", addr)\n\t}\n\n\tudpAddr, err := net.ResolveUDPAddr(\"udp\", addr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn udpTransport.Listen(\"udp\", udpAddr)\n}\n"
  },
  {
    "path": "gb28181/rtppub/manager_test.go",
    "content": "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/q191201771/lalmax/config\"\n)\n\nfunc freeTCPPort(t *testing.T) uint16 {\n\tt.Helper()\n\n\tlistener, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer listener.Close()\n\n\treturn uint16(listener.Addr().(*net.TCPAddr).Port)\n}\n\nfunc newTestManager(t *testing.T) *Manager {\n\tt.Helper()\n\n\treturn NewManager(nil, config.GB28181MediaConfig{})\n}\n\nfunc TestManagerStartStopBySessionID(t *testing.T) {\n\tmanager := newTestManager(t)\n\n\tresp := manager.Start(base.ApiCtrlStartRtpPubReq{\n\t\tStreamName: \"rtp-pub-start-stop\",\n\t\tPort:       int(freeTCPPort(t)),\n\t\tTimeoutMs:  0,\n\t\tIsTcpFlag:  1,\n\t})\n\tif resp.ErrorCode != base.ErrorCodeSucc {\n\t\tt.Fatalf(\"start failed, code=%d desp=%s\", resp.ErrorCode, resp.Desp)\n\t}\n\tif resp.Data.SessionId == \"\" || resp.Data.Port == 0 {\n\t\tt.Fatalf(\"unexpected start response: %+v\", resp.Data)\n\t}\n\n\tsession, err := manager.Stop(\"\", resp.Data.SessionId)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif session.ID != resp.Data.SessionId {\n\t\tt.Fatalf(\"stopped session id = %s, want %s\", session.ID, resp.Data.SessionId)\n\t}\n\n\tif _, err = manager.Stop(\"\", resp.Data.SessionId); !errors.Is(err, errSessionNotFound) {\n\t\tt.Fatalf(\"second stop err = %v, want %v\", err, errSessionNotFound)\n\t}\n}\n\nfunc TestManagerRejectsDuplicateStream(t *testing.T) {\n\tmanager := newTestManager(t)\n\n\tresp := manager.Start(base.ApiCtrlStartRtpPubReq{\n\t\tStreamName: \"rtp-pub-duplicate\",\n\t\tPort:       int(freeTCPPort(t)),\n\t\tTimeoutMs:  0,\n\t\tIsTcpFlag:  1,\n\t})\n\tif resp.ErrorCode != base.ErrorCodeSucc {\n\t\tt.Fatalf(\"start failed, code=%d desp=%s\", resp.ErrorCode, resp.Desp)\n\t}\n\tdefer manager.Stop(resp.Data.StreamName, \"\")\n\n\tduplicate := manager.Start(base.ApiCtrlStartRtpPubReq{\n\t\tStreamName: \"rtp-pub-duplicate\",\n\t\tPort:       int(freeTCPPort(t)),\n\t\tTimeoutMs:  0,\n\t\tIsTcpFlag:  1,\n\t})\n\tif duplicate.ErrorCode == base.ErrorCodeSucc {\n\t\tt.Fatalf(\"duplicate stream start unexpectedly succeeded: %+v\", duplicate.Data)\n\t}\n}\n\nfunc TestManagerTimeoutRemovesIdleSession(t *testing.T) {\n\tmanager := newTestManager(t)\n\n\tresp := manager.Start(base.ApiCtrlStartRtpPubReq{\n\t\tStreamName: \"rtp-pub-timeout\",\n\t\tPort:       int(freeTCPPort(t)),\n\t\tTimeoutMs:  10,\n\t\tIsTcpFlag:  1,\n\t})\n\tif resp.ErrorCode != base.ErrorCodeSucc {\n\t\tt.Fatalf(\"start failed, code=%d desp=%s\", resp.ErrorCode, resp.Desp)\n\t}\n\tdefer manager.Stop(resp.Data.StreamName, \"\")\n\n\tdeadline := time.After(2 * time.Second)\n\tticker := time.NewTicker(20 * time.Millisecond)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-deadline:\n\t\t\tt.Fatal(\"session was not removed after timeout\")\n\t\tcase <-ticker.C:\n\t\t\tmanager.mu.Lock()\n\t\t\t_, ok := manager.sessionsByID[resp.Data.SessionId]\n\t\t\tmanager.mu.Unlock()\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc TestNewManagerUsesConfiguredPortRangeAfterListenPort(t *testing.T) {\n\tmanager := NewManager(nil, config.GB28181MediaConfig{\n\t\tListenPort:            31000,\n\t\tMultiPortMaxIncrement: 10,\n\t})\n\n\tif manager.portMin != 31001 || manager.portMax != 31010 {\n\t\tt.Fatalf(\"port range = [%d,%d], want [31001,31010]\", manager.portMin, manager.portMax)\n\t}\n}\n"
  },
  {
    "path": "gb28181/rtppush/lower_push_session.go",
    "content": "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/lal/pkg/avc\"\n\tlalbase \"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/lal/pkg/hevc\"\n\t\"github.com/q191201771/lal/pkg/rtprtcp\"\n\t\"github.com/q191201771/lalmax/gb28181/mpegps\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n\t\"github.com/q191201771/naza/pkg/unique\"\n)\n\nconst (\n\tlowerPushNetworkUDP = \"udp\"\n\tlowerPushNetworkTCP = \"tcp\"\n\n\t// 单个 RTP 包承载的 PS 数据最大长度，避免 UDP 包过大导致分片。\n\tlowerPushRtpPacketMax = 1400\n\t// 启动阶段待处理 RTMP 消息的最大缓存数量，不是持续发送队列长度。\n\tlowerPushQueueMax = 256\n)\n\nvar lowerPushUnique = unique.NewSingleGenerator(\"GBLOWERPUSH\")\n\n// LowerPushSession 表示 lalmax 作为 GB28181 下级平台，\n// 主动向上级平台推送 RTP 媒体流。\n// 这里只支持主动 UDP/TCP 模式。\ntype LowerPushSession struct {\n\tuniqueKey  string\n\tstreamName string\n\n\tnetwork   string\n\tlocalIP   string\n\tlocalPort int\n\tpeerIP    string\n\tpeerPort  int\n\n\tudpConn net.Conn\n\ttcpConn net.Conn\n\n\tlog nazalog.Logger\n\n\tdisposeOnce sync.Once\n\n\twriteBytes uint64\n\n\tpsMuxer *mpegps.PsMuxer\n\tssrc    uint32\n\tseq     uint16\n\n\tvideoID uint8\n\taudioID uint8\n\n\tvideoHeader *lalbase.RtmpMsg\n\taudioHeader *lalbase.RtmpMsg\n\tascCtx      *aac.AscContext\n\n\tvideoCodec []byte\n\tpending    []lalbase.RtmpMsg\n\n\tready        bool\n\tonlyAudio    bool\n\twaitKeyFrame bool\n\teraseSei     bool\n}\n\n// NewLowerPushSession 创建下级平台推流会话，并绑定 PS 到 RTP 的发送回调。\nfunc NewLowerPushSession() *LowerPushSession {\n\ts := &LowerPushSession{\n\t\tuniqueKey: lowerPushUnique.GenUniqueKey(),\n\t\tlog:       nazalog.GetGlobalLogger(),\n\t\teraseSei:  true,\n\t}\n\ts.log = s.log.WithPrefix(s.uniqueKey)\n\ts.psMuxer = mpegps.NewPsMuxer()\n\ts.psMuxer.OnPacket = func(pkg []byte, pts uint64) {\n\t\tfor _, pkt := range s.packRtp(pkg, uint32(pts)) {\n\t\t\tif err := s.WriteRtpPacket(pkt); err != nil {\n\t\t\t\ts.log.Warnf(\"gb28181 lower push write rtp failed. err=%v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\treturn s\n}\n\n// WithStreamName 设置业务流名称，便于日志和外部管理。\nfunc (s *LowerPushSession) WithStreamName(streamName string) *LowerPushSession {\n\ts.streamName = streamName\n\treturn s\n}\n\n// WithLogPrefix 设置日志前缀，用于区分不同推流会话。\nfunc (s *LowerPushSession) WithLogPrefix(prefix string) *LowerPushSession {\n\ts.log = s.log.WithPrefix(prefix)\n\treturn s\n}\n\n// SetLocalIP 设置本地绑定 IP，为空时由系统自动选择。\nfunc (s *LowerPushSession) SetLocalIP(localIP string) {\n\ts.localIP = localIP\n}\n\n// SetLocalPort 设置本地绑定端口，为 0 时由系统自动分配。\nfunc (s *LowerPushSession) SetLocalPort(localPort int) {\n\ts.localPort = localPort\n}\n\n// SetPeerIP 设置上级平台接收 RTP 的 IP。\nfunc (s *LowerPushSession) SetPeerIP(peerIP string) {\n\ts.peerIP = peerIP\n}\n\n// SetPeerPort 设置上级平台接收 RTP 的端口。\nfunc (s *LowerPushSession) SetPeerPort(peerPort int) {\n\ts.peerPort = peerPort\n}\n\n// SetSsrc 设置 RTP 包中的 SSRC。\nfunc (s *LowerPushSession) SetSsrc(ssrc uint32) {\n\ts.ssrc = ssrc\n}\n\n// Start 按指定网络类型启动推流连接，当前支持 UDP 和 TCP 主动连接。\nfunc (s *LowerPushSession) Start(network string) error {\n\tswitch network {\n\tcase lowerPushNetworkUDP:\n\t\treturn s.startUDP()\n\tcase lowerPushNetworkTCP:\n\t\treturn s.startTCP()\n\tdefault:\n\t\treturn fmt.Errorf(\"gb28181 lower push invalid network: %s\", network)\n\t}\n}\n\n// startUDP 建立到上级平台的 UDP 连接。\nfunc (s *LowerPushSession) startUDP() error {\n\tladdr := &net.UDPAddr{Port: s.localPort}\n\tif s.localIP != \"\" {\n\t\tladdr.IP = net.ParseIP(s.localIP)\n\t}\n\traddr := &net.UDPAddr{\n\t\tIP:   net.ParseIP(s.peerIP),\n\t\tPort: s.peerPort,\n\t}\n\tif raddr.IP == nil || raddr.Port == 0 {\n\t\treturn fmt.Errorf(\"gb28181 lower push invalid udp peer addr: %s:%d\", s.peerIP, s.peerPort)\n\t}\n\n\tconn, err := net.DialUDP(lowerPushNetworkUDP, laddr, raddr)\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.network = lowerPushNetworkUDP\n\ts.udpConn = conn\n\ts.log.Infof(\"gb28181 lower push udp ready. local=%s remote=%s\", conn.LocalAddr(), conn.RemoteAddr())\n\treturn nil\n}\n\n// startTCP 建立到上级平台的 TCP 连接。\nfunc (s *LowerPushSession) startTCP() error {\n\tlocalAddr := &net.TCPAddr{Port: s.localPort}\n\tif s.localIP != \"\" {\n\t\tlocalAddr.IP = net.ParseIP(s.localIP)\n\t}\n\tdialer := &net.Dialer{\n\t\tLocalAddr: localAddr,\n\t\tTimeout:   3 * time.Second,\n\t}\n\tconn, err := dialer.Dial(lowerPushNetworkTCP, net.JoinHostPort(s.peerIP, fmt.Sprintf(\"%d\", s.peerPort)))\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.network = lowerPushNetworkTCP\n\ts.tcpConn = conn\n\ts.log.Infof(\"gb28181 lower push tcp ready. local=%s remote=%s\", conn.LocalAddr(), conn.RemoteAddr())\n\treturn nil\n}\n\n// OnMsg 接收 RTMP 消息，完成启动阶段缓存、音视频头解析和后续 PS/RTP 推送。\nfunc (s *LowerPushSession) OnMsg(msg lalbase.RtmpMsg) {\n\tif s.consumeControlMsg(msg) {\n\t\tif !s.ready && s.shouldDrain(msg) {\n\t\t\ts.drain()\n\t\t}\n\t\treturn\n\t}\n\n\tif s.ready {\n\t\tif err := s.feedRtmpMsg(msg); err != nil {\n\t\t\ts.log.Warnf(\"gb28181 lower push feed msg failed. err=%v, msg=%s\", err, msg.DebugString())\n\t\t}\n\t\treturn\n\t}\n\n\ts.pending = append(s.pending, msg.Clone())\n\tif s.shouldDrain(msg) || len(s.pending) >= lowerPushQueueMax {\n\t\ts.drain()\n\t}\n}\n\n// OnStop 在上游流停止时释放推流连接。\nfunc (s *LowerPushSession) OnStop() {\n\t_ = s.Dispose()\n}\n\n// WriteRtpPacket 按当前网络类型发送已经封装好的 RTP 包。\nfunc (s *LowerPushSession) WriteRtpPacket(pkt rtprtcp.RtpPacket) error {\n\tif s.network == \"\" {\n\t\treturn lalbase.ErrSessionNotStarted\n\t}\n\tswitch s.network {\n\tcase lowerPushNetworkUDP:\n\t\treturn s.writeUDP(pkt.Raw)\n\tcase lowerPushNetworkTCP:\n\t\treturn s.writeTCP(pkt.Raw)\n\tdefault:\n\t\treturn fmt.Errorf(\"gb28181 lower push invalid network state: %s\", s.network)\n\t}\n}\n\n// WriteRtpPsPacket 写入单个 RTP/PS 包。\n// 在 TCP 模式下，既支持原始 RTP 负载，也支持带 2 字节长度前缀的 RTP 包。\n// 在 UDP 模式下，只发送 RTP 负载本身。\nfunc (s *LowerPushSession) WriteRtpPsPacket(buf []byte) error {\n\tif s.network == \"\" {\n\t\treturn lalbase.ErrSessionNotStarted\n\t}\n\tpayload := buf\n\tif len(buf) >= 2 && len(buf) == int(uint16(buf[0])<<8|uint16(buf[1]))+2 {\n\t\tpayload = buf[2:]\n\t}\n\tswitch s.network {\n\tcase lowerPushNetworkUDP:\n\t\treturn s.writeUDP(payload)\n\tcase lowerPushNetworkTCP:\n\t\treturn s.writeTCP(payload)\n\tdefault:\n\t\treturn fmt.Errorf(\"gb28181 lower push invalid network state: %s\", s.network)\n\t}\n}\n\n// writeUDP 通过 UDP 直接发送 RTP 负载。\nfunc (s *LowerPushSession) writeUDP(payload []byte) error {\n\tif s.udpConn == nil {\n\t\treturn lalbase.ErrSessionNotStarted\n\t}\n\tn, err := s.udpConn.Write(payload)\n\ts.writeBytes += uint64(n)\n\treturn err\n}\n\n// writeTCP 按 GB28181 TCP 传输格式添加 2 字节长度前缀后发送 RTP 负载。\nfunc (s *LowerPushSession) writeTCP(payload []byte) error {\n\tif s.tcpConn == nil {\n\t\treturn lalbase.ErrSessionNotStarted\n\t}\n\theader := []byte{byte(len(payload) >> 8), byte(len(payload))}\n\tn, err := s.tcpConn.Write(append(header, payload...))\n\ts.writeBytes += uint64(n)\n\treturn err\n}\n\n// Dispose 关闭底层连接，重复调用是安全的。\nfunc (s *LowerPushSession) Dispose() error {\n\tvar retErr error\n\ts.disposeOnce.Do(func() {\n\t\tif s.udpConn != nil {\n\t\t\tretErr = s.udpConn.Close()\n\t\t\ts.udpConn = nil\n\t\t}\n\t\tif s.tcpConn != nil {\n\t\t\tif err := s.tcpConn.Close(); retErr == nil {\n\t\t\t\tretErr = err\n\t\t\t}\n\t\t\ts.tcpConn = nil\n\t\t}\n\t})\n\treturn retErr\n}\n\n// UniqueKey 返回当前推流会话的唯一标识。\nfunc (s *LowerPushSession) UniqueKey() string {\n\treturn s.uniqueKey\n}\n\n// StreamName 返回业务流名称，未设置时使用会话唯一标识。\nfunc (s *LowerPushSession) StreamName() string {\n\tif s.streamName == \"\" {\n\t\treturn s.uniqueKey\n\t}\n\treturn s.streamName\n}\n\n// LocalAddr 返回当前连接的本地地址。\nfunc (s *LowerPushSession) LocalAddr() net.Addr {\n\tif s.udpConn != nil {\n\t\treturn s.udpConn.LocalAddr()\n\t}\n\tif s.tcpConn != nil {\n\t\treturn s.tcpConn.LocalAddr()\n\t}\n\treturn nil\n}\n\n// RemoteAddr 返回当前连接的远端地址。\nfunc (s *LowerPushSession) RemoteAddr() net.Addr {\n\tif s.udpConn != nil {\n\t\treturn s.udpConn.RemoteAddr()\n\t}\n\tif s.tcpConn != nil {\n\t\treturn s.tcpConn.RemoteAddr()\n\t}\n\treturn nil\n}\n\n// consumeControlMsg 处理元数据、音视频序列头和不支持的消息，返回 true 表示不再进入媒体发送流程。\nfunc (s *LowerPushSession) consumeControlMsg(msg lalbase.RtmpMsg) bool {\n\tswitch msg.Header.MsgTypeId {\n\tcase lalbase.RtmpTypeIdMetadata:\n\t\treturn true\n\tcase lalbase.RtmpTypeIdVideo:\n\t\tif len(msg.Payload) < 2 {\n\t\t\treturn true\n\t\t}\n\t\tif msg.IsVideoKeySeqHeader() {\n\t\t\tif err := s.updateVideoHeader(msg); err != nil {\n\t\t\t\ts.log.Warnf(\"gb28181 lower push parse video seq header failed. err=%v\", err)\n\t\t\t}\n\t\t\treturn true\n\t\t}\n\t\tif msg.IsEnhanced() && !msg.IsEnchanedHevcNalu() {\n\t\t\treturn true\n\t\t}\n\tcase lalbase.RtmpTypeIdAudio:\n\t\tif len(msg.Payload) < 1 {\n\t\t\treturn true\n\t\t}\n\t\tswitch msg.AudioCodecId() {\n\t\tcase lalbase.RtmpSoundFormatAac:\n\t\t\tif len(msg.Payload) < 2 {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif msg.IsAacSeqHeader() {\n\t\t\t\tif err := s.updateAacHeader(msg); err != nil {\n\t\t\t\t\ts.log.Warnf(\"gb28181 lower push parse aac seq header failed. err=%v\", err)\n\t\t\t\t}\n\t\t\t\treturn true\n\t\t\t}\n\t\tcase lalbase.RtmpSoundFormatG711A:\n\t\t\tif s.audioID == 0 {\n\t\t\t\ts.audioID = s.psMuxer.AddStream(mpegps.PsStreamG711A)\n\t\t\t}\n\t\t\tif s.audioHeader == nil {\n\t\t\t\tcloned := msg.Clone()\n\t\t\t\ts.audioHeader = &cloned\n\t\t\t}\n\t\tcase lalbase.RtmpSoundFormatG711U:\n\t\t\tif s.audioID == 0 {\n\t\t\t\ts.audioID = s.psMuxer.AddStream(mpegps.PsStreamG711U)\n\t\t\t}\n\t\t\tif s.audioHeader == nil {\n\t\t\t\tcloned := msg.Clone()\n\t\t\t\ts.audioHeader = &cloned\n\t\t\t}\n\t\tcase lalbase.RtmpSoundFormatOpus:\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// updateVideoHeader 解析并缓存 H264/H265 序列头，同时注册对应的 PS 视频流。\nfunc (s *LowerPushSession) updateVideoHeader(msg lalbase.RtmpMsg) error {\n\tif msg.IsAvcKeySeqHeader() {\n\t\tif s.videoID == 0 {\n\t\t\ts.videoID = s.psMuxer.AddStream(mpegps.PsStreamH264)\n\t\t}\n\t\tcodec, err := avc.SpsPpsSeqHeader2Annexb(msg.Payload)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ts.videoCodec = append(s.videoCodec[:0], codec...)\n\t} else if msg.IsHevcKeySeqHeader() {\n\t\tif s.videoID == 0 {\n\t\t\ts.videoID = s.psMuxer.AddStream(mpegps.PsStreamH265)\n\t\t}\n\t\tvar (\n\t\t\tcodec []byte\n\t\t\terr   error\n\t\t)\n\t\tif msg.IsEnhanced() {\n\t\t\tcodec, err = hevc.VpsSpsPpsEnhancedSeqHeader2Annexb(msg.Payload)\n\t\t} else {\n\t\t\tcodec, err = hevc.VpsSpsPpsSeqHeader2Annexb(msg.Payload)\n\t\t}\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ts.videoCodec = append(s.videoCodec[:0], codec...)\n\t}\n\n\tcloned := msg.Clone()\n\ts.videoHeader = &cloned\n\treturn nil\n}\n\n// updateAacHeader 解析并缓存 AAC 序列头，同时注册对应的 PS 音频流。\nfunc (s *LowerPushSession) updateAacHeader(msg lalbase.RtmpMsg) error {\n\tif s.audioID == 0 {\n\t\ts.audioID = s.psMuxer.AddStream(mpegps.PsStreamAac)\n\t}\n\tascCtx, err := aac.NewAscContext(msg.Payload[2:])\n\tif err != nil {\n\t\treturn err\n\t}\n\ts.ascCtx = ascCtx\n\tcloned := msg.Clone()\n\ts.audioHeader = &cloned\n\treturn nil\n}\n\n// shouldDrain 判断启动阶段缓存是否已经满足发送条件。\nfunc (s *LowerPushSession) shouldDrain(msg lalbase.RtmpMsg) bool {\n\tif s.videoHeader != nil && s.audioHeader != nil {\n\t\treturn true\n\t}\n\tif s.videoHeader != nil && msg.Header.MsgTypeId == lalbase.RtmpTypeIdVideo && !msg.IsVideoKeySeqHeader() {\n\t\treturn true\n\t}\n\tif s.videoHeader == nil && s.audioHeader != nil && msg.Header.MsgTypeId == lalbase.RtmpTypeIdAudio {\n\t\tif len(msg.Payload) == 0 {\n\t\t\treturn false\n\t\t}\n\t\tif msg.AudioCodecId() == lalbase.RtmpSoundFormatAac {\n\t\t\treturn !msg.IsAacSeqHeader()\n\t\t}\n\t\treturn true\n\t}\n\treturn false\n}\n\n// drain 结束启动阶段缓存，将已缓存消息按顺序送入打包流程。\nfunc (s *LowerPushSession) drain() {\n\tif s.ready {\n\t\treturn\n\t}\n\ts.ready = true\n\ts.onlyAudio = s.videoHeader == nil && s.audioHeader != nil\n\ts.waitKeyFrame = s.videoHeader != nil\n\n\tfor i := range s.pending {\n\t\tif err := s.feedRtmpMsg(s.pending[i]); err != nil {\n\t\t\ts.log.Warnf(\"gb28181 lower push drain msg failed. err=%v, msg=%s\", err, s.pending[i].DebugString())\n\t\t}\n\t}\n\ts.pending = nil\n}\n\n// feedRtmpMsg 按消息类型分发音频或视频数据。\nfunc (s *LowerPushSession) feedRtmpMsg(msg lalbase.RtmpMsg) error {\n\tswitch msg.Header.MsgTypeId {\n\tcase lalbase.RtmpTypeIdVideo:\n\t\tif s.onlyAudio {\n\t\t\treturn nil\n\t\t}\n\t\treturn s.feedVideo(msg)\n\tcase lalbase.RtmpTypeIdAudio:\n\t\treturn s.feedAudio(msg)\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// feedVideo 将 RTMP 视频帧转换为 Annex-B 格式，并写入 PS 复用器。\nfunc (s *LowerPushSession) feedVideo(msg lalbase.RtmpMsg) error {\n\tstartIndex := 5\n\tif msg.IsEnchanedHevcNalu() {\n\t\tstartIndex = msg.GetEnchanedHevcNaluIndex()\n\t}\n\tif len(msg.Payload) <= startIndex {\n\t\treturn nil\n\t}\n\n\tvar (\n\t\tbuf            []byte\n\t\tappendCodec    bool\n\t\tsps            []byte\n\t\tpps            []byte\n\t\tvps            []byte\n\t\terr            error\n\t\tisH264         = msg.VideoCodecId() == lalbase.RtmpCodecIdAvc\n\t\tvideoPayload   = msg.Payload[startIndex:]\n\t\tappendStartCde = avc.NaluStartCode4\n\t)\n\n\terr = avc.IterateNaluAvcc(videoPayload, func(nal []byte) {\n\t\tif len(nal) == 0 {\n\t\t\treturn\n\t\t}\n\n\t\tif isH264 {\n\t\t\tswitch avc.ParseNaluType(nal[0]) {\n\t\t\tcase avc.NaluTypeSps:\n\t\t\t\tsps = nal\n\t\t\tcase avc.NaluTypePps:\n\t\t\t\tpps = nal\n\t\t\t\tif len(sps) != 0 {\n\t\t\t\t\ts.videoCodec = s.videoCodec[:0]\n\t\t\t\t\ts.videoCodec = append(s.videoCodec, appendStartCde...)\n\t\t\t\t\ts.videoCodec = append(s.videoCodec, sps...)\n\t\t\t\t\ts.videoCodec = append(s.videoCodec, appendStartCde...)\n\t\t\t\t\ts.videoCodec = append(s.videoCodec, pps...)\n\t\t\t\t}\n\t\t\tcase avc.NaluTypeIdrSlice:\n\t\t\t\tif !appendCodec && len(s.videoCodec) != 0 {\n\t\t\t\t\tbuf = append(buf, s.videoCodec...)\n\t\t\t\t\tappendCodec = true\n\t\t\t\t}\n\t\t\t\tbuf = append(buf, appendStartCde...)\n\t\t\t\tbuf = append(buf, nal...)\n\t\t\t\ts.waitKeyFrame = false\n\t\t\tcase avc.NaluTypeSei:\n\t\t\t\tif !s.eraseSei {\n\t\t\t\t\tbuf = append(buf, appendStartCde...)\n\t\t\t\t\tbuf = append(buf, nal...)\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tif s.waitKeyFrame {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tbuf = append(buf, appendStartCde...)\n\t\t\t\tbuf = append(buf, nal...)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tswitch hevc.ParseNaluType(nal[0]) {\n\t\tcase hevc.NaluTypeVps:\n\t\t\tvps = nal\n\t\tcase hevc.NaluTypeSps:\n\t\t\tsps = nal\n\t\tcase hevc.NaluTypePps:\n\t\t\tpps = nal\n\t\t\tif len(vps) != 0 && len(sps) != 0 {\n\t\t\t\ts.videoCodec = s.videoCodec[:0]\n\t\t\t\ts.videoCodec = append(s.videoCodec, appendStartCde...)\n\t\t\t\ts.videoCodec = append(s.videoCodec, vps...)\n\t\t\t\ts.videoCodec = append(s.videoCodec, appendStartCde...)\n\t\t\t\ts.videoCodec = append(s.videoCodec, sps...)\n\t\t\t\ts.videoCodec = append(s.videoCodec, appendStartCde...)\n\t\t\t\ts.videoCodec = append(s.videoCodec, pps...)\n\t\t\t}\n\t\tcase hevc.NaluTypeSei, hevc.NaluTypeSeiSuffix:\n\t\t\tif !s.eraseSei {\n\t\t\t\tbuf = append(buf, appendStartCde...)\n\t\t\t\tbuf = append(buf, nal...)\n\t\t\t}\n\t\tdefault:\n\t\t\tif hevc.IsIrapNalu(hevc.ParseNaluType(nal[0])) {\n\t\t\t\tif !appendCodec && len(s.videoCodec) != 0 {\n\t\t\t\t\tbuf = append(buf, s.videoCodec...)\n\t\t\t\t\tappendCodec = true\n\t\t\t\t}\n\t\t\t\tbuf = append(buf, appendStartCde...)\n\t\t\t\tbuf = append(buf, nal...)\n\t\t\t\ts.waitKeyFrame = false\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif s.waitKeyFrame {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tbuf = append(buf, appendStartCde...)\n\t\t\tbuf = append(buf, nal...)\n\t\t}\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif len(buf) == 0 || s.videoID == 0 {\n\t\treturn nil\n\t}\n\treturn s.psMuxer.Write(s.videoID, buf, uint64(msg.Pts()), uint64(msg.Dts()))\n}\n\n// feedAudio 将 RTMP 音频帧转换为 PS 支持的音频负载，并写入 PS 复用器。\nfunc (s *LowerPushSession) feedAudio(msg lalbase.RtmpMsg) error {\n\tif s.waitKeyFrame {\n\t\treturn nil\n\t}\n\tif len(msg.Payload) == 0 {\n\t\treturn nil\n\t}\n\n\tswitch msg.AudioCodecId() {\n\tcase lalbase.RtmpSoundFormatAac:\n\t\tif len(msg.Payload) <= 2 || s.ascCtx == nil || s.audioID == 0 {\n\t\t\treturn nil\n\t\t}\n\t\tbuf := s.ascCtx.PackAdtsHeader(len(msg.Payload) - 2)\n\t\tbuf = append(buf, msg.Payload[2:]...)\n\t\treturn s.psMuxer.Write(s.audioID, buf, uint64(msg.Dts()), uint64(msg.Dts()))\n\tcase lalbase.RtmpSoundFormatG711A, lalbase.RtmpSoundFormatG711U:\n\t\tif len(msg.Payload) <= 1 || s.audioID == 0 {\n\t\t\treturn nil\n\t\t}\n\t\treturn s.psMuxer.Write(s.audioID, msg.Payload[1:], uint64(msg.Dts()), uint64(msg.Dts()))\n\tdefault:\n\t\treturn nil\n\t}\n}\n\n// nextSeq 生成下一个 RTP 序列号。\nfunc (s *LowerPushSession) nextSeq() uint16 {\n\ts.seq++\n\treturn s.seq\n}\n\n// packRtp 将 PS 数据按 RTP 最大负载长度切片，并生成 RTP 包。\nfunc (s *LowerPushSession) packRtp(buf []byte, timestamp uint32) []rtprtcp.RtpPacket {\n\tvar out []rtprtcp.RtpPacket\n\tfor offset := 0; offset < len(buf); {\n\t\tsize := len(buf) - offset\n\t\tmark := uint8(1)\n\t\tif size > lowerPushRtpPacketMax {\n\t\t\tsize = lowerPushRtpPacketMax\n\t\t\tmark = 0\n\t\t}\n\n\t\th := rtprtcp.MakeDefaultRtpHeader()\n\t\th.Mark = mark\n\t\th.PacketType = uint8(lalbase.AvPacketPtAvc)\n\t\th.Seq = s.nextSeq()\n\t\th.Timestamp = timestamp\n\t\th.Ssrc = s.ssrc\n\n\t\tout = append(out, rtprtcp.MakeRtpPacket(h, buf[offset:offset+size]))\n\t\toffset += size\n\t}\n\treturn out\n}\n"
  },
  {
    "path": "gb28181/rtppush/lower_push_session_test.go",
    "content": "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/lal/pkg/avc\"\n\tlalbase \"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/lal/pkg/rtprtcp\"\n)\n\nfunc TestLowerPushSessionUDPWriteRtpPacket(t *testing.T) {\n\tln, err := net.ListenPacket(\"udp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\tt.Fatalf(\"listen udp: %v\", err)\n\t}\n\tdefer ln.Close()\n\n\tserverAddr := ln.LocalAddr().(*net.UDPAddr)\n\tgot := make(chan []byte, 1)\n\terrCh := make(chan error, 1)\n\tgo func() {\n\t\tbuf := make([]byte, 1500)\n\t\t_ = ln.SetDeadline(time.Now().Add(3 * time.Second))\n\t\tn, _, err := ln.ReadFrom(buf)\n\t\tif err != nil {\n\t\t\terrCh <- err\n\t\t\treturn\n\t\t}\n\t\tgot <- append([]byte(nil), buf[:n]...)\n\t}()\n\n\tsession := NewLowerPushSession()\n\tsession.SetPeerIP(serverAddr.IP.String())\n\tsession.SetPeerPort(serverAddr.Port)\n\tif err := session.Start(\"udp\"); err != nil {\n\t\tt.Fatalf(\"start udp: %v\", err)\n\t}\n\tdefer session.Dispose()\n\n\tpkt := makeTestRtpPacket([]byte{0x11, 0x22, 0x33, 0x44})\n\tif err := session.WriteRtpPacket(pkt); err != nil {\n\t\tt.Fatalf(\"write udp rtp: %v\", err)\n\t}\n\n\tselect {\n\tcase err := <-errCh:\n\t\tt.Fatalf(\"udp read failed: %v\", err)\n\tcase b := <-got:\n\t\tif string(b) != string(pkt.Raw) {\n\t\t\tt.Fatalf(\"udp payload mismatch, got=%v want=%v\", b, pkt.Raw)\n\t\t}\n\tcase <-time.After(4 * time.Second):\n\t\tt.Fatal(\"udp read timeout\")\n\t}\n}\n\nfunc TestLowerPushSessionTCPWriteRtpPsPacket(t *testing.T) {\n\tln, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\tt.Fatalf(\"listen tcp: %v\", err)\n\t}\n\tdefer ln.Close()\n\n\tgot := make(chan []byte, 1)\n\terrCh := make(chan error, 1)\n\tgo func() {\n\t\tconn, err := ln.Accept()\n\t\tif err != nil {\n\t\t\terrCh <- err\n\t\t\treturn\n\t\t}\n\t\tdefer conn.Close()\n\n\t\t_ = conn.SetDeadline(time.Now().Add(3 * time.Second))\n\t\tbuf := make([]byte, 6)\n\t\tif _, err := io.ReadFull(conn, buf); err != nil {\n\t\t\terrCh <- err\n\t\t\treturn\n\t\t}\n\t\tgot <- buf\n\t}()\n\n\taddr := ln.Addr().(*net.TCPAddr)\n\tsession := NewLowerPushSession()\n\tsession.SetPeerIP(addr.IP.String())\n\tsession.SetPeerPort(addr.Port)\n\tif err := session.Start(\"tcp\"); err != nil {\n\t\tt.Fatalf(\"start tcp: %v\", err)\n\t}\n\tdefer session.Dispose()\n\n\trawRTP := []byte{0x80, 0x60, 0x00, 0x01}\n\tif err := session.WriteRtpPsPacket(rawRTP); err != nil {\n\t\tt.Fatalf(\"write tcp rtp/ps: %v\", err)\n\t}\n\n\tselect {\n\tcase err := <-errCh:\n\t\tt.Fatalf(\"tcp read failed: %v\", err)\n\tcase b := <-got:\n\t\twant := []byte{0x00, 0x04, 0x80, 0x60, 0x00, 0x01}\n\t\tif string(b) != string(want) {\n\t\t\tt.Fatalf(\"tcp payload mismatch, got=%v want=%v\", b, want)\n\t\t}\n\tcase <-time.After(4 * time.Second):\n\t\tt.Fatal(\"tcp read timeout\")\n\t}\n}\n\nfunc TestLowerPushSessionWriteBeforeStart(t *testing.T) {\n\tsession := NewLowerPushSession()\n\terr := session.WriteRtpPacket(makeTestRtpPacket([]byte{0x01}))\n\tif err != lalbase.ErrSessionNotStarted {\n\t\tt.Fatalf(\"unexpected error: %v\", err)\n\t}\n}\n\nfunc TestLowerPushSessionOnMsgVideoUDP(t *testing.T) {\n\tln, err := net.ListenPacket(\"udp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\tt.Fatalf(\"listen udp: %v\", err)\n\t}\n\tdefer ln.Close()\n\n\tserverAddr := ln.LocalAddr().(*net.UDPAddr)\n\tgot := make(chan []byte, 2)\n\terrCh := make(chan error, 1)\n\tgo func() {\n\t\tfor i := 0; i < 2; i++ {\n\t\t\tbuf := make([]byte, 2048)\n\t\t\t_ = ln.SetDeadline(time.Now().Add(3 * time.Second))\n\t\t\tn, _, err := ln.ReadFrom(buf)\n\t\t\tif err != nil {\n\t\t\t\terrCh <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t\tgot <- append([]byte(nil), buf[:n]...)\n\t\t}\n\t}()\n\n\tsession := NewLowerPushSession()\n\tsession.SetPeerIP(serverAddr.IP.String())\n\tsession.SetPeerPort(serverAddr.Port)\n\tsession.SetSsrc(0x11223344)\n\tif err := session.Start(\"udp\"); err != nil {\n\t\tt.Fatalf(\"start udp: %v\", err)\n\t}\n\tdefer session.Dispose()\n\n\tsession.OnMsg(makeAvcSeqHeaderMsg())\n\tsession.OnMsg(makeAacSeqHeaderMsg())\n\tsession.OnMsg(makeAvcKeyFrameMsg())\n\tsession.OnMsg(makeAacRawMsg())\n\n\tvar pkts []rtprtcp.RtpPacket\n\tdeadline := time.After(4 * time.Second)\n\tfor len(pkts) < 2 {\n\t\tselect {\n\t\tcase err := <-errCh:\n\t\t\tt.Fatalf(\"udp read failed: %v\", err)\n\t\tcase b := <-got:\n\t\t\tpkt, err := rtprtcp.ParseRtpPacket(b)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatalf(\"parse rtp packet failed: %v\", err)\n\t\t\t}\n\t\t\tpkts = append(pkts, pkt)\n\t\tcase <-deadline:\n\t\t\tt.Fatal(\"udp onmsg timeout\")\n\t\t}\n\t}\n\n\tfoundPS := false\n\tfor _, pkt := range pkts {\n\t\tif pkt.Header.Ssrc != 0x11223344 {\n\t\t\tt.Fatalf(\"ssrc mismatch. got=%d\", pkt.Header.Ssrc)\n\t\t}\n\t\tif pkt.Header.PacketType != uint8(lalbase.AvPacketPtAvc) {\n\t\t\tt.Fatalf(\"payload type mismatch. got=%d\", pkt.Header.PacketType)\n\t\t}\n\t\tbody := pkt.Body()\n\t\tif len(body) >= 4 && body[0] == 0x00 && body[1] == 0x00 && body[2] == 0x01 && body[3] == 0xBA {\n\t\t\tfoundPS = true\n\t\t}\n\t}\n\tif !foundPS {\n\t\tt.Fatalf(\"expected ps pack header in udp packets\")\n\t}\n}\n\nfunc TestLowerPushSessionOnMsgAudioTCP(t *testing.T) {\n\tln, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\tif err != nil {\n\t\tt.Fatalf(\"listen tcp: %v\", err)\n\t}\n\tdefer ln.Close()\n\n\tgot := make(chan []byte, 1)\n\terrCh := make(chan error, 1)\n\tgo func() {\n\t\tconn, err := ln.Accept()\n\t\tif err != nil {\n\t\t\terrCh <- err\n\t\t\treturn\n\t\t}\n\t\tdefer conn.Close()\n\n\t\t_ = conn.SetDeadline(time.Now().Add(3 * time.Second))\n\t\tsizeBuf := make([]byte, 2)\n\t\tif _, err := io.ReadFull(conn, sizeBuf); err != nil {\n\t\t\terrCh <- err\n\t\t\treturn\n\t\t}\n\t\tsize := int(sizeBuf[0])<<8 | int(sizeBuf[1])\n\t\tpayload := make([]byte, size)\n\t\tif _, err := io.ReadFull(conn, payload); err != nil {\n\t\t\terrCh <- err\n\t\t\treturn\n\t\t}\n\t\tgot <- append(sizeBuf, payload...)\n\t}()\n\n\taddr := ln.Addr().(*net.TCPAddr)\n\tsession := NewLowerPushSession()\n\tsession.SetPeerIP(addr.IP.String())\n\tsession.SetPeerPort(addr.Port)\n\tsession.SetSsrc(0x55667788)\n\tif err := session.Start(\"tcp\"); err != nil {\n\t\tt.Fatalf(\"start tcp: %v\", err)\n\t}\n\tdefer session.Dispose()\n\n\tsession.OnMsg(makeG711AMsg())\n\n\tselect {\n\tcase err := <-errCh:\n\t\tt.Fatalf(\"tcp read failed: %v\", err)\n\tcase b := <-got:\n\t\tif len(b) < 14 {\n\t\t\tt.Fatalf(\"tcp packet too short: %d\", len(b))\n\t\t}\n\t\tsize := int(b[0])<<8 | int(b[1])\n\t\tif size != len(b)-2 {\n\t\t\tt.Fatalf(\"tcp length mismatch. prefix=%d actual=%d\", size, len(b)-2)\n\t\t}\n\t\tpkt, err := rtprtcp.ParseRtpPacket(b[2:])\n\t\tif err != nil {\n\t\t\tt.Fatalf(\"parse tcp rtp failed: %v\", err)\n\t\t}\n\t\tif pkt.Header.Ssrc != 0x55667788 {\n\t\t\tt.Fatalf(\"ssrc mismatch. got=%d\", pkt.Header.Ssrc)\n\t\t}\n\t\tbody := pkt.Body()\n\t\tif len(body) < 4 || body[0] != 0x00 || body[1] != 0x00 || body[2] != 0x01 || body[3] != 0xBA {\n\t\t\tt.Fatalf(\"expected ps pack header, body prefix=%v\", body[:min(4, len(body))])\n\t\t}\n\tcase <-time.After(4 * time.Second):\n\t\tt.Fatal(\"tcp onmsg timeout\")\n\t}\n}\n\nfunc makeTestRtpPacket(payload []byte) rtprtcp.RtpPacket {\n\th := rtprtcp.MakeDefaultRtpHeader()\n\th.PacketType = uint8(lalbase.AvPacketPtAvc)\n\th.Seq = 1\n\th.Timestamp = 90000\n\th.Ssrc = 1234\n\treturn rtprtcp.MakeRtpPacket(h, payload)\n}\n\nfunc makeAvcSeqHeaderMsg() lalbase.RtmpMsg {\n\tpayload := []byte{\n\t\t0x17, 0x00, 0x00, 0x00, 0x00,\n\t\t0x01, 0x64, 0x00, 0x20, 0xFF,\n\t\t0xE1, 0x00, 0x19,\n\t\t0x67, 0x64, 0x00, 0x20, 0xAC, 0xD9, 0x40, 0xC0, 0x29, 0xB0, 0x11, 0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x00, 0x03, 0x00, 0x32, 0x0F, 0x18, 0x31, 0x96,\n\t\t0x01, 0x00, 0x05,\n\t\t0x68, 0xEB, 0xEC, 0xB2, 0x2C,\n\t}\n\treturn lalbase.RtmpMsg{\n\t\tHeader: lalbase.RtmpHeader{\n\t\t\tMsgTypeId:    lalbase.RtmpTypeIdVideo,\n\t\t\tMsgLen:       uint32(len(payload)),\n\t\t\tTimestampAbs: 0,\n\t\t},\n\t\tPayload: payload,\n\t}\n}\n\nfunc makeAvcKeyFrameMsg() lalbase.RtmpMsg {\n\tidr := []byte{0x65, 0x88, 0x84, 0x21, 0xA0}\n\tpayload := make([]byte, 5+4+len(idr))\n\tpayload[0] = lalbase.RtmpAvcKeyFrame\n\tpayload[1] = lalbase.RtmpAvcPacketTypeNalu\n\tpayload[2] = 0\n\tpayload[3] = 0\n\tpayload[4] = 0\n\tpayload[5] = 0\n\tpayload[6] = 0\n\tpayload[7] = 0\n\tpayload[8] = byte(len(idr))\n\tcopy(payload[9:], idr)\n\treturn lalbase.RtmpMsg{\n\t\tHeader: lalbase.RtmpHeader{\n\t\t\tMsgTypeId:    lalbase.RtmpTypeIdVideo,\n\t\t\tMsgLen:       uint32(len(payload)),\n\t\t\tTimestampAbs: 40,\n\t\t},\n\t\tPayload: payload,\n\t}\n}\n\nfunc makeAacSeqHeaderMsg() lalbase.RtmpMsg {\n\tpayload := []byte{0xAF, 0x00, 0x11, 0x90}\n\treturn lalbase.RtmpMsg{\n\t\tHeader: lalbase.RtmpHeader{\n\t\t\tMsgTypeId:    lalbase.RtmpTypeIdAudio,\n\t\t\tMsgLen:       uint32(len(payload)),\n\t\t\tTimestampAbs: 0,\n\t\t},\n\t\tPayload: payload,\n\t}\n}\n\nfunc makeAacRawMsg() lalbase.RtmpMsg {\n\traw := []byte{0x21, 0x2B, 0x94, 0xA5, 0xB6, 0x0A, 0xE1, 0x63}\n\tpayload := append([]byte{0xAF, 0x01}, raw...)\n\treturn lalbase.RtmpMsg{\n\t\tHeader: lalbase.RtmpHeader{\n\t\t\tMsgTypeId:    lalbase.RtmpTypeIdAudio,\n\t\t\tMsgLen:       uint32(len(payload)),\n\t\t\tTimestampAbs: 40,\n\t\t},\n\t\tPayload: payload,\n\t}\n}\n\nfunc makeG711AMsg() lalbase.RtmpMsg {\n\tpayload := []byte{lalbase.RtmpSoundFormatG711A << 4, 0xD5, 0x5A, 0x11, 0x22}\n\treturn lalbase.RtmpMsg{\n\t\tHeader: lalbase.RtmpHeader{\n\t\t\tMsgTypeId:    lalbase.RtmpTypeIdAudio,\n\t\t\tMsgLen:       uint32(len(payload)),\n\t\t\tTimestampAbs: 20,\n\t\t},\n\t\tPayload: payload,\n\t}\n}\n\nfunc min(a, b int) int {\n\tif a < b {\n\t\treturn a\n\t}\n\treturn b\n}\n\nfunc TestFixturesAreValid(t *testing.T) {\n\tif _, _, err := avc.ParseSpsPpsFromSeqHeader(makeAvcSeqHeaderMsg().Payload); err != nil {\n\t\tt.Fatalf(\"invalid avc fixture: %v\", err)\n\t}\n\tif _, err := aac.NewAscContext(makeAacSeqHeaderMsg().Payload[2:]); err != nil {\n\t\tt.Fatalf(\"invalid aac fixture: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "gb28181/server.go",
    "content": "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\tudpTransport \"github.com/pion/transport/v3/udp\"\n\tconfig \"github.com/q191201771/lalmax/config\"\n\t\"github.com/q191201771/lalmax/gb28181/mediaserver\"\n\n\t\"github.com/ghettovoice/gosip\"\n\t\"github.com/ghettovoice/gosip/log\"\n\t\"github.com/ghettovoice/gosip/sip\"\n\t\"github.com/q191201771/lal/pkg/logic\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n\t\"golang.org/x/net/html/charset\"\n)\n\ntype IMediaOpObserver interface {\n\tOnStartMediaServer(netWork string, singlePort bool, deviceId string, channelId string) *mediaserver.GB28181MediaServer\n\tOnStopMediaServer(netWork string, singlePort bool, deviceId string, channelId string, StreamName string) error\n}\ntype GB28181Server struct {\n\tconf              config.GB28181Config\n\tRegisterValidity  time.Duration // 注册有效期，单位秒，默认 3600\n\tHeartbeatInterval time.Duration // 心跳间隔，单位秒，默认 60\n\tRemoveBanInterval time.Duration // 移除禁止设备间隔,默认600s\n\tkeepaliveInterval int\n\n\tlalServer logic.ILalServer\n\n\tudpAvailConnPool *AvailConnPool\n\ttcpAvailConnPool *AvailConnPool\n\n\tsipUdpSvr gosip.Server\n\tsipTcpSvr gosip.Server\n\n\tMediaServerMap sync.Map\n\tdisposeOnce    sync.Once\n}\n\nconst MaxRegisterCount = 3\n\nvar (\n\tlogger log.Logger\n\tsipsvr gosip.Server\n)\n\nfunc init() {\n\tlogger = log.NewDefaultLogrusLogger().WithPrefix(\"LalMaxServer\")\n}\n\nfunc NewGB28181Server(conf config.GB28181Config, lal logic.ILalServer) *GB28181Server {\n\tif conf.ListenAddr == \"\" {\n\t\tconf.ListenAddr = \"0.0.0.0\"\n\t}\n\tif conf.SipPort == 0 {\n\t\tconf.SipPort = 5060\n\t}\n\tif conf.KeepaliveInterval == 0 {\n\t\tconf.KeepaliveInterval = 60\n\t}\n\tif conf.Serial == \"\" {\n\t\tconf.Serial = \"34020000002000000001\"\n\t}\n\n\tif conf.Realm == \"\" {\n\t\tconf.Realm = \"3402000000\"\n\t}\n\n\tif conf.MediaConfig.MediaIp == \"\" {\n\t\tconf.MediaConfig.MediaIp = \"0.0.0.0\"\n\t}\n\n\tif conf.MediaConfig.ListenPort == 0 {\n\t\tconf.MediaConfig.ListenPort = 30000\n\t}\n\tif conf.MediaConfig.MultiPortMaxIncrement == 0 {\n\t\tconf.MediaConfig.MultiPortMaxIncrement = 3000\n\t}\n\tgb28181Server := &GB28181Server{\n\t\tconf:              conf,\n\t\tRegisterValidity:  3600 * time.Second,\n\t\tHeartbeatInterval: 60 * time.Second,\n\t\tRemoveBanInterval: 600 * time.Second,\n\t\tkeepaliveInterval: conf.KeepaliveInterval,\n\t\tlalServer:         lal,\n\t\tudpAvailConnPool:  NewAvailConnPool(conf.MediaConfig.ListenPort+1, conf.MediaConfig.ListenPort+conf.MediaConfig.MultiPortMaxIncrement),\n\t\ttcpAvailConnPool:  NewAvailConnPool(conf.MediaConfig.ListenPort+1, conf.MediaConfig.ListenPort+conf.MediaConfig.MultiPortMaxIncrement),\n\t}\n\tgb28181Server.tcpAvailConnPool.onListenWithPort = func(port uint16) (net.Listener, error) {\n\t\treturn net.Listen(\"tcp\", fmt.Sprintf(\":%d\", port))\n\t}\n\n\tgb28181Server.udpAvailConnPool.onListenWithPort = func(port uint16) (net.Listener, error) {\n\t\taddr, err := net.ResolveUDPAddr(\"udp\", fmt.Sprintf(\":%d\", port))\n\t\tif err != nil {\n\t\t\tnazalog.Error(\"gb28181 media server udp listen failed,err:\", err)\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn udpTransport.Listen(\"udp\", addr)\n\t}\n\treturn gb28181Server\n}\n\nfunc (s *GB28181Server) Start() {\n\ts.sipUdpSvr = s.newSipServer(\"udp\")\n\ts.sipTcpSvr = s.newSipServer(\"tcp\")\n\tgo s.startJob()\n}\nfunc (s *GB28181Server) newSipServer(network string) gosip.Server {\n\tsrvConf := gosip.ServerConfig{}\n\n\tif s.conf.SipIP != \"\" {\n\t\tsrvConf.Host = s.conf.SipIP\n\t}\n\tsipSvr := gosip.NewServer(srvConf, nil, nil, logger)\n\tsipSvr.OnRequest(sip.REGISTER, s.OnRegister)\n\tsipSvr.OnRequest(sip.MESSAGE, s.OnMessage)\n\tsipSvr.OnRequest(sip.NOTIFY, s.OnNotify)\n\tsipSvr.OnRequest(sip.BYE, s.OnBye)\n\n\taddr := s.conf.ListenAddr + \":\" + strconv.Itoa(int(s.conf.SipPort))\n\terr := sipSvr.Listen(network, addr)\n\tif err != nil {\n\t\tnazalog.Fatal(err)\n\t}\n\n\tnazalog.Info(\" start sip server listen. addr= \" + addr + \"  network:\" + network)\n\treturn sipSvr\n}\nfunc (s *GB28181Server) Dispose() {\n\ts.disposeOnce.Do(\n\t\tfunc() {\n\t\t\ts.MediaServerMap.Range(func(_, value any) bool {\n\t\t\t\tmediaServer := value.(*mediaserver.GB28181MediaServer)\n\t\t\t\tmediaServer.Dispose()\n\t\t\t\treturn true\n\t\t\t})\n\t\t\ts.sipTcpSvr.Shutdown()\n\t\t\ts.sipUdpSvr.Shutdown()\n\t\t})\n}\nfunc (s *GB28181Server) OnStartMediaServer(netWork string, singlePort bool, deviceId string, channelId string) *mediaserver.GB28181MediaServer {\n\tisTcpFlag := false\n\tif netWork == \"tcp\" {\n\t\tisTcpFlag = true\n\t}\n\tvar mediasvr *mediaserver.GB28181MediaServer\n\tif singlePort {\n\t\tif isTcpFlag {\n\t\t\tvalue, ok := s.MediaServerMap.Load(fmt.Sprintf(\"%s%d\", \"tcp\", s.conf.MediaConfig.ListenPort))\n\t\t\tif ok {\n\t\t\t\tmediasvr = value.(*mediaserver.GB28181MediaServer)\n\t\t\t}\n\t\t} else {\n\t\t\tvalue, ok := s.MediaServerMap.Load(fmt.Sprintf(\"%s%d\", \"udp\", s.conf.MediaConfig.ListenPort))\n\t\t\tif ok {\n\t\t\t\tmediasvr = value.(*mediaserver.GB28181MediaServer)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tvalue, ok := s.MediaServerMap.Load(fmt.Sprintf(\"%s%s\", deviceId, channelId))\n\t\tif ok {\n\t\t\tmediasvr = value.(*mediaserver.GB28181MediaServer)\n\t\t}\n\t}\n\tvar listener net.Listener\n\tvar err error\n\tvar port uint16\n\tif mediasvr == nil {\n\t\tif singlePort {\n\t\t\tif isTcpFlag {\n\t\t\t\tmediasvr = mediaserver.NewGB28181MediaServer(int(s.conf.MediaConfig.ListenPort), fmt.Sprintf(\"%s%d\", \"tcp\", s.conf.MediaConfig.ListenPort), s, s.lalServer)\n\t\t\t\tlistener, err = s.tcpAvailConnPool.ListenWithPort(s.conf.MediaConfig.ListenPort)\n\t\t\t\tif err != nil {\n\t\t\t\t\tnazalog.Errorf(\"gb28181 media server tcp Listen failed:%s\", err.Error())\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\ts.MediaServerMap.Store(fmt.Sprintf(\"%s%d\", \"tcp\", s.conf.MediaConfig.ListenPort), mediasvr)\n\t\t\t} else {\n\t\t\t\tmediasvr = mediaserver.NewGB28181MediaServer(int(s.conf.MediaConfig.ListenPort), fmt.Sprintf(\"%s%d\", \"udp\", s.conf.MediaConfig.ListenPort), s, s.lalServer)\n\t\t\t\tlistener, err = s.udpAvailConnPool.ListenWithPort(s.conf.MediaConfig.ListenPort)\n\t\t\t\tif err != nil {\n\t\t\t\t\tnazalog.Errorf(\"gb28181 media server udp Listen failed:%s\", err.Error())\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\ts.MediaServerMap.Store(fmt.Sprintf(\"%s%d\", \"udp\", s.conf.MediaConfig.ListenPort), mediasvr)\n\t\t\t}\n\t\t} else {\n\t\t\tmediaKey := \"\"\n\t\t\tif isTcpFlag {\n\t\t\t\tlistener, port, err = s.tcpAvailConnPool.Acquire()\n\t\t\t\tif err != nil {\n\t\t\t\t\tnazalog.Errorf(\"gb28181 media server tcp acquire failed:%s\", err.Error())\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tmediaKey = fmt.Sprintf(\"%s%d\", \"tcp\", port)\n\t\t\t} else {\n\t\t\t\tlistener, port, err = s.udpAvailConnPool.Acquire()\n\t\t\t\tif err != nil {\n\t\t\t\t\tnazalog.Errorf(\"gb28181 media server udp acquire failed:%s\", err.Error())\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tmediaKey = fmt.Sprintf(\"%s%d\", \"udp\", port)\n\t\t\t}\n\t\t\tmediasvr = mediaserver.NewGB28181MediaServer(int(port), mediaKey, s, s.lalServer)\n\t\t\ts.MediaServerMap.Store(fmt.Sprintf(\"%s%s\", deviceId, channelId), mediasvr)\n\t\t}\n\t\tgo mediasvr.Start(listener)\n\t}\n\treturn mediasvr\n}\nfunc (s *GB28181Server) OnStopMediaServer(netWork string, singlePort bool, deviceId string, channelId string, StreamName string) error {\n\tisTcpFlag := false\n\tif netWork == \"tcp\" {\n\t\tisTcpFlag = true\n\t}\n\tvar mediasvr *mediaserver.GB28181MediaServer\n\tif singlePort {\n\t\tif isTcpFlag {\n\t\t\tkey := fmt.Sprintf(\"%s%d\", \"tcp\", s.conf.MediaConfig.ListenPort)\n\t\t\tvalue, ok := s.MediaServerMap.Load(key)\n\t\t\tif ok {\n\t\t\t\tmediasvr = value.(*mediaserver.GB28181MediaServer)\n\t\t\t\ts.MediaServerMap.Delete(key)\n\t\t\t}\n\t\t} else {\n\t\t\tkey := fmt.Sprintf(\"%s%d\", \"udp\", s.conf.MediaConfig.ListenPort)\n\t\t\tvalue, ok := s.MediaServerMap.Load(key)\n\t\t\tif ok {\n\t\t\t\tmediasvr = value.(*mediaserver.GB28181MediaServer)\n\t\t\t\ts.MediaServerMap.Delete(key)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tkey := fmt.Sprintf(\"%s%s\", deviceId, channelId)\n\t\tvalue, ok := s.MediaServerMap.Load(key)\n\t\tif ok {\n\t\t\tmediasvr = value.(*mediaserver.GB28181MediaServer)\n\t\t\ts.MediaServerMap.Delete(key)\n\t\t}\n\t}\n\tif mediasvr != nil {\n\t\tif singlePort {\n\t\t\tmediasvr.CloseConn(StreamName)\n\t\t} else {\n\t\t\tmediasvr.Dispose()\n\t\t}\n\t}\n\treturn nil\n}\nfunc (s *GB28181Server) CheckSsrc(ssrc uint32) (*mediaserver.MediaInfo, bool) {\n\tvar isValidSsrc bool\n\tvar mediaInfo *mediaserver.MediaInfo\n\n\tDevices.Range(func(_, value any) bool {\n\t\td := value.(*Device)\n\t\td.channelMap.Range(func(key, value any) bool {\n\t\t\tch := value.(*Channel)\n\t\t\tif ch.MediaInfo.Ssrc == ssrc {\n\t\t\t\tisValidSsrc = true\n\t\t\t\tmediaInfo = &ch.MediaInfo\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\t\tif isValidSsrc {\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t})\n\n\tif isValidSsrc {\n\t\treturn mediaInfo, true\n\t}\n\n\treturn nil, false\n}\nfunc (s *GB28181Server) GetMediaInfoByKey(key string) (*mediaserver.MediaInfo, bool) {\n\tvar isValidMediaInfo bool\n\tvar mediaInfo *mediaserver.MediaInfo\n\n\tDevices.Range(func(_, value any) bool {\n\t\td := value.(*Device)\n\t\td.channelMap.Range(func(_, value any) bool {\n\t\t\tch := value.(*Channel)\n\t\t\tif ch.MediaInfo.MediaKey == key {\n\t\t\t\tisValidMediaInfo = true\n\t\t\t\tmediaInfo = &ch.MediaInfo\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\t\tif isValidMediaInfo {\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t})\n\n\tif isValidMediaInfo {\n\t\treturn mediaInfo, true\n\t}\n\n\treturn nil, false\n}\n\nfunc (s *GB28181Server) NotifyClose(streamName string) {\n\tvar ok bool\n\tDevices.Range(func(_, value any) bool {\n\t\td := value.(*Device)\n\t\td.channelMap.Range(func(key, value any) bool {\n\t\t\tch := value.(*Channel)\n\t\t\tif ch.MediaInfo.StreamName == streamName {\n\t\t\t\tif ch.MediaInfo.IsInvite {\n\t\t\t\t\tch.Bye(streamName)\n\t\t\t\t}\n\t\t\t\tch.MediaInfo.Clear()\n\t\t\t\tok = true\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\t\tif ok {\n\t\t\treturn false\n\t\t}\n\t\treturn true\n\t})\n}\n\nfunc (s *GB28181Server) OnRtpPacket(streamName string, mediaKey string) {\n}\n\nfunc (s *GB28181Server) startJob() {\n\tstatusTick := time.NewTicker(s.HeartbeatInterval / 2)\n\tbanTick := time.NewTicker(s.RemoveBanInterval)\n\tfor {\n\t\tselect {\n\t\tcase <-banTick.C:\n\t\t\tif s.conf.Username != \"\" || s.conf.Password != \"\" {\n\t\t\t\ts.removeBanDevice()\n\t\t\t}\n\t\tcase <-statusTick.C:\n\t\t\ts.statusCheck()\n\t\t}\n\t}\n}\n\nfunc (s *GB28181Server) removeBanDevice() {\n\tDeviceRegisterCount.Range(func(key, value interface{}) bool {\n\t\tif value.(int) > MaxRegisterCount {\n\t\t\tDeviceRegisterCount.Delete(key)\n\t\t}\n\t\treturn true\n\t})\n}\n\n// statusCheck\n// -  当设备超过 3 倍心跳时间未发送过心跳（通过 UpdateTime 判断）, 视为离线\n// - \t当设备超过注册有效期内为发送过消息，则从设备列表中删除\n// UpdateTime 在设备发送心跳之外的消息也会被更新，相对于 LastKeepaliveAt 更能体现出设备最会一次活跃的时间\nfunc (s *GB28181Server) statusCheck() {\n\tDevices.Range(func(key, value any) bool {\n\t\td := value.(*Device)\n\t\tif int(time.Since(d.LastKeepaliveAt).Seconds()) > s.keepaliveInterval*3 {\n\t\t\tDevices.Delete(key)\n\t\t\tnazalog.Warn(\"Device Keepalive timeout, id:\", d.ID, \" LastKeepaliveAt:\", d.LastKeepaliveAt, \" updateTime:\", d.UpdateTime)\n\t\t} else if time.Since(d.UpdateTime) > s.HeartbeatInterval*3 {\n\t\t\td.Status = DeviceOfflineStatus\n\t\t\td.channelMap.Range(func(key, value any) bool {\n\t\t\t\tch := value.(*Channel)\n\t\t\t\tch.Status = ChannelOffStatus\n\t\t\t\treturn true\n\t\t\t})\n\t\t\tnazalog.Warn(\"Device offline, id:\", d.ID, \" registerTime:\", d.RegisterTime, \" updateTime:\", d.UpdateTime)\n\t\t}\n\t\treturn true\n\t})\n}\nfunc (s *GB28181Server) getDeviceInfos() (deviceInfos *DeviceInfos) {\n\tdeviceInfos = &DeviceInfos{\n\t\tDeviceItems: make([]*DeviceItem, 0),\n\t}\n\tDevices.Range(func(key, value any) bool {\n\t\td := value.(*Device)\n\t\td.Status = DeviceOfflineStatus\n\t\tdeviceItem := &DeviceItem{\n\t\t\tDeviceId: d.ID,\n\t\t\tChannels: make([]*ChannelItem, 0),\n\t\t}\n\t\td.channelMap.Range(func(key, value any) bool {\n\t\t\tch := value.(*Channel)\n\t\t\tchannel := &ChannelItem{\n\t\t\t\tChannelId:    ch.ChannelId,\n\t\t\t\tName:         ch.Name,\n\t\t\t\tManufacturer: ch.Manufacturer,\n\t\t\t\tOwner:        ch.Owner,\n\t\t\t\tCivilCode:    ch.CivilCode,\n\t\t\t\tAddress:      ch.Address,\n\t\t\t\tStatus:       ch.Status,\n\t\t\t\tLongitude:    ch.Longitude,\n\t\t\t\tLatitude:     ch.Latitude,\n\t\t\t\tStreamName:   ch.StreamName,\n\t\t\t}\n\t\t\tdeviceItem.Channels = append(deviceItem.Channels, channel)\n\t\t\treturn true\n\t\t})\n\t\tdeviceInfos.DeviceItems = append(deviceInfos.DeviceItems, deviceItem)\n\t\treturn true\n\t})\n\treturn deviceInfos\n}\nfunc (s *GB28181Server) GetAllSyncChannels() {\n\tDevices.Range(func(key, value any) bool {\n\t\td := value.(*Device)\n\t\td.syncChannels()\n\t\treturn true\n\t})\n}\nfunc (s *GB28181Server) GetSyncChannels(deviceId string) bool {\n\tif v, ok := Devices.Load(deviceId); ok {\n\t\td := v.(*Device)\n\t\td.syncChannels()\n\t\treturn true\n\t} else {\n\t\treturn false\n\t}\n}\nfunc (s *GB28181Server) FindChannel(deviceId string, channelId string) (channel *Channel) {\n\tif v, ok := Devices.Load(deviceId); ok {\n\t\td := v.(*Device)\n\t\tif ch, ok := d.channelMap.Load(channelId); ok {\n\t\t\tchannel = ch.(*Channel)\n\t\t\treturn channel\n\t\t} else {\n\t\t\treturn nil\n\t\t}\n\t} else {\n\t\treturn nil\n\t}\n}\nfunc (s *GB28181Server) OnRegister(req sip.Request, tx sip.ServerTransaction) {\n\tfrom, ok := req.From()\n\tif !ok || from.Address == nil {\n\t\tnazalog.Error(\"OnRegister, no from\")\n\t\treturn\n\t}\n\tid := from.Address.User().String()\n\n\tnazalog.Info(\"OnRegister\", \" id:\", id, \" source:\", req.Source(), \" req:\", req.String())\n\n\tisUnregister := false\n\tif exps := req.GetHeaders(\"Expires\"); len(exps) > 0 {\n\t\texp := exps[0]\n\t\texpSec, err := strconv.ParseInt(exp.Value(), 10, 32)\n\t\tif err != nil {\n\t\t\tnazalog.Error(err)\n\t\t\treturn\n\t\t}\n\t\tif expSec == 0 {\n\t\t\tisUnregister = true\n\t\t}\n\t} else {\n\t\tnazalog.Error(\"has no expire header\")\n\t\treturn\n\t}\n\n\tnazalog.Info(\"OnRegister\", \" isUnregister:\", isUnregister, \" id:\", id, \" source:\", req.Source(), \" destination:\", req.Destination())\n\n\tif len(id) != 20 {\n\t\tnazalog.Error(\"invalid id: \", id)\n\t\treturn\n\t}\n\n\tpassAuth := false\n\t// 不需要密码情况\n\tif s.conf.Username == \"\" && s.conf.Password == \"\" {\n\t\tpassAuth = true\n\t} else {\n\t\t// 需要密码情况 设备第一次上报，返回401和加密算法\n\t\tif hdrs := req.GetHeaders(\"Authorization\"); len(hdrs) > 0 {\n\t\t\tauthenticateHeader := hdrs[0].(*sip.GenericHeader)\n\t\t\tauth := &Authorization{sip.AuthFromValue(authenticateHeader.Contents)}\n\n\t\t\t// 有些摄像头没有配置用户名的地方，用户名就是摄像头自己的国标id\n\t\t\tvar username string\n\t\t\tif auth.Username() == id {\n\t\t\t\tusername = id\n\t\t\t} else {\n\t\t\t\tusername = s.conf.Username\n\t\t\t}\n\n\t\t\tif dc, ok := DeviceRegisterCount.LoadOrStore(id, 1); ok && dc.(int) > MaxRegisterCount {\n\t\t\t\tresponse := sip.NewResponseFromRequest(\"\", req, http.StatusForbidden, \"Forbidden\", \"\")\n\t\t\t\ttx.Respond(response)\n\t\t\t\treturn\n\t\t\t} else {\n\t\t\t\t// 设备第二次上报，校验\n\t\t\t\t_nonce, loaded := DeviceNonce.Load(id)\n\t\t\t\tif loaded && auth.Verify(username, s.conf.Password, s.conf.Realm, _nonce.(string)) {\n\t\t\t\t\tpassAuth = true\n\t\t\t\t} else {\n\t\t\t\t\tDeviceRegisterCount.Store(id, dc.(int)+1)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif passAuth {\n\t\tvar d *Device\n\t\tif isUnregister {\n\t\t\ttmpd, ok := Devices.LoadAndDelete(id)\n\t\t\tif ok {\n\t\t\t\tnazalog.Info(\"Unregister Device, id:\", id)\n\t\t\t\td = tmpd.(*Device)\n\t\t\t} else {\n\t\t\t\treturn\n\t\t\t}\n\t\t} else {\n\t\t\tif v, ok := Devices.Load(id); ok {\n\t\t\t\td = v.(*Device)\n\t\t\t\ts.RecoverDevice(d, req)\n\t\t\t} else {\n\t\t\t\td = s.StoreDevice(id, req)\n\t\t\t}\n\t\t}\n\n\t\tDeviceNonce.Delete(id)\n\t\tDeviceRegisterCount.Delete(id)\n\t\tresp := sip.NewResponseFromRequest(\"\", req, http.StatusOK, \"OK\", \"\")\n\t\tto, _ := resp.To()\n\t\tresp.ReplaceHeaders(\"To\", []sip.Header{&sip.ToHeader{Address: to.Address, Params: sip.NewParams().Add(\"tag\", sip.String{Str: RandNumString(9)})}})\n\t\tresp.RemoveHeader(\"Allow\")\n\t\texpires := sip.Expires(3600)\n\t\tresp.AppendHeader(&expires)\n\t\tresp.AppendHeader(&sip.GenericHeader{\n\t\t\tHeaderName: \"Date\",\n\t\t\tContents:   time.Now().Format(TIME_LAYOUT),\n\t\t})\n\t\t_ = tx.Respond(resp)\n\n\t\tif !isUnregister {\n\t\t\t//订阅设备更新\n\t\t\tgo d.syncChannels()\n\t\t}\n\t} else {\n\t\tnazalog.Info(\"OnRegister unauthorized, id:\", id, \" source:\", req.Source(), \" destination:\", req.Destination())\n\t\tresponse := sip.NewResponseFromRequest(\"\", req, http.StatusUnauthorized, \"Unauthorized\", \"\")\n\t\t_nonce, _ := DeviceNonce.LoadOrStore(id, RandNumString(32))\n\t\tauth := fmt.Sprintf(\n\t\t\t`Digest realm=\"%s\",algorithm=%s,nonce=\"%s\"`,\n\t\t\ts.conf.Realm,\n\t\t\t\"MD5\",\n\t\t\t_nonce.(string),\n\t\t)\n\t\tresponse.AppendHeader(&sip.GenericHeader{\n\t\t\tHeaderName: \"WWW-Authenticate\",\n\t\t\tContents:   auth,\n\t\t})\n\t\t_ = tx.Respond(response)\n\t}\n}\n\nfunc (s *GB28181Server) OnMessage(req sip.Request, tx sip.ServerTransaction) {\n\tfrom, _ := req.From()\n\tid := from.Address.User().String()\n\tnazalog.Info(\"SIP<-OnMessage, id:\", id, \" source:\", req.Source(), \" req:\", req.String())\n\ttemp := &struct {\n\t\tXMLName      xml.Name\n\t\tCmdType      string\n\t\tSN           int // 请求序列号，一般用于对应 request 和 response\n\t\tDeviceID     string\n\t\tDeviceName   string\n\t\tManufacturer string\n\t\tModel        string\n\t\tChannel      string\n\t\tDeviceList   []ChannelInfo `xml:\"DeviceList>Item\"`\n\t\tSumNum       int           // 录像结果的总数 SumNum，录像结果会按照多条消息返回，可用于判断是否全部返回\n\t}{}\n\tdecoder := xml.NewDecoder(bytes.NewReader([]byte(req.Body())))\n\tdecoder.CharsetReader = charset.NewReaderLabel\n\terr := decoder.Decode(temp)\n\tif err != nil {\n\t\terr = DecodeGbk(temp, []byte(req.Body()))\n\t\tif err != nil {\n\t\t\tnazalog.Error(\"decode catelog err:\", err)\n\t\t}\n\t}\n\tif v, ok := Devices.Load(id); ok {\n\t\td := v.(*Device)\n\t\tswitch d.Status {\n\t\tcase DeviceOfflineStatus, DeviceRecoverStatus:\n\t\t\ts.RecoverDevice(d, req)\n\t\t\t//go d.syncChannels(s.conf)\n\t\tcase DeviceRegisterStatus:\n\t\t\td.Status = DeviceOnlineStatus\n\t\t}\n\t\td.UpdateTime = time.Now()\n\n\t\tvar body string\n\t\tswitch temp.CmdType {\n\t\tcase \"Keepalive\":\n\t\t\td.LastKeepaliveAt = time.Now()\n\t\t\t//callID !=\"\" 说明是订阅的事件类型信息\n\t\t\t//if d.lastSyncTime.IsZero() {\n\t\t\t//\tgo d.syncChannels(s.conf)\n\t\t\t//}\n\t\tcase \"Catalog\":\n\t\t\td.UpdateChannels(temp.DeviceList...)\n\t\tcase \"DeviceInfo\":\n\t\t\t// 主设备信息\n\t\t\td.Name = temp.DeviceName\n\t\t\td.Manufacturer = temp.Manufacturer\n\t\t\td.Model = temp.Model\n\t\tcase \"Alarm\":\n\t\t\td.Status = DeviceAlarmedStatus\n\t\t\tbody = BuildAlarmResponseXML(d.ID)\n\t\tdefault:\n\t\t\tnazalog.Warn(\"Not supported CmdType, CmdType:\", temp.CmdType, \" body:\", req.Body())\n\t\t\tresponse := sip.NewResponseFromRequest(\"\", req, http.StatusBadRequest, \"\", \"\")\n\t\t\ttx.Respond(response)\n\t\t\treturn\n\t\t}\n\n\t\ttx.Respond(sip.NewResponseFromRequest(\"\", req, http.StatusOK, \"OK\", body))\n\t} else {\n\t\tif s.conf.QuickLogin {\n\t\t\tswitch temp.CmdType {\n\t\t\tcase \"Keepalive\":\n\t\t\t\td := s.StoreDevice(id, req)\n\t\t\t\td.LastKeepaliveAt = time.Now()\n\t\t\t\ttx.Respond(sip.NewResponseFromRequest(\"\", req, http.StatusOK, \"OK\", \"\"))\n\t\t\t\tgo d.syncChannels()\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tnazalog.Warn(\"Unauthorized message, device not found, id:\", id)\n\t\ttx.Respond(sip.NewResponseFromRequest(\"\", req, http.StatusBadRequest, \"device not found\", \"\"))\n\t}\n}\n\nfunc (s *GB28181Server) OnNotify(req sip.Request, tx sip.ServerTransaction) {\n\tfrom, _ := req.From()\n\tid := from.Address.User().String()\n\tif v, ok := Devices.Load(id); ok {\n\t\td := v.(*Device)\n\t\td.UpdateTime = time.Now()\n\t\ttemp := &struct {\n\t\t\tXMLName    xml.Name\n\t\t\tCmdType    string\n\t\t\tDeviceID   string\n\t\t\tTime       string           //位置订阅-GPS时间\n\t\t\tLongitude  string           //位置订阅-经度\n\t\t\tLatitude   string           //位置订阅-维度\n\t\t\tDeviceList []*notifyMessage `xml:\"DeviceList>Item\"` //目录订阅\n\t\t}{}\n\t\tdecoder := xml.NewDecoder(bytes.NewReader([]byte(req.Body())))\n\t\tdecoder.CharsetReader = charset.NewReaderLabel\n\t\terr := decoder.Decode(temp)\n\t\tif err != nil {\n\t\t\terr = DecodeGbk(temp, []byte(req.Body()))\n\t\t\tif err != nil {\n\t\t\t\tnazalog.Error(\"decode catelog failed, err:\", err)\n\t\t\t}\n\t\t}\n\t\tvar body string\n\t\tswitch temp.CmdType {\n\t\tcase \"Catalog\":\n\t\t\t//目录状态\n\t\t\td.UpdateChannelStatus(temp.DeviceList, s.conf)\n\t\tcase \"MobilePosition\":\n\t\t\t//更新channel的坐标\n\t\t\td.UpdateChannelPosition(temp.DeviceID, temp.Time, temp.Longitude, temp.Latitude)\n\t\tdefault:\n\t\t\tnazalog.Warn(\"Not supported CmdType, cmdType:\", temp.CmdType, \" body:\", req.Body())\n\t\t\tresponse := sip.NewResponseFromRequest(\"\", req, http.StatusBadRequest, \"\", \"\")\n\t\t\ttx.Respond(response)\n\t\t\treturn\n\t\t}\n\n\t\ttx.Respond(sip.NewResponseFromRequest(\"\", req, http.StatusOK, \"OK\", body))\n\t} else {\n\t\ttx.Respond(sip.NewResponseFromRequest(\"\", req, http.StatusBadRequest, \"device not found\", \"\"))\n\t}\n}\n\nfunc (s *GB28181Server) OnBye(req sip.Request, tx sip.ServerTransaction) {\n\tcallIdStr := \"\"\n\tif callId, ok := req.CallID(); ok {\n\t\tcallIdStr = callId.Value()\n\t}\n\tfrom, _ := req.From()\n\tdevId := from.Address.User().String()\n\tif _d, ok := Devices.Load(devId); ok {\n\t\td := _d.(*Device)\n\t\td.channelMap.Range(func(key, value any) bool {\n\t\t\tch := value.(*Channel)\n\t\t\tif ch.GetCallId() == callIdStr {\n\t\t\t\tch.byeClear()\n\t\t\t\treturn false\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\t}\n\ttx.Respond(sip.NewResponseFromRequest(\"\", req, http.StatusOK, \"OK\", \"\"))\n}\n\nfunc (s *GB28181Server) StoreDevice(id string, req sip.Request) (d *Device) {\n\tfrom, _ := req.From()\n\tdeviceAddr := sip.Address{\n\t\tDisplayName: from.DisplayName,\n\t\tUri:         from.Address,\n\t}\n\tdeviceIp := req.Source()\n\tif _d, ok := Devices.Load(id); ok {\n\t\td = _d.(*Device)\n\t\td.UpdateTime = time.Now()\n\t\td.NetAddr = deviceIp\n\t\td.addr = deviceAddr\n\t\td.network = strings.ToLower(req.Transport())\n\t\tif d.network == \"udp\" {\n\t\t\td.sipSvr = s.sipUdpSvr\n\t\t} else {\n\t\t\td.sipSvr = s.sipTcpSvr\n\t\t}\n\t\tnazalog.Info(\"UpdateDevice, netaddr:\", d.NetAddr)\n\t} else {\n\t\tservIp := req.Recipient().Host()\n\n\t\tsipIp := s.conf.SipIP\n\t\tmediaIp := s.conf.MediaConfig.MediaIp\n\t\td = &Device{\n\t\t\tID:           id,\n\t\t\tRegisterTime: time.Now(),\n\t\t\tUpdateTime:   time.Now(),\n\t\t\tStatus:       DeviceRegisterStatus,\n\t\t\taddr:         deviceAddr,\n\t\t\tsipIP:        sipIp,\n\t\t\tmediaIP:      mediaIp,\n\t\t\tNetAddr:      deviceIp,\n\t\t\tconf:         s.conf,\n\t\t\tnetwork:      strings.ToLower(req.Transport()),\n\t\t}\n\t\tif d.network == \"udp\" {\n\t\t\td.sipSvr = s.sipUdpSvr\n\t\t} else {\n\t\t\td.sipSvr = s.sipTcpSvr\n\t\t}\n\t\td.WithMediaServer(s)\n\t\tnazalog.Info(\"StoreDevice, deviceIp:\", deviceIp, \" serverIp:\", servIp, \" mediaIp:\", mediaIp, \" sipIP:\", sipIp)\n\t\tDevices.Store(id, d)\n\t}\n\n\treturn d\n}\n\nfunc (s *GB28181Server) RecoverDevice(d *Device, req sip.Request) {\n\tfrom, _ := req.From()\n\td.addr = sip.Address{\n\t\tDisplayName: from.DisplayName,\n\t\tUri:         from.Address,\n\t}\n\tdeviceIp := req.Source()\n\tservIp := req.Recipient().Host()\n\tsipIp := s.conf.SipIP\n\tmediaIp := sipIp\n\td.Status = DeviceRegisterStatus\n\td.sipIP = sipIp\n\td.mediaIP = mediaIp\n\td.NetAddr = deviceIp\n\td.network = strings.ToLower(req.Transport())\n\tif d.network == \"udp\" {\n\t\td.sipSvr = s.sipUdpSvr\n\t} else {\n\t\td.sipSvr = s.sipTcpSvr\n\t}\n\td.UpdateTime = time.Now()\n\n\tnazalog.Info(\"RecoverDevice, deviceIp:\", deviceIp, \" serverIp:\", servIp, \" mediaIp:\", mediaIp, \" sipIP:\", sipIp)\n}\n\ntype notifyMessage struct {\n\tChannelInfo\n\n\t//状态改变事件 ON:上线,OFF:离线,VLOST:视频丢失,DEFECT:故障,ADD:增加,DEL:删除,UPDATE:更新(必选)\n\tEvent string\n}\n"
  },
  {
    "path": "gb28181/t_http_api.go",
    "content": "package gb28181\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype DeviceInfos struct {\n\tDeviceItems []*DeviceItem `json:\"device_items\"`\n}\ntype DeviceItem struct {\n\tDeviceId string         `json:\"device_id\"` // 设备ID\n\tChannels []*ChannelItem `json:\"channels\"`\n}\ntype ChannelItem struct {\n\tChannelId    string        `json:\"channel_id\"`   // channel id\n\tName         string        `json:\"name\"`         // 设备名称\n\tManufacturer string        `json:\"manufacturer\"` // 制造厂商\n\tOwner        string        `json:\"owner\"`        // 设备归属\n\tCivilCode    string        `json:\"civilCode\"`    // 行政区划编码\n\tAddress      string        `json:\"address\"`      // 地址\n\tStatus       ChannelStatus `json:\"status\"`       // 状态  on 在线 off离线\n\tLongitude    string        `json:\"longitude\"`    // 经度\n\tLatitude     string        `json:\"latitude\"`     // 纬度\n\tStreamName   string        `json:\"-\"`\n}\ntype PlayInfo struct {\n\tNetWork      string `json:\"network\" form:\"network\" url:\"network\"`                      // 媒体传输类型,tcp/udp,默认udp\n\tDeviceId     string `json:\"device_id\" form:\"device_id\" url:\"device_id\"`                // 设备 Id\n\tChannelId    string `json:\"channel_id\" form:\"channel_id\" url:\"channel_id\"`             // channel id\n\tStreamName   string `json:\"stream_name\" form:\"stream_name\" url:\"stream_name\"`          // 对应的流名\n\tSinglePort   bool   `json:\"single_port\" form:\"single_port\" url:\"single_port\"`          // 是否单端口\n\tDumpFileName string `json:\"dump_file_name\" form:\"dump_file_name\" url:\"dump_file_name\"` // dump文件路径\n}\ntype ReqPlay struct {\n\tPlayInfo\n}\ntype RespPlay struct {\n\tStreamName string `json:\"stream_name\" form:\"stream_name\" url:\"stream_name\"`\n}\ntype ReqStop struct {\n\tPlayInfo\n}\n\ntype PtzDirection struct {\n\tDeviceId  string `json:\"device_id\" form:\"device_id\" url:\"device_id\"`    // 设备 Id\n\tChannelId string `json:\"channel_id\" form:\"channel_id\" url:\"channel_id\"` // channel id\n\tUp        bool   `json:\"up\" form:\"up\" url:\"up\"`\n\tDown      bool   `json:\"down\" form:\"down\" url:\"down\"`\n\tLeft      bool   `json:\"left\" form:\"left\" url:\"left\"`\n\tRight     bool   `json:\"right\" form:\"right\" url:\"right\"`\n\tSpeed     byte   `json:\"speed\" form:\"speed\" url:\"speed\"` //0-8\n}\ntype PtzZoom struct {\n\tDeviceId  string `json:\"device_id\" form:\"device_id\" url:\"device_id\"`    // 设备 Id\n\tChannelId string `json:\"channel_id\" form:\"channel_id\" url:\"channel_id\"` // channel id\n\tZoomOut   bool   `json:\"zoom_out\" form:\"zoom_out\" url:\"zoom_out\"`\n\tZoomIn    bool   `json:\"zoom_in\" form:\"zoom_in\" url:\"zoom_in\"`\n\tSpeed     byte   `json:\"speed\" form:\"speed\" url:\"speed\"` //0-8\n}\ntype PtzFi struct {\n\tDeviceId  string `json:\"device_id\" form:\"device_id\" url:\"device_id\"`    // 设备 Id\n\tChannelId string `json:\"channel_id\" form:\"channel_id\" url:\"channel_id\"` // channel id\n\tIrisIn    bool   `json:\"iris_in\" form:\"iris_in\" url:\"iris_in\"`\n\tIrisOut   bool   `json:\"iris_out\" form:\"iris_out\" url:\"iris_out\"`\n\tFocusNear bool   `json:\"focus_near\" form:\"focus_near\" url:\"focus_near\"`\n\tFocusFar  bool   `json:\"focus_far\" form:\"focus_far\" url:\"focus_far\"`\n\tSpeed     byte   `json:\"speed\" form:\"speed\" url:\"speed\"` //0-8\n}\ntype PresetCmd byte\n\nconst (\n\tPresetEditPoint PresetCmd = iota\n\tPresetDelPoint\n\tPresetCallPoint\n)\n\ntype PtzPreset struct {\n\tDeviceId  string    `json:\"device_id\" form:\"device_id\" url:\"device_id\"`    // 设备 Id\n\tChannelId string    `json:\"channel_id\" form:\"channel_id\" url:\"channel_id\"` // channel id\n\tCmd       PresetCmd `json:\"cmd\" form:\"cmd\" url:\"cmd\"`\n\tPoint     byte      `json:\"point\" form:\"point\" url:\"point\"`\n}\ntype PtzStop struct {\n\tDeviceId  string `json:\"device_id\" form:\"device_id\" url:\"device_id\"`    // 设备 Id\n\tChannelId string `json:\"channel_id\" form:\"channel_id\" url:\"channel_id\"` // channel id\n}\ntype ReqUpdateNotify struct {\n\tDeviceId string `json:\"device_id\" form:\"device_id\" url:\"device_id\"` //设备 Id\n}\n\nfunc ResponseErrorWithMsg(c *gin.Context, code ResCode, msg interface{}) {\n\tc.JSON(http.StatusOK, &ResponseData{\n\t\tCode: code,\n\t\tMsg:  msg,\n\t\tData: nil,\n\t})\n}\n\nfunc ResponseSuccess(c *gin.Context, data interface{}) {\n\tc.JSON(http.StatusOK, &ResponseData{\n\t\tCode: CodeSuccess,\n\t\tMsg:  CodeSuccess.Msg(),\n\t\tData: data,\n\t})\n}\n\ntype ResCode int64\n\nconst (\n\tCodeSuccess ResCode = 1000 + iota\n\tCodeInvalidParam\n\tCodeServerBusy\n\tCodeDeviceNotRegister\n\tCodeDeviceStopError\n)\n\nvar codeMsgMap = map[ResCode]string{\n\tCodeSuccess:           \"success\",\n\tCodeInvalidParam:      \"请求参数错误\",\n\tCodeServerBusy:        \"服务繁忙\",\n\tCodeDeviceNotRegister: \"设备暂时未注册\",\n\tCodeDeviceStopError:   \"设备停止播放错误\",\n}\n\nconst (\n\tSpeedParamError = \"speed 范围(0,8]\"\n\tPointParamError = \"point 范围(0,50]\"\n)\n\nfunc (c ResCode) Msg() string {\n\tmsg, ok := codeMsgMap[c]\n\tif !ok {\n\t\tmsg = codeMsgMap[CodeServerBusy]\n\t}\n\treturn msg\n}\n\ntype ResponseData struct {\n\tCode ResCode     `json:\"code\"`\n\tMsg  interface{} `json:\"msg\"`\n\tData interface{} `json:\"data,omitempty\"`\n}\n"
  },
  {
    "path": "gb28181/util.go",
    "content": "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/simplifiedchinese\"\n\t\"golang.org/x/text/transform\"\n\t\"io/ioutil\"\n\t\"math/rand\"\n\t\"time\"\n)\n\nfunc RandNumString(n int) string {\n\tnumbers := \"0123456789\"\n\treturn randStringBySoure(numbers, n)\n}\n\nfunc RandString(n int) string {\n\tletterBytes := \"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n\treturn randStringBySoure(letterBytes, n)\n}\n\n// https://github.com/kpbird/golang_random_string\nfunc randStringBySoure(src string, n int) string {\n\trandomness := make([]byte, n)\n\n\trand.Seed(time.Now().UnixNano())\n\t_, err := rand.Read(randomness)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tl := len(src)\n\n\t// fill output\n\toutput := make([]byte, n)\n\tfor pos := range output {\n\t\trandom := randomness[pos]\n\t\trandomPos := random % uint8(l)\n\t\toutput[pos] = src[randomPos]\n\t}\n\n\treturn string(output)\n}\n\nfunc DecodeGbk(v interface{}, body []byte) error {\n\tbodyBytes, err := GbkToUtf8(body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdecoder := xml.NewDecoder(bytes.NewReader(bodyBytes))\n\tdecoder.CharsetReader = charset.NewReaderLabel\n\terr = decoder.Decode(v)\n\treturn err\n}\n\nfunc GbkToUtf8(s []byte) ([]byte, error) {\n\treader := transform.NewReader(bytes.NewReader(s), simplifiedchinese.GBK.NewDecoder())\n\td, e := ioutil.ReadAll(reader)\n\tif e != nil {\n\t\treturn s, e\n\t}\n\treturn d, nil\n}\n"
  },
  {
    "path": "gb28181/xml.go",
    "content": "package gb28181\n\nimport (\n\t\"encoding/xml\"\n\t\"fmt\"\n)\n\nvar (\n\t// CatalogXML 获取设备列表xml样式\n\tCatalogXML = `<?xml version=\"1.0\"?><Query>\n<CmdType>Catalog</CmdType>\n<SN>%d</SN>\n<DeviceID>%s</DeviceID>\n</Query>\n`\n\t// RecordInfoXML 获取录像文件列表xml样式\n\tRecordInfoXML = `<?xml version=\"1.0\"?>\n<Query>\n<CmdType>RecordInfo</CmdType>\n<SN>%d</SN>\n<DeviceID>%s</DeviceID>\n<StartTime>%s</StartTime>\n<EndTime>%s</EndTime>\n<Secrecy>0</Secrecy>\n<Type>all</Type>\n</Query>\n`\n\t// DeviceInfoXML 查询设备详情xml样式\n\tDeviceInfoXML = `<?xml version=\"1.0\"?>\n<Query>\n<CmdType>DeviceInfo</CmdType>\n<SN>%d</SN>\n<DeviceID>%s</DeviceID>\n</Query>\n`\n\t// DevicePositionXML 订阅设备位置\n\tDevicePositionXML = `<?xml version=\"1.0\"?>\n<Query>\n<CmdType>MobilePosition</CmdType>\n<SN>%d</SN>\n<DeviceID>%s</DeviceID>\n<Interval>%d</Interval>\n</Query>`\n)\n\nfunc BuildCatalogXML(sn int, id string) string {\n\treturn fmt.Sprintf(CatalogXML, sn, id)\n}\n\n// AlarmResponseXML alarm response xml样式\nvar (\n\tAlarmResponseXML = `<?xml version=\"1.0\"?>\n<Response>\n<CmdType>Alarm</CmdType>\n<SN>17430</SN>\n<DeviceID>%s</DeviceID>\n</Response>\n`\n)\n\n// BuildRecordInfoXML 获取录像文件列表指令\nfunc BuildAlarmResponseXML(id string) string {\n\treturn fmt.Sprintf(AlarmResponseXML, id)\n}\n\nfunc BuildDeviceInfoXML(sn int, id string) string {\n\treturn fmt.Sprintf(DeviceInfoXML, sn, id)\n}\n\nfunc XmlEncode(v interface{}) (string, error) {\n\txmlData, err := xml.MarshalIndent(v, \"\", \" \")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\txml := string(xmlData)\n\txml = `<?xml version=\"1.0\" ?>` + \"\\n\" + xml + \"\\n\"\n\treturn xml, err\n}\n"
  },
  {
    "path": "go.mod",
    "content": "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 v1.3.0\n\tgithub.com/bluenviron/gortsplib/v4 v4.8.0\n\tgithub.com/bluenviron/mediacommon v1.9.2\n\tgithub.com/datarhei/gosrt v0.5.4\n\tgithub.com/ghettovoice/gosip v0.0.0-20230802091127-d58873a3fe44\n\tgithub.com/gin-gonic/gin v1.9.1\n\tgithub.com/gofrs/uuid v4.4.0+incompatible\n\tgithub.com/pion/ice/v2 v2.3.13\n\tgithub.com/pion/interceptor v0.1.40\n\tgithub.com/pion/rtp v1.8.20\n\tgithub.com/pion/transport/v3 v3.0.7\n\tgithub.com/pion/webrtc/v3 v3.2.28\n\tgithub.com/q191201771/lal v0.37.4\n\tgithub.com/q191201771/naza v0.30.48\n\tgithub.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b\n\tgithub.com/smallnest/chanx v1.2.0\n\tgithub.com/yapingcat/gomedia v0.0.0-20240316172424-76660eca7389\n\tgolang.org/x/net v0.35.0\n\tgolang.org/x/text v0.22.0\n)\n\nrequire (\n\tgithub.com/asticode/go-astikit v0.30.0 // indirect\n\tgithub.com/asticode/go-astits v1.13.0 // indirect\n\tgithub.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c // indirect\n\tgithub.com/bytedance/sonic v1.9.1 // indirect\n\tgithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca // indirect\n\tgithub.com/fsnotify/fsnotify v1.8.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.2 // indirect\n\tgithub.com/gin-contrib/sse v0.1.0 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.14.0 // indirect\n\tgithub.com/gobwas/httphead v0.1.0 // indirect\n\tgithub.com/gobwas/pool v0.2.1 // indirect\n\tgithub.com/gobwas/ws v1.1.0-rc.1 // indirect\n\tgithub.com/goccy/go-json v0.10.2 // indirect\n\tgithub.com/google/go-cmp v0.6.0 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.2.6 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/leodido/go-urn v1.2.4 // indirect\n\tgithub.com/mattn/go-colorable v0.1.13 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.0.8 // indirect\n\tgithub.com/pion/datachannel v1.5.10 // indirect\n\tgithub.com/pion/dtls/v2 v2.2.11 // indirect\n\tgithub.com/pion/logging v0.2.4 // indirect\n\tgithub.com/pion/mdns v0.0.12 // indirect\n\tgithub.com/pion/randutil v0.1.0 // indirect\n\tgithub.com/pion/rtcp v1.2.15 // indirect\n\tgithub.com/pion/sctp v1.8.39 // indirect\n\tgithub.com/pion/sdp/v3 v3.0.14 // indirect\n\tgithub.com/pion/srtp/v2 v2.0.18 // indirect\n\tgithub.com/pion/stun v0.6.1 // indirect\n\tgithub.com/pion/transport/v2 v2.2.5 // indirect\n\tgithub.com/pion/turn/v2 v2.1.6 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/rogpeppe/go-internal v1.11.0 // indirect\n\tgithub.com/sirupsen/logrus v1.9.3 // indirect\n\tgithub.com/stretchr/testify v1.10.0 // indirect\n\tgithub.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/ugorji/go/codec v1.2.11 // indirect\n\tgithub.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect\n\tgo.uber.org/goleak v1.3.0 // indirect\n\tgolang.org/x/arch v0.3.0 // indirect\n\tgolang.org/x/crypto v0.33.0 // indirect\n\tgolang.org/x/sys v0.30.0 // indirect\n\tgolang.org/x/term v0.29.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.1 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/abema/go-mp4 v1.2.0 h1:gi4X8xg/m179N/J15Fn5ugywN9vtI6PLk6iLldHGLAk=\ngithub.com/abema/go-mp4 v1.2.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=\ngithub.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=\ngithub.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=\ngithub.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c=\ngithub.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=\ngithub.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYhJeJ2aZxADI2tGADS15AzIF8MQ8XAhT4=\ngithub.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI=\ngithub.com/bluenviron/gohlslib v1.3.0 h1:I9t1Nba6VJKg5rLoXSzQFPkZZYBUwBqCU2Divp0oU2I=\ngithub.com/bluenviron/gohlslib v1.3.0/go.mod h1:wD8ysO6HB90d17sxoIQXGHINo2KYj/mZirMnPtKLJZQ=\ngithub.com/bluenviron/gortsplib/v4 v4.8.0 h1:nvFp6rHALcSep3G9uBFI0uogS9stVZLNq/92TzGZdQg=\ngithub.com/bluenviron/gortsplib/v4 v4.8.0/go.mod h1:+d+veuyvhvikUNp0GRQkk6fEbd/DtcXNidMRm7FQRaA=\ngithub.com/bluenviron/mediacommon v1.9.2 h1:EHcvoC5YMXRcFE010bTNf07ZiSlB/e/AdZyG7GsEYN0=\ngithub.com/bluenviron/mediacommon v1.9.2/go.mod h1:lt8V+wMyPw8C69HAqDWV5tsAwzN9u2Z+ca8B6C//+n0=\ngithub.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=\ngithub.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=\ngithub.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=\ngithub.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=\ngithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=\ngithub.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=\ngithub.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/datarhei/gosrt v0.5.4 h1:dE3mmSB+n1GeviGM8xQAW3+UD3mKeFmd84iefDul5Vs=\ngithub.com/datarhei/gosrt v0.5.4/go.mod h1:MiUCwCG+LzFMzLM/kTA+3wiTtlnkVvGbW/F0XzyhtG8=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca h1:cTTdXpkQ1aVbOOmHwdwtYuwUZcQtcMrleD1UXLWhAq8=\ngithub.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca/go.mod h1:W+3LQaEkN8qAwwcw0KC546sUEnX86GIT8CcMLZC4mG0=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=\ngithub.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=\ngithub.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=\ngithub.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=\ngithub.com/ghettovoice/gosip v0.0.0-20230802091127-d58873a3fe44 h1:m4/46V6uAJ95CLimMRHJjiH5psW1JuL+iLeUBzF2r70=\ngithub.com/ghettovoice/gosip v0.0.0-20230802091127-d58873a3fe44/go.mod h1:rlD1yLOErWYohWTryG/2bTTpmzB79p52ntLA/uIFXeI=\ngithub.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=\ngithub.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=\ngithub.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=\ngithub.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=\ngithub.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=\ngithub.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=\ngithub.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=\ngithub.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=\ngithub.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.1.0-rc.1 h1:VK3aeRXMI8osaS6YCDKNZhU6RKtcP3B2wzqxOogNDz8=\ngithub.com/gobwas/ws v1.1.0-rc.1/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=\ngithub.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=\ngithub.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=\ngithub.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA=\ngithub.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=\ngithub.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=\ngithub.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=\ngithub.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=\ngithub.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=\ngithub.com/nxadm/tail v1.4.5/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=\ngithub.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=\ngithub.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=\ngithub.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=\ngithub.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=\ngithub.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=\ngithub.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=\ngithub.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=\ngithub.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=\ngithub.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=\ngithub.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw=\ngithub.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0=\ngithub.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=\ngithub.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=\ngithub.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=\ngithub.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=\ngithub.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=\ngithub.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=\ngithub.com/pion/dtls/v2 v2.2.11 h1:9U/dpCYl1ySttROPWJgqWKEylUdT0fXp/xst6JwY5Ks=\ngithub.com/pion/dtls/v2 v2.2.11/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=\ngithub.com/pion/ice/v2 v2.3.13 h1:xOxP+4V9nSDlUaGFRf/LvAuGHDXRcjIdsbbXPK/w7c8=\ngithub.com/pion/ice/v2 v2.3.13/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw=\ngithub.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y=\ngithub.com/pion/interceptor v0.1.40 h1:e0BjnPcGpr2CFQgKhrQisBU7V3GXK6wrfYrGYaU6Jq4=\ngithub.com/pion/interceptor v0.1.40/go.mod h1:Z6kqH7M/FYirg3frjGJ21VLSRJGBXB/KqaTIrdqnOic=\ngithub.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=\ngithub.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=\ngithub.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=\ngithub.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8=\ngithub.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk=\ngithub.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=\ngithub.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=\ngithub.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=\ngithub.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4=\ngithub.com/pion/rtcp v1.2.15 h1:LZQi2JbdipLOj4eBjK4wlVoQWfrZbh3Q6eHtWtJBZBo=\ngithub.com/pion/rtcp v1.2.15/go.mod h1:jlGuAjHMEXwMUHK78RgX0UmEJFV4zUKOFHR7OP+D3D0=\ngithub.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=\ngithub.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=\ngithub.com/pion/rtp v1.8.20 h1:8zcyqohadZE8FCBeGdyEvHiclPIezcwRQH9zfapFyYI=\ngithub.com/pion/rtp v1.8.20/go.mod h1:bAu2UFKScgzyFqvUKmbvzSdPr+NGbZtv6UB2hesqXBk=\ngithub.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=\ngithub.com/pion/sctp v1.8.12/go.mod h1:cMLT45jqw3+jiJCrtHVwfQLnfR0MGZ4rgOJwUOIqLkI=\ngithub.com/pion/sctp v1.8.39 h1:PJma40vRHa3UTO3C4MyeJDQ+KIobVYRZQZ0Nt7SjQnE=\ngithub.com/pion/sctp v1.8.39/go.mod h1:cNiLdchXra8fHQwmIoqw0MbLLMs+f7uQ+dGMG2gWebE=\ngithub.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=\ngithub.com/pion/sdp/v3 v3.0.14 h1:1h7gBr9FhOWH5GjWWY5lcw/U85MtdcibTyt/o6RxRUI=\ngithub.com/pion/sdp/v3 v3.0.14/go.mod h1:88GMahN5xnScv1hIMTqLdu/cOcUkj6a9ytbncwMCq2E=\ngithub.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo=\ngithub.com/pion/srtp/v2 v2.0.18/go.mod h1:0KJQjA99A6/a0DOVTu1PhDSw0CXF2jTkqOoMg3ODqdA=\ngithub.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4=\ngithub.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=\ngithub.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=\ngithub.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=\ngithub.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=\ngithub.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=\ngithub.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=\ngithub.com/pion/transport/v2 v2.2.5 h1:iyi25i/21gQck4hfRhomF6SktmUQjRsRW4WJdhfc3Kc=\ngithub.com/pion/transport/v2 v2.2.5/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=\ngithub.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=\ngithub.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=\ngithub.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=\ngithub.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=\ngithub.com/pion/turn/v2 v2.1.6 h1:Xr2niVsiPTB0FPtt+yAWKFUkU1eotQbGgpTIld4x1Gc=\ngithub.com/pion/turn/v2 v2.1.6/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=\ngithub.com/pion/webrtc/v3 v3.2.28 h1:ienStxZ6HcjtH2UlmnFpMM0loENiYjaX437uIUpQSKo=\ngithub.com/pion/webrtc/v3 v3.2.28/go.mod h1:PNRCEuQlibrmuBhOTnol9j6KkIbUG11aHLEfNpUYey0=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/q191201771/lal v0.37.4 h1:yq1PuuHfyzOjGLsZOZIorI+FcmgKSJKEitG9rYAMpYk=\ngithub.com/q191201771/lal v0.37.4/go.mod h1:DNDsCng/5dZOira1v6Z/yR45l5K4+EmPC4BqciZGdgQ=\ngithub.com/q191201771/naza v0.30.48 h1:lbYUaa7A15kJKYwOiU4AbFS1Zo8oQwppl2tLEbJTqnw=\ngithub.com/q191201771/naza v0.30.48/go.mod h1:n+dpJjQSh90PxBwxBNuifOwQttywvSIN5TkWSSYCeBk=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=\ngithub.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=\ngithub.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM=\ngithub.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=\ngithub.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=\ngithub.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/smallnest/chanx v1.2.0 h1:RLyldZBbQZ4O0dSvdkTMHo4+mDw20Bc1jXXTHf+ymZo=\ngithub.com/smallnest/chanx v1.2.0/go.mod h1:+4nWMF0+CqEcU74SnX2NxaGqZ8zX4pcQ8Jcs77DbX5A=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=\ngithub.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5 h1:hNna6Fi0eP1f2sMBe/rJicDmaHmoXGe1Ta84FPYHLuE=\ngithub.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5/go.mod h1:f1SCnEOt6sc3fOJfPQDRDzHOtSXuTtnz0ImG9kPRDV0=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=\ngithub.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=\ngithub.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=\ngithub.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=\ngithub.com/yapingcat/gomedia v0.0.0-20240316172424-76660eca7389 h1:L33BsOOJZx9Fe97IJHQWeQTecAPKnoCcX7nOtJ3tGoE=\ngithub.com/yapingcat/gomedia v0.0.0-20240316172424-76660eca7389/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngolang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=\ngolang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=\ngolang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=\ngolang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=\ngolang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=\ngolang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=\ngolang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=\ngolang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=\ngolang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=\ngolang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=\ngolang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=\ngolang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=\ngolang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=\ngolang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=\ngolang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=\ngolang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=\ngolang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=\ngolang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=\ngolang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=\ngolang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=\ngolang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=\ngolang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=\ngoogle.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=\ngopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\nrsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=\n"
  },
  {
    "path": "logic/gop_cache.go",
    "content": "package logic\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/q191201771/lal/pkg/base\"\n)\n\ntype GopCache struct {\n\tvideoheader *base.RtmpMsg\n\taudioheader *base.RtmpMsg\n\n\tgopSize              int\n\tsingleGopMaxFrameNum int\n\n\tdata  []Gop\n\tfirst int\n\tlast  int\n}\n\n// gopSize 为 0 时只保存音视频头，不缓存 GOP。\nfunc NewGopCache(gopSize, singleGopMaxFrameNum int) *GopCache {\n\tif gopSize < 0 {\n\t\tgopSize = 0\n\t}\n\tif singleGopMaxFrameNum < 0 {\n\t\tsingleGopMaxFrameNum = 0\n\t}\n\tnum := gopSize + 1\n\treturn &GopCache{\n\t\tdata:                 make([]Gop, num),\n\t\tgopSize:              num,\n\t\tsingleGopMaxFrameNum: singleGopMaxFrameNum,\n\t}\n}\n\nfunc (c *GopCache) Feed(msg base.RtmpMsg) {\n\tswitch msg.Header.MsgTypeId {\n\tcase base.RtmpTypeIdMetadata:\n\t\treturn\n\tcase base.RtmpTypeIdAudio:\n\t\tif msg.IsAacSeqHeader() {\n\t\t\tif c.audioheader == nil || !bytes.Equal(c.audioheader.Payload, msg.Payload) {\n\t\t\t\tc.Clear()\n\t\t\t}\n\t\t\tm := msg.Clone()\n\t\t\tc.audioheader = &m\n\t\t\treturn\n\t\t}\n\t\tif msg.AudioCodecId() == base.RtmpSoundFormatG711A || msg.AudioCodecId() == base.RtmpSoundFormatG711U || msg.AudioCodecId() == base.RtmpSoundFormatOpus {\n\t\t\tif c.audioheader == nil || c.audioheader.AudioCodecId() != msg.AudioCodecId() {\n\t\t\t\tm := msg.Clone()\n\t\t\t\tc.audioheader = &m\n\t\t\t}\n\t\t}\n\tcase base.RtmpTypeIdVideo:\n\t\tif msg.IsVideoKeySeqHeader() {\n\t\t\tif c.videoheader == nil || !bytes.Equal(c.videoheader.Payload, msg.Payload) {\n\t\t\t\tc.Clear()\n\t\t\t}\n\t\t\tm := msg.Clone()\n\t\t\tc.videoheader = &m\n\t\t\treturn\n\t\t}\n\t}\n\n\tif c.gopSize > 1 {\n\t\tif msg.IsVideoKeyNalu() {\n\t\t\tc.feedNewGop(msg)\n\t\t} else {\n\t\t\tc.feedLastGop(msg)\n\t\t}\n\t}\n}\n\nfunc (c *GopCache) feedNewGop(msg base.RtmpMsg) {\n\tif c.isGopRingFull() {\n\t\tc.first = (c.first + 1) % c.gopSize\n\t}\n\tc.data[c.last].clear()\n\tc.data[c.last].feed(msg)\n\tc.last = (c.last + 1) % c.gopSize\n}\n\nfunc (c *GopCache) feedLastGop(msg base.RtmpMsg) {\n\tif c.isGopRingEmpty() {\n\t\treturn\n\t}\n\n\tidx := (c.last - 1 + c.gopSize) % c.gopSize\n\tif c.singleGopMaxFrameNum == 0 || c.data[idx].size() < c.singleGopMaxFrameNum {\n\t\tc.data[idx].feed(msg)\n\t}\n}\n\nfunc (c *GopCache) isGopRingFull() bool {\n\treturn (c.last+1)%c.gopSize == c.first\n}\n\nfunc (c *GopCache) isGopRingEmpty() bool {\n\treturn c.first == c.last\n}\n\nfunc (c *GopCache) Clear() {\n\tfor i := range c.data {\n\t\tc.data[i].release()\n\t}\n\tc.last = 0\n\tc.first = 0\n}\n\nfunc (c *GopCache) GetGopCount() int {\n\treturn (c.last + c.gopSize - c.first) % c.gopSize\n}\n\nfunc (c *GopCache) GetGopDataAt(pos int) []base.RtmpMsg {\n\tif pos >= c.GetGopCount() || pos < 0 {\n\t\treturn nil\n\t}\n\treturn c.data[(c.first+pos)%c.gopSize].data\n}\n\n// clear 保留底层容量用于复用；release 用于码流头变化时释放旧 payload。\ntype Gop struct {\n\tdata []base.RtmpMsg\n}\n\nfunc (g *Gop) feed(msg base.RtmpMsg) {\n\tg.data = append(g.data, msg.Clone())\n}\n\nfunc (g *Gop) clear() {\n\tif len(g.data) == 0 {\n\t\treturn\n\t}\n\tfor i := range g.data {\n\t\tg.data[i] = base.RtmpMsg{}\n\t}\n\tg.data = g.data[:0]\n}\n\nfunc (g *Gop) release() {\n\tg.data = nil\n}\n\nfunc (g *Gop) size() int {\n\treturn len(g.data)\n}\n"
  },
  {
    "path": "logic/group.go",
    "content": "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/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\nvar _ base.ISession = (*subscriberState)(nil)\n\nconst (\n\tSubscriberProtocolLalmax    = \"LALMAX\"\n\tSubscriberProtocolWHEP      = \"WHEP\"\n\tSubscriberProtocolJessibuca = \"JESSIBUCA\"\n\tSubscriberProtocolHTTPFMP4  = \"HTTP-FMP4\"\n\tSubscriberProtocolSRT       = \"SRT\"\n)\n\ntype Subscriber interface {\n\tOnMsg(msg base.RtmpMsg)\n\tOnStop()\n}\n\n// 可选接口：订阅者需要区分 GOP 回放和实时帧时实现。\ntype ReplaySubscriber interface {\n\tOnReplayStart()\n\tOnReplayStop()\n}\n\ntype SubscriberInfo struct {\n\tSubscriberID string\n\tProtocol     string\n\tRemoteAddr   string\n}\n\n// Group 只维护 lalmax 侧订阅者和回放缓存，推流状态仍以 lal 为准。\ntype Group struct {\n\tuniqueKey      string\n\tkey            StreamKey\n\tconsumers      sync.Map\n\thlssvr         *hls.HlsServer\n\tmanager        *ComplexGroupManager\n\thookMux        sync.RWMutex\n\tactiveHookKey  StreamKey\n\tonActiveHook   func(StreamKey)\n\tstopHookKey    StreamKey\n\tonStopHook     func(StreamKey)\n\tgopCache       *GopCache\n\tgopCacheMux    sync.RWMutex\n\tlifecycleMux   sync.RWMutex\n\tstopOnce       sync.Once\n\tmsgMux         sync.Mutex\n\tactiveHookSent bool\n\thasVideo       bool\n\tclosed         atomic.Bool\n}\n\ntype subscriberState struct {\n\tkey          StreamKey\n\tsubscriber   Subscriber\n\tstatProvider SubscriberStatProvider\n\thasSendVideo bool\n\treplayCache  bool\n\twriteMux     sync.Mutex\n\tstatMux      sync.Mutex\n\tstopped      atomic.Bool\n\tlastStatAt   time.Time\n\n\tprevReadBytesSum  uint64\n\tprevWroteBytesSum uint64\n\n\tbase.StatSession\n}\n\nfunc (s *subscriberState) AppName() string {\n\treturn s.key.AppName\n}\n\nfunc (s *subscriberState) GetStat() base.StatSession {\n\tif s == nil {\n\t\treturn base.StatSession{}\n\t}\n\treturn s.refreshStat(0)\n}\n\nfunc (s *subscriberState) IsAlive() (readAlive bool, writeAlive bool) {\n\treturn true, true\n}\n\nfunc (s *subscriberState) RawQuery() string {\n\treturn \"\"\n}\n\nfunc (s *subscriberState) StreamName() string {\n\treturn s.key.StreamName\n}\n\nfunc (s *subscriberState) UniqueKey() string {\n\treturn s.SessionId\n}\n\nfunc (s *subscriberState) UpdateStat(intervalSec uint32) {\n\tif s == nil {\n\t\treturn\n\t}\n\ts.refreshStat(float64(intervalSec))\n}\n\nfunc (s *subscriberState) Url() string {\n\treturn s.key.String()\n}\n\nfunc newGroup(manager *ComplexGroupManager, uniqueKey string, key StreamKey, hlssvr *hls.HlsServer, gopNum, singleGopMaxFrameNum int) *Group {\n\tgroup := &Group{\n\t\tuniqueKey: uniqueKey,\n\t\tkey:       key,\n\t\thlssvr:    hlssvr,\n\t\tmanager:   manager,\n\t\tgopCache:  NewGopCache(gopNum, singleGopMaxFrameNum),\n\t}\n\n\tnazalog.Infof(\"create group, uniqueKey:%s, streamKey:%s\", uniqueKey, key.String())\n\n\treturn group\n}\n\nfunc (group *Group) initHlsSession() {\n\tif group != nil && group.hlssvr != nil {\n\t\tgroup.hlssvr.NewHlsSessionWithAppName(group.key.AppName, group.key.StreamName)\n\t}\n}\n\nfunc (group *Group) waitLifecycleIdle() {\n\tif group == nil {\n\t\treturn\n\t}\n\n\tgroup.lifecycleMux.RLock()\n\tgroup.lifecycleMux.RUnlock()\n}\n\nfunc (group *Group) Key() StreamKey {\n\treturn group.key\n}\n\nfunc (group *Group) UniqueKey() string {\n\treturn group.uniqueKey\n}\n\nfunc (group *Group) BindStopHook(key StreamKey, onStop func(StreamKey)) {\n\tif group == nil {\n\t\treturn\n\t}\n\n\tgroup.hookMux.Lock()\n\tgroup.stopHookKey = key\n\tgroup.onStopHook = onStop\n\tgroup.hookMux.Unlock()\n}\n\nfunc (group *Group) BindActiveHook(key StreamKey, onActive func(StreamKey)) {\n\tif group == nil {\n\t\treturn\n\t}\n\n\tgroup.hookMux.Lock()\n\tgroup.activeHookKey = key\n\tgroup.onActiveHook = onActive\n\tgroup.hookMux.Unlock()\n}\n\nfunc (group *Group) OnMsg(msg base.RtmpMsg) {\n\tgroup.lifecycleMux.RLock()\n\tif group.closed.Load() {\n\t\tgroup.lifecycleMux.RUnlock()\n\t\treturn\n\t}\n\tdefer group.lifecycleMux.RUnlock()\n\n\tif group.hlssvr != nil {\n\t\tgroup.hlssvr.OnMsgWithAppName(group.key.AppName, group.key.StreamName, msg)\n\t}\n\n\tgroup.msgMux.Lock()\n\thasVideo := group.hasVideo\n\tshouldNotifyActive := false\n\tconsumers := make([]*subscriberState, 0)\n\tgroup.consumers.Range(func(key, value interface{}) bool {\n\t\tif c, ok := value.(*subscriberState); ok {\n\t\t\tconsumers = append(consumers, c)\n\t\t}\n\t\treturn true\n\t})\n\n\tif !group.hasVideo && msg.IsVideoKeyNalu() {\n\t\tgroup.hasVideo = true\n\t}\n\tif !group.activeHookSent && isActiveMediaMsg(msg) {\n\t\tgroup.activeHookSent = true\n\t\tshouldNotifyActive = true\n\t}\n\n\tgroup.gopCacheMux.Lock()\n\tgroup.gopCache.Feed(msg)\n\tgroup.gopCacheMux.Unlock()\n\tgroup.msgMux.Unlock()\n\n\tif shouldNotifyActive {\n\t\tgroup.hookMux.RLock()\n\t\tactiveHookKey := group.activeHookKey\n\t\tonActiveHook := group.onActiveHook\n\t\tgroup.hookMux.RUnlock()\n\t\tif onActiveHook != nil {\n\t\t\tonActiveHook(activeHookKey)\n\t\t}\n\t}\n\n\tfor _, c := range consumers {\n\t\tgroup.handleSubscriberMsg(c, msg, hasVideo)\n\t}\n}\n\nfunc isActiveMediaMsg(msg base.RtmpMsg) bool {\n\tswitch msg.Header.MsgTypeId {\n\tcase base.RtmpTypeIdAudio:\n\t\treturn !msg.IsAacSeqHeader()\n\tcase base.RtmpTypeIdVideo:\n\t\treturn !msg.IsVideoKeySeqHeader()\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc (group *Group) OnStop() {\n\tgroup.stopOnce.Do(func() {\n\t\tgroup.lifecycleMux.Lock()\n\t\tgroup.closed.Store(true)\n\n\t\tif group.hlssvr != nil {\n\t\t\tgroup.hlssvr.OnStopWithAppName(group.key.AppName, group.key.StreamName)\n\t\t}\n\n\t\tconsumers := make([]*subscriberState, 0)\n\t\tgroup.consumers.Range(func(key, value interface{}) bool {\n\t\t\tc, ok := value.(*subscriberState)\n\t\t\tif ok {\n\t\t\t\tconsumers = append(consumers, c)\n\t\t\t}\n\t\t\tgroup.consumers.Delete(key)\n\t\t\treturn true\n\t\t})\n\t\tgroup.lifecycleMux.Unlock()\n\n\t\tnazalog.Debugf(\"OnStop, uniqueKey:%s, streamKey:%s\", group.uniqueKey, group.key.String())\n\t\tfor _, c := range consumers {\n\t\t\tc.stopWithNotify()\n\t\t}\n\n\t\tif group.manager != nil {\n\t\t\tgroup.manager.RemoveGroupIfMatch(group.key, group)\n\t\t}\n\n\t\tgroup.hookMux.RLock()\n\t\tstopHookKey := group.stopHookKey\n\t\tonStopHook := group.onStopHook\n\t\tgroup.hookMux.RUnlock()\n\t\tif onStopHook != nil {\n\t\t\tonStopHook(stopHookKey)\n\t\t}\n\t})\n}\n\nfunc (group *Group) AddSubscriber(info SubscriberInfo, subscriber Subscriber) {\n\tgroup.AddSubscriberWithReplay(info, subscriber, true)\n}\n\nfunc (group *Group) AddSubscriberWithReplay(info SubscriberInfo, subscriber Subscriber, replayCache bool) {\n\tif info.SubscriberID == \"\" {\n\t\tnazalog.Warn(\"AddSubscriber skipped, subscriber id is empty\")\n\t\treturn\n\t}\n\tif info.Protocol == \"\" {\n\t\tinfo.Protocol = SubscriberProtocolLalmax\n\t}\n\n\tgroup.lifecycleMux.RLock()\n\tif group.closed.Load() {\n\t\tgroup.lifecycleMux.RUnlock()\n\t\tnazalog.Warnf(\"AddSubscriber skipped, group is closed, streamKey:%s, subscriberId:%s\", group.key.String(), info.SubscriberID)\n\t\treturn\n\t}\n\tdefer group.lifecycleMux.RUnlock()\n\n\tstate := &subscriberState{\n\t\tkey:          group.key,\n\t\tsubscriber:   subscriber,\n\t\treplayCache:  replayCache,\n\t\tlastStatAt:   time.Now(),\n\t\tstatProvider: nil,\n\t\tStatSession: base.StatSession{\n\t\t\tSessionId:  info.SubscriberID,\n\t\t\tProtocol:   info.Protocol,\n\t\t\tBaseType:   base.SessionBaseTypeSubStr,\n\t\t\tRemoteAddr: info.RemoteAddr,\n\t\t\tStartTime:  time.Now().Format(time.DateTime),\n\t\t},\n\t}\n\tif provider, ok := subscriber.(SubscriberStatProvider); ok {\n\t\tstate.statProvider = provider\n\t}\n\n\tnazalog.Infof(\"AddSubscriber, streamKey:%s, subscriberId:%s, protocol:%s\", group.key.String(), info.SubscriberID, info.Protocol)\n\tif replayCache {\n\t\t// 保证该订阅者先收到缓存 GOP，再收到实时帧。\n\t\tstate.writeMux.Lock()\n\t}\n\tvar replayMsgs []base.RtmpMsg\n\n\tgroup.msgMux.Lock()\n\tif _, loaded := group.consumers.Load(info.SubscriberID); loaded {\n\t\tgroup.msgMux.Unlock()\n\t\tif replayCache {\n\t\t\tstate.writeMux.Unlock()\n\t\t}\n\t\tnazalog.Warnf(\"AddSubscriber skipped, subscriber already exists, streamKey:%s, subscriberId:%s\", group.key.String(), info.SubscriberID)\n\t\treturn\n\t}\n\tgroup.consumers.Store(info.SubscriberID, state)\n\tif replayCache {\n\t\treplayMsgs = group.getGopReplayMessages()\n\t}\n\tgroup.msgMux.Unlock()\n\n\tif replayCache {\n\t\tgroup.replayGopMessagesLocked(state, replayMsgs)\n\t\tstate.writeMux.Unlock()\n\t}\n}\n\nfunc (group *Group) AddConsumer(consumerID string, subscriber Subscriber) {\n\tgroup.AddSubscriber(SubscriberInfo{SubscriberID: consumerID}, subscriber)\n}\n\nfunc (group *Group) AddConsumerWithReplay(consumerID string, subscriber Subscriber, replayCache bool) {\n\tgroup.AddSubscriberWithReplay(SubscriberInfo{SubscriberID: consumerID}, subscriber, replayCache)\n}\n\nfunc (group *Group) StatSubscribers() []base.StatSub {\n\tout := make([]base.StatSub, 0, 10)\n\tgroup.consumers.Range(func(key, value any) bool {\n\t\tv, ok := value.(*subscriberState)\n\t\tif ok {\n\t\t\tout = append(out, base.Session2StatSub(v))\n\t\t}\n\t\treturn true\n\t})\n\treturn out\n}\n\nfunc (group *Group) GetAllConsumer() []base.StatSub {\n\treturn group.StatSubscribers()\n}\n\nfunc (group *Group) RemoveSubscriber(subscriberID string) {\n\tvalue, ok := group.consumers.LoadAndDelete(subscriberID)\n\tif ok {\n\t\tnazalog.Infof(\"RemoveSubscriber, streamKey:%s, subscriberId:%s\", group.key.String(), subscriberID)\n\t\tif c, ok := value.(*subscriberState); ok {\n\t\t\tc.stopWithoutNotify()\n\t\t}\n\t}\n}\n\nfunc (group *Group) RemoveConsumer(consumerID string) {\n\tgroup.RemoveSubscriber(consumerID)\n}\n\nfunc (group *Group) GetVideoSeqHeaderMsg() *base.RtmpMsg {\n\tgroup.gopCacheMux.RLock()\n\tdefer group.gopCacheMux.RUnlock()\n\tif group.gopCache.videoheader == nil {\n\t\treturn nil\n\t}\n\tm := group.gopCache.videoheader.Clone()\n\treturn &m\n}\n\nfunc (group *Group) GetAudioSeqHeaderMsg() *base.RtmpMsg {\n\tgroup.gopCacheMux.RLock()\n\tdefer group.gopCacheMux.RUnlock()\n\tif group.gopCache.audioheader == nil {\n\t\treturn nil\n\t}\n\tm := group.gopCache.audioheader.Clone()\n\treturn &m\n}\n\nfunc (group *Group) handleSubscriberMsg(c *subscriberState, msg base.RtmpMsg, hasVideo bool) {\n\tif c == nil {\n\t\treturn\n\t}\n\n\tc.writeMux.Lock()\n\tdefer c.writeMux.Unlock()\n\n\tif c.stopped.Load() || c.subscriber == nil {\n\t\treturn\n\t}\n\n\tif msg.Header.MsgTypeId == base.RtmpTypeIdVideo {\n\t\tif !c.hasSendVideo {\n\t\t\tif !msg.IsVideoKeyNalu() {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif v := group.GetVideoSeqHeaderMsg(); v != nil {\n\t\t\t\tif !c.deliverMsg(*v) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tif v := group.GetAudioSeqHeaderMsg(); v != nil && v.IsAacSeqHeader() {\n\t\t\t\tif !c.deliverMsg(*v) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\tc.hasSendVideo = true\n\t\t}\n\n\t\tc.deliverMsg(msg)\n\t} else if msg.Header.MsgTypeId == base.RtmpTypeIdAudio {\n\t\tif !hasVideo || c.hasSendVideo {\n\t\t\tc.deliverMsg(msg)\n\t\t}\n\t}\n}\n\nfunc (group *Group) replayGopMessagesLocked(c *subscriberState, msgs []base.RtmpMsg) {\n\tif c == nil || c.subscriber == nil || c.stopped.Load() || c.hasSendVideo || !c.replayCache {\n\t\treturn\n\t}\n\n\tif len(msgs) == 0 {\n\t\treturn\n\t}\n\n\tif replaySubscriber, ok := c.subscriber.(ReplaySubscriber); ok {\n\t\treplaySubscriber.OnReplayStart()\n\t\tdefer replaySubscriber.OnReplayStop()\n\t}\n\n\tfor _, msg := range msgs {\n\t\tif !c.deliverMsg(msg) {\n\t\t\treturn\n\t\t}\n\t}\n\tc.hasSendVideo = true\n}\n\nfunc (s *subscriberState) deliverMsg(msg base.RtmpMsg) bool {\n\tif s == nil || s.stopped.Load() || s.subscriber == nil {\n\t\treturn false\n\t}\n\n\ts.subscriber.OnMsg(msg)\n\treturn !s.stopped.Load() && s.subscriber != nil\n}\n\nfunc (s *subscriberState) refreshStat(intervalSec float64) base.StatSession {\n\ts.statMux.Lock()\n\tdefer s.statMux.Unlock()\n\n\ts.refreshStatSnapshotLocked()\n\n\tif intervalSec <= 0 {\n\t\tif s.lastStatAt.IsZero() {\n\t\t\ts.lastStatAt = time.Now()\n\t\t\treturn s.StatSession\n\t\t}\n\t\tintervalSec = time.Since(s.lastStatAt).Seconds()\n\t\tif intervalSec < 1 {\n\t\t\treturn s.StatSession\n\t\t}\n\t}\n\n\ts.updateBitrateLocked(intervalSec)\n\ts.lastStatAt = time.Now()\n\treturn s.StatSession\n}\n\nfunc (s *subscriberState) refreshStatSnapshotLocked() {\n\tif s.statProvider == nil {\n\t\treturn\n\t}\n\n\tstat := s.statProvider.GetSubscriberStat()\n\tif stat.RemoteAddr != \"\" {\n\t\ts.StatSession.RemoteAddr = stat.RemoteAddr\n\t}\n\ts.StatSession.ReadBytesSum = stat.ReadBytesSum\n\ts.StatSession.WroteBytesSum = stat.WroteBytesSum\n}\n\nfunc (s *subscriberState) updateBitrateLocked(intervalSec float64) {\n\tif intervalSec <= 0 {\n\t\treturn\n\t}\n\n\treadDiff := diffUint64(s.StatSession.ReadBytesSum, s.prevReadBytesSum)\n\twriteDiff := diffUint64(s.StatSession.WroteBytesSum, s.prevWroteBytesSum)\n\n\ts.StatSession.ReadBitrateKbits = bitrateFromBytes(readDiff, intervalSec)\n\ts.StatSession.WriteBitrateKbits = bitrateFromBytes(writeDiff, intervalSec)\n\ts.StatSession.BitrateKbits = s.StatSession.WriteBitrateKbits\n\n\ts.prevReadBytesSum = s.StatSession.ReadBytesSum\n\ts.prevWroteBytesSum = s.StatSession.WroteBytesSum\n}\n\nfunc bitrateFromBytes(bytes uint64, intervalSec float64) int {\n\treturn int(float64(bytes) * 8 / 1024 / intervalSec)\n}\n\nfunc diffUint64(curr, prev uint64) uint64 {\n\tif curr < prev {\n\t\treturn curr\n\t}\n\treturn curr - prev\n}\n\nfunc (s *subscriberState) stopWithNotify() {\n\tif s == nil {\n\t\treturn\n\t}\n\n\ts.writeMux.Lock()\n\tdefer s.writeMux.Unlock()\n\n\tif s.stopped.Swap(true) {\n\t\treturn\n\t}\n\tif s.subscriber != nil {\n\t\ts.subscriber.OnStop()\n\t\ts.subscriber = nil\n\t}\n}\n\nfunc (s *subscriberState) stopWithoutNotify() {\n\tif s == nil {\n\t\treturn\n\t}\n\n\t// 不能在这里获取 writeMux：部分订阅者会在 OnMsg 调用栈内主动移除自己。\n\t// 只标记停止，避免后续投递；订阅者对象随 state 一起释放。\n\ts.stopped.Store(true)\n}\n\nfunc (group *Group) getGopReplayMessages() []base.RtmpMsg {\n\tgroup.gopCacheMux.RLock()\n\tdefer group.gopCacheMux.RUnlock()\n\n\tgopCount := group.gopCache.GetGopCount()\n\tif gopCount == 0 {\n\t\treturn nil\n\t}\n\n\tmsgs := make([]base.RtmpMsg, 0, gopCount)\n\tif v := group.gopCache.videoheader; v != nil {\n\t\tmsgs = append(msgs, v.Clone())\n\t}\n\tif v := group.gopCache.audioheader; v != nil && v.IsAacSeqHeader() {\n\t\tmsgs = append(msgs, v.Clone())\n\t}\n\tfor i := 0; i < gopCount; i++ {\n\t\tfor _, item := range group.gopCache.GetGopDataAt(i) {\n\t\t\tmsgs = append(msgs, item.Clone())\n\t\t}\n\t}\n\n\treturn msgs\n}\n"
  },
  {
    "path": "logic/group_manager.go",
    "content": "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/nazalog\"\n)\n\ntype IGroupManager interface {\n\tGetOrCreateGroup(key StreamKey, uniqueKey string, hlssvr *hls.HlsServer, gopNum, singleGopMaxFrameNum int) (*Group, bool)\n\tRemoveGroup(key StreamKey)\n\tRemoveGroupIfMatch(key StreamKey, group *Group)\n\tGetGroup(key StreamKey) (bool, *Group)\n\tIterate(onIterateGroup func(key StreamKey, group *Group) bool)\n\tLen() int\n}\n\ntype ComplexGroupManager struct {\n\tmutex sync.RWMutex\n\n\tonlyStreamNameGroups    map[string]*Group\n\tappNameStreamNameGroups map[string]map[string]*Group\n}\n\n// 同时支持新路径 app/stream 和旧路径 stream 的查找方式。\nfunc NewComplexGroupManager() *ComplexGroupManager {\n\treturn &ComplexGroupManager{\n\t\tonlyStreamNameGroups:    make(map[string]*Group),\n\t\tappNameStreamNameGroups: make(map[string]map[string]*Group),\n\t}\n}\n\nvar (\n\tdefaultGroupManager *ComplexGroupManager\n\tgroupManagerOnce    sync.Once\n)\n\nfunc GetGroupManagerInstance() *ComplexGroupManager {\n\tgroupManagerOnce.Do(func() {\n\t\tdefaultGroupManager = NewComplexGroupManager()\n\t})\n\treturn defaultGroupManager\n}\n\nfunc (m *ComplexGroupManager) GetOrCreateGroup(key StreamKey, uniqueKey string, hlssvr *hls.HlsServer, gopNum, singleGopMaxFrameNum int) (*Group, bool) {\n\tif m == nil || !key.Valid() {\n\t\treturn nil, false\n\t}\n\n\tfor {\n\t\tm.mutex.Lock()\n\t\tok, existing := m.getGroupLocked(key)\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\t\tif !existing.closed.Load() {\n\t\t\tm.mutex.Unlock()\n\t\t\treturn existing, false\n\t\t}\n\t\tm.mutex.Unlock()\n\n\t\t// 等旧 group 完成 HLS 清理后再发布替换 group，\n\t\t// 否则旧 group 的 OnStop 可能删掉新的 HLS session。\n\t\texisting.waitLifecycleIdle()\n\n\t\tm.mutex.Lock()\n\t\tok, current := m.getGroupLocked(key)\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\t\tif current == existing {\n\t\t\tbreak\n\t\t}\n\t\tif !current.closed.Load() {\n\t\t\tm.mutex.Unlock()\n\t\t\treturn current, false\n\t\t}\n\t\tm.mutex.Unlock()\n\t}\n\n\tgroup := newGroup(m, uniqueKey, key, hlssvr, gopNum, singleGopMaxFrameNum)\n\tgroup.initHlsSession()\n\tm.setGroupLocked(key, group)\n\tm.mutex.Unlock()\n\treturn group, true\n}\n\nfunc (m *ComplexGroupManager) GetOrCreateGroupByStreamName(uniqueKey, streamName string, hlssvr *hls.HlsServer, gopNum, singleGopMaxFrameNum int) (*Group, bool) {\n\treturn m.GetOrCreateGroup(StreamKeyFromStreamName(streamName), uniqueKey, hlssvr, gopNum, singleGopMaxFrameNum)\n}\n\nfunc (m *ComplexGroupManager) setGroup(key StreamKey, group *Group) {\n\tif m == nil || !key.Valid() || group == nil {\n\t\treturn\n\t}\n\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\n\tm.setGroupLocked(key, group)\n}\n\nfunc (m *ComplexGroupManager) setGroupLocked(key StreamKey, group *Group) {\n\tnazalog.Info(\"SetGroup, streamKey:\", key.String())\n\n\tgroup.manager = m\n\tif key.AppName == \"\" {\n\t\tm.onlyStreamNameGroups[key.StreamName] = group\n\t\treturn\n\t}\n\n\tgroups, ok := m.appNameStreamNameGroups[key.AppName]\n\tif !ok {\n\t\tgroups = make(map[string]*Group)\n\t\tm.appNameStreamNameGroups[key.AppName] = groups\n\t}\n\tgroups[key.StreamName] = group\n}\n\nfunc (m *ComplexGroupManager) setGroupByStreamName(streamName string, group *Group) {\n\tm.setGroup(StreamKeyFromStreamName(streamName), group)\n}\n\nfunc (m *ComplexGroupManager) RemoveGroup(key StreamKey) {\n\tm.removeGroup(key, nil, false)\n}\n\n// 避免旧流晚到的 OnStop 或遍历删除误删同 key 的新流。\nfunc (m *ComplexGroupManager) RemoveGroupIfMatch(key StreamKey, group *Group) {\n\tm.removeGroup(key, group, true)\n}\n\nfunc (m *ComplexGroupManager) removeGroup(key StreamKey, group *Group, shouldMatch bool) {\n\tif m == nil || !key.Valid() {\n\t\treturn\n\t}\n\n\tnazalog.Info(\"RemoveGroup, streamKey:\", key.String())\n\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\n\tif key.AppName == \"\" {\n\t\tif shouldMatch && m.onlyStreamNameGroups[key.StreamName] != group {\n\t\t\treturn\n\t\t}\n\t\tdelete(m.onlyStreamNameGroups, key.StreamName)\n\t\treturn\n\t}\n\n\tdeleted := false\n\tif groups, ok := m.appNameStreamNameGroups[key.AppName]; ok {\n\t\tif current, ok := groups[key.StreamName]; ok {\n\t\t\tif shouldMatch && current != group {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdelete(groups, key.StreamName)\n\t\t\tdeleted = true\n\t\t}\n\t\tif len(groups) == 0 {\n\t\t\tdelete(m.appNameStreamNameGroups, key.AppName)\n\t\t}\n\t}\n\n\tif !deleted {\n\t\tif shouldMatch && m.onlyStreamNameGroups[key.StreamName] != group {\n\t\t\treturn\n\t\t}\n\t\tdelete(m.onlyStreamNameGroups, key.StreamName)\n\t}\n}\n\nfunc (m *ComplexGroupManager) RemoveGroupByStreamName(streamName string) {\n\tm.RemoveGroup(StreamKeyFromStreamName(streamName))\n}\n\nfunc (m *ComplexGroupManager) GetGroup(key StreamKey) (bool, *Group) {\n\tif m == nil || !key.Valid() {\n\t\treturn false, nil\n\t}\n\n\tm.mutex.RLock()\n\tdefer m.mutex.RUnlock()\n\n\treturn m.getGroupLocked(key)\n}\n\nfunc (m *ComplexGroupManager) getGroupLocked(key StreamKey) (bool, *Group) {\n\tif key.AppName == \"\" {\n\t\tif group, ok := m.onlyStreamNameGroups[key.StreamName]; ok {\n\t\t\treturn true, group\n\t\t}\n\t\treturn m.getGroupByOnlyStreamNameLocked(key.StreamName)\n\t}\n\n\tif groups, ok := m.appNameStreamNameGroups[key.AppName]; ok {\n\t\tif group, ok := groups[key.StreamName]; ok {\n\t\t\treturn true, group\n\t\t}\n\t}\n\n\tif group, ok := m.onlyStreamNameGroups[key.StreamName]; ok {\n\t\treturn true, group\n\t}\n\n\treturn false, nil\n}\n\nfunc (m *ComplexGroupManager) GetGroupByStreamName(streamName string) (bool, *Group) {\n\treturn m.GetGroup(StreamKeyFromStreamName(streamName))\n}\n\n// WaitGroup 等待流就绪，轮询 interval 间隔，总超时 timeout\n// 为什么：GB28181 设备推流有延迟，播放端先于推流端到达，需短暂等待\nfunc (m *ComplexGroupManager) WaitGroup(key StreamKey, interval, timeout time.Duration) (bool, *Group) {\n\tdeadline := time.Now().Add(timeout)\n\tfor {\n\t\tif ok, g := m.GetGroup(key); ok {\n\t\t\treturn true, g\n\t\t}\n\t\tif time.Now().After(deadline) {\n\t\t\treturn false, nil\n\t\t}\n\t\ttime.Sleep(interval)\n\t}\n}\n\n// streamName 单独查找只在匹配唯一 appName 时成功，避免跨 app 串流。\nfunc (m *ComplexGroupManager) getGroupByOnlyStreamNameLocked(streamName string) (bool, *Group) {\n\tvar found *Group\n\tmatchCount := 0\n\tfor _, groups := range m.appNameStreamNameGroups {\n\t\tif group, ok := groups[streamName]; ok {\n\t\t\tfound = group\n\t\t\tmatchCount++\n\t\t\tif matchCount > 1 {\n\t\t\t\tnazalog.Warn(\"streamName matched multiple appName groups, streamName:\", streamName)\n\t\t\t\treturn false, nil\n\t\t\t}\n\t\t}\n\t}\n\treturn matchCount == 1, found\n}\n\nfunc (m *ComplexGroupManager) Iterate(onIterateGroup func(key StreamKey, group *Group) bool) {\n\tif m == nil || onIterateGroup == nil {\n\t\treturn\n\t}\n\n\ttype entry struct {\n\t\tkey   StreamKey\n\t\tgroup *Group\n\t}\n\tentries := make([]entry, 0, m.Len())\n\n\tm.mutex.RLock()\n\tfor streamName, group := range m.onlyStreamNameGroups {\n\t\tentries = append(entries, entry{key: StreamKeyFromStreamName(streamName), group: group})\n\t}\n\tfor appName, groups := range m.appNameStreamNameGroups {\n\t\tfor streamName, group := range groups {\n\t\t\tentries = append(entries, entry{key: NewStreamKey(appName, streamName), group: group})\n\t\t}\n\t}\n\tm.mutex.RUnlock()\n\n\tfor _, item := range entries {\n\t\tif !onIterateGroup(item.key, item.group) {\n\t\t\tm.RemoveGroupIfMatch(item.key, item.group)\n\t\t}\n\t}\n}\n\nfunc (m *ComplexGroupManager) Len() int {\n\tif m == nil {\n\t\treturn 0\n\t}\n\n\tm.mutex.RLock()\n\tdefer m.mutex.RUnlock()\n\n\tcount := len(m.onlyStreamNameGroups)\n\tfor _, groups := range m.appNameStreamNameGroups {\n\t\tcount += len(groups)\n\t}\n\treturn count\n}\n"
  },
  {
    "path": "logic/group_test.go",
    "content": "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 struct {\n\tmu        sync.Mutex\n\tmsgs      []base.RtmpMsg\n\tstopCount int\n}\n\nfunc (s *recordSubscriber) OnMsg(msg base.RtmpMsg) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\ts.msgs = append(s.msgs, msg.Clone())\n}\n\nfunc (s *recordSubscriber) OnStop() {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\ts.stopCount++\n}\n\nfunc (s *recordSubscriber) len() int {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\treturn len(s.msgs)\n}\n\nfunc (s *recordSubscriber) markerAt(idx int) byte {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\treturn payloadMarker(s.msgs[idx])\n}\n\nfunc (s *recordSubscriber) stopCountValue() int {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\treturn s.stopCount\n}\n\ntype blockingSubscriber struct {\n\tmu        sync.Mutex\n\tmsgs      []base.RtmpMsg\n\tblocked   chan struct{}\n\trelease   chan struct{}\n\treplaying bool\n\tblockOnce sync.Once\n}\n\nfunc newBlockingSubscriber() *blockingSubscriber {\n\treturn &blockingSubscriber{\n\t\tblocked: make(chan struct{}),\n\t\trelease: make(chan struct{}),\n\t}\n}\n\nfunc (s *blockingSubscriber) OnMsg(msg base.RtmpMsg) {\n\ts.mu.Lock()\n\ts.msgs = append(s.msgs, msg.Clone())\n\tshouldBlock := s.replaying\n\ts.mu.Unlock()\n\n\tif shouldBlock {\n\t\ts.blockOnce.Do(func() {\n\t\t\tclose(s.blocked)\n\t\t\t<-s.release\n\t\t})\n\t}\n}\n\nfunc (s *blockingSubscriber) OnStop() {}\n\nfunc (s *blockingSubscriber) OnReplayStart() {\n\ts.mu.Lock()\n\ts.replaying = true\n\ts.mu.Unlock()\n}\n\nfunc (s *blockingSubscriber) OnReplayStop() {\n\ts.mu.Lock()\n\ts.replaying = false\n\ts.mu.Unlock()\n}\n\nfunc (s *blockingSubscriber) markers() []byte {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tout := make([]byte, 0, len(s.msgs))\n\tfor _, msg := range s.msgs {\n\t\tout = append(out, payloadMarker(msg))\n\t}\n\treturn out\n}\n\ntype selfRemovingSubscriber struct {\n\tgroup *Group\n\tid    string\n\n\tmu   sync.Mutex\n\tmsgs []base.RtmpMsg\n}\n\nfunc (s *selfRemovingSubscriber) OnMsg(msg base.RtmpMsg) {\n\ts.mu.Lock()\n\ts.msgs = append(s.msgs, msg.Clone())\n\tshouldRemove := len(s.msgs) == 1\n\ts.mu.Unlock()\n\n\tif shouldRemove {\n\t\ts.group.RemoveConsumer(s.id)\n\t}\n}\n\nfunc (s *selfRemovingSubscriber) OnStop() {}\n\nfunc (s *selfRemovingSubscriber) len() int {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\treturn len(s.msgs)\n}\n\nfunc (s *selfRemovingSubscriber) markerAt(idx int) byte {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\treturn payloadMarker(s.msgs[idx])\n}\n\ntype statSubscriber struct {\n\tmu   sync.Mutex\n\tstat SubscriberStat\n}\n\nfunc (s *statSubscriber) OnMsg(msg base.RtmpMsg) {}\n\nfunc (s *statSubscriber) OnStop() {}\n\nfunc (s *statSubscriber) GetSubscriberStat() SubscriberStat {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\treturn s.stat\n}\n\nfunc (s *statSubscriber) setStat(stat SubscriberStat) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\ts.stat = stat\n}\n\nfunc videoSeqHeader(marker byte) base.RtmpMsg {\n\treturn base.RtmpMsg{\n\t\tHeader: base.RtmpHeader{MsgTypeId: base.RtmpTypeIdVideo},\n\t\tPayload: []byte{\n\t\t\tbase.RtmpAvcKeyFrame,\n\t\t\tbase.RtmpAvcPacketTypeSeqHeader,\n\t\t\t0, 0, 0,\n\t\t\tmarker,\n\t\t},\n\t}\n}\n\nfunc videoKeyNalu(marker byte) base.RtmpMsg {\n\treturn base.RtmpMsg{\n\t\tHeader: base.RtmpHeader{MsgTypeId: base.RtmpTypeIdVideo},\n\t\tPayload: []byte{\n\t\t\tbase.RtmpAvcKeyFrame,\n\t\t\tbase.RtmpAvcPacketTypeNalu,\n\t\t\t0, 0, 0,\n\t\t\tmarker,\n\t\t},\n\t}\n}\n\nfunc videoInterNalu(marker byte) base.RtmpMsg {\n\treturn base.RtmpMsg{\n\t\tHeader: base.RtmpHeader{MsgTypeId: base.RtmpTypeIdVideo},\n\t\tPayload: []byte{\n\t\t\tbase.RtmpAvcInterFrame,\n\t\t\tbase.RtmpAvcPacketTypeNalu,\n\t\t\t0, 0, 0,\n\t\t\tmarker,\n\t\t},\n\t}\n}\n\nfunc aacSeqHeader(marker byte) base.RtmpMsg {\n\treturn base.RtmpMsg{\n\t\tHeader: base.RtmpHeader{MsgTypeId: base.RtmpTypeIdAudio},\n\t\tPayload: []byte{\n\t\t\tbase.RtmpSoundFormatAac << 4,\n\t\t\tbase.RtmpAacPacketTypeSeqHeader,\n\t\t\tmarker,\n\t\t},\n\t}\n}\n\nfunc aacRaw(marker byte) base.RtmpMsg {\n\treturn base.RtmpMsg{\n\t\tHeader: base.RtmpHeader{MsgTypeId: base.RtmpTypeIdAudio},\n\t\tPayload: []byte{\n\t\t\tbase.RtmpSoundFormatAac << 4,\n\t\t\tbase.RtmpAacPacketTypeRaw,\n\t\t\tmarker,\n\t\t},\n\t}\n}\n\nfunc g711aAudio(marker byte) base.RtmpMsg {\n\treturn base.RtmpMsg{\n\t\tHeader:  base.RtmpHeader{MsgTypeId: base.RtmpTypeIdAudio},\n\t\tPayload: []byte{base.RtmpSoundFormatG711A<<4 | marker},\n\t}\n}\n\nfunc payloadMarker(msg base.RtmpMsg) byte {\n\treturn msg.Payload[len(msg.Payload)-1]\n}\n\nfunc newTestGroup(streamName string) *Group {\n\tgroup, _ := GetGroupManagerInstance().GetOrCreateGroupByStreamName(streamName, streamName, nil, 1, 0)\n\treturn group\n}\n\nfunc testSubscriberState(t *testing.T, group *Group, subscriberID string) *subscriberState {\n\tt.Helper()\n\n\tvalue, ok := group.consumers.Load(subscriberID)\n\tif !ok {\n\t\tt.Fatalf(\"subscriber %s not found\", subscriberID)\n\t}\n\n\tstate, ok := value.(*subscriberState)\n\tif !ok {\n\t\tt.Fatalf(\"subscriber %s has unexpected type %T\", subscriberID, value)\n\t}\n\n\treturn state\n}\n\nfunc TestAddConsumerReplaysCachedGopImmediately(t *testing.T) {\n\tgroup := newTestGroup(\"test-replay\")\n\tdefer GetGroupManagerInstance().RemoveGroupByStreamName(\"test-replay\")\n\n\tgroup.OnMsg(videoSeqHeader(1))\n\tgroup.OnMsg(aacSeqHeader(2))\n\tgroup.OnMsg(videoKeyNalu(3))\n\tgroup.OnMsg(aacRaw(4))\n\tgroup.OnMsg(videoInterNalu(5))\n\n\tsub := &recordSubscriber{}\n\tgroup.AddConsumer(\"consumer\", sub)\n\n\tif sub.len() != 5 {\n\t\tt.Fatalf(\"expected 5 replay messages, got %d\", sub.len())\n\t}\n\n\twantMarkers := []byte{1, 2, 3, 4, 5}\n\tfor i, want := range wantMarkers {\n\t\tif got := sub.markerAt(i); got != want {\n\t\t\tt.Fatalf(\"message %d marker = %d, want %d\", i, got, want)\n\t\t}\n\t}\n}\n\nfunc TestVideoSeqHeaderChangeClearsStaleGop(t *testing.T) {\n\tgroup := newTestGroup(\"test-clear\")\n\tdefer GetGroupManagerInstance().RemoveGroupByStreamName(\"test-clear\")\n\n\tgroup.OnMsg(videoSeqHeader(1))\n\tgroup.OnMsg(videoKeyNalu(2))\n\tgroup.OnMsg(videoInterNalu(3))\n\tgroup.OnMsg(videoSeqHeader(4))\n\n\tsub := &recordSubscriber{}\n\tgroup.AddConsumer(\"consumer\", sub)\n\tif sub.len() != 0 {\n\t\tt.Fatalf(\"expected no stale GOP replay after sequence header change, got %d messages\", sub.len())\n\t}\n\n\tgroup.OnMsg(videoKeyNalu(5))\n\tif sub.len() != 2 {\n\t\tt.Fatalf(\"expected new header and current key frame, got %d messages\", sub.len())\n\t}\n\tif got := sub.markerAt(0); got != 4 {\n\t\tt.Fatalf(\"header marker = %d, want 4\", got)\n\t}\n\tif got := sub.markerAt(1); got != 5 {\n\t\tt.Fatalf(\"key frame marker = %d, want 5\", got)\n\t}\n}\n\nfunc TestNonAacAudioIsNotReplayedAsHeader(t *testing.T) {\n\tgroup := newTestGroup(\"test-g711\")\n\tdefer GetGroupManagerInstance().RemoveGroupByStreamName(\"test-g711\")\n\n\tgroup.OnMsg(videoSeqHeader(1))\n\tgroup.OnMsg(videoKeyNalu(2))\n\tgroup.OnMsg(g711aAudio(3))\n\n\tsub := &recordSubscriber{}\n\tgroup.AddConsumer(\"consumer\", sub)\n\n\tif sub.len() != 3 {\n\t\tt.Fatalf(\"expected video header, key frame and one G711 packet, got %d messages\", sub.len())\n\t}\n\n\twantMarkers := []byte{1, 2, base.RtmpSoundFormatG711A<<4 | 3}\n\tfor i, want := range wantMarkers {\n\t\tif got := sub.markerAt(i); got != want {\n\t\t\tt.Fatalf(\"message %d marker = %d, want %d\", i, got, want)\n\t\t}\n\t}\n}\n\nfunc TestAddConsumerWithReplayDisabledDoesNotReplayCachedGop(t *testing.T) {\n\tgroup := newTestGroup(\"test-no-replay\")\n\tdefer GetGroupManagerInstance().RemoveGroupByStreamName(\"test-no-replay\")\n\n\tgroup.OnMsg(videoSeqHeader(1))\n\tgroup.OnMsg(videoKeyNalu(2))\n\tgroup.OnMsg(videoInterNalu(3))\n\n\tsub := &recordSubscriber{}\n\tgroup.AddConsumerWithReplay(\"consumer\", sub, false)\n\n\tif sub.len() != 0 {\n\t\tt.Fatalf(\"expected no cached messages when replay is disabled, got %d messages\", sub.len())\n\t}\n\n\tgroup.OnMsg(videoInterNalu(4))\n\tif sub.len() != 0 {\n\t\tt.Fatalf(\"expected to wait for next key frame, got %d messages\", sub.len())\n\t}\n\n\tgroup.OnMsg(videoKeyNalu(5))\n\tif sub.len() != 2 {\n\t\tt.Fatalf(\"expected header and current key frame, got %d messages\", sub.len())\n\t}\n\tif got := sub.markerAt(0); got != 1 {\n\t\tt.Fatalf(\"header marker = %d, want 1\", got)\n\t}\n\tif got := sub.markerAt(1); got != 5 {\n\t\tt.Fatalf(\"key frame marker = %d, want 5\", got)\n\t}\n}\n\nfunc TestAddConsumerReplayDoesNotInterleaveWithLiveKeyFrame(t *testing.T) {\n\tgroup := newTestGroup(\"test-replay-order\")\n\tdefer GetGroupManagerInstance().RemoveGroupByStreamName(\"test-replay-order\")\n\n\tgroup.OnMsg(videoSeqHeader(1))\n\tgroup.OnMsg(videoKeyNalu(2))\n\tgroup.OnMsg(videoInterNalu(3))\n\n\tsub := newBlockingSubscriber()\n\taddDone := make(chan struct{})\n\tgo func() {\n\t\tgroup.AddConsumer(\"consumer\", sub)\n\t\tclose(addDone)\n\t}()\n\n\t<-sub.blocked\n\n\tliveDone := make(chan struct{})\n\tgo func() {\n\t\tgroup.OnMsg(videoKeyNalu(4))\n\t\tclose(liveDone)\n\t}()\n\n\tselect {\n\tcase <-liveDone:\n\t\tt.Fatal(\"live key frame should not be delivered before cached GOP replay finishes\")\n\tcase <-time.After(50 * time.Millisecond):\n\t}\n\n\tclose(sub.release)\n\t<-addDone\n\t<-liveDone\n\n\twantMarkers := []byte{1, 2, 3, 4}\n\tgotMarkers := sub.markers()\n\tif len(gotMarkers) != len(wantMarkers) {\n\t\tt.Fatalf(\"markers = %v, want %v\", gotMarkers, wantMarkers)\n\t}\n\tfor i, want := range wantMarkers {\n\t\tif got := gotMarkers[i]; got != want {\n\t\t\tt.Fatalf(\"message %d marker = %d, want %d, all=%v\", i, got, want, gotMarkers)\n\t\t}\n\t}\n}\n\nfunc TestSubscriberRemovingItselfStopsReplayDelivery(t *testing.T) {\n\tgroup := newTestGroup(\"test-self-remove-replay\")\n\tdefer GetGroupManagerInstance().RemoveGroupByStreamName(\"test-self-remove-replay\")\n\n\tgroup.OnMsg(videoSeqHeader(1))\n\tgroup.OnMsg(videoKeyNalu(2))\n\tgroup.OnMsg(videoInterNalu(3))\n\n\tsub := &selfRemovingSubscriber{group: group, id: \"consumer\"}\n\tgroup.AddConsumer(sub.id, sub)\n\n\tif sub.len() != 1 {\n\t\tt.Fatalf(\"messages after self remove = %d, want 1\", sub.len())\n\t}\n\tif got := sub.markerAt(0); got != 1 {\n\t\tt.Fatalf(\"first marker = %d, want 1\", got)\n\t}\n\n\tgroup.OnMsg(videoKeyNalu(4))\n\tif sub.len() != 1 {\n\t\tt.Fatalf(\"messages after live frame = %d, want 1\", sub.len())\n\t}\n}\n\nfunc TestSubscriberRemovingItselfStopsHeaderAndLiveDelivery(t *testing.T) {\n\tgroup := newTestGroup(\"test-self-remove-live\")\n\tdefer GetGroupManagerInstance().RemoveGroupByStreamName(\"test-self-remove-live\")\n\n\tgroup.OnMsg(videoSeqHeader(1))\n\tgroup.OnMsg(aacSeqHeader(2))\n\n\tsub := &selfRemovingSubscriber{group: group, id: \"consumer\"}\n\tgroup.AddConsumerWithReplay(sub.id, sub, false)\n\n\tgroup.OnMsg(videoKeyNalu(3))\n\tif sub.len() != 1 {\n\t\tt.Fatalf(\"messages after self remove = %d, want 1\", sub.len())\n\t}\n\tif got := sub.markerAt(0); got != 1 {\n\t\tt.Fatalf(\"first marker = %d, want 1\", got)\n\t}\n}\n\nfunc TestGroupManagerSupportsAppNameAndStreamName(t *testing.T) {\n\tmanager := NewComplexGroupManager()\n\tgroup := &Group{key: NewStreamKey(\"live\", \"camera\")}\n\n\tmanager.setGroup(group.Key(), group)\n\n\tok, got := manager.GetGroup(NewStreamKey(\"live\", \"camera\"))\n\tif !ok || got != group {\n\t\tt.Fatal(\"expected exact appName and streamName lookup\")\n\t}\n\n\tok, got = manager.GetGroup(StreamKeyFromStreamName(\"camera\"))\n\tif !ok || got != group {\n\t\tt.Fatal(\"expected streamName-only lookup to find the unique appName group\")\n\t}\n}\n\nfunc TestGroupManagerStreamNameFallbackRejectsAmbiguousAppName(t *testing.T) {\n\tmanager := NewComplexGroupManager()\n\tmanager.setGroup(NewStreamKey(\"app1\", \"camera\"), &Group{key: NewStreamKey(\"app1\", \"camera\")})\n\tmanager.setGroup(NewStreamKey(\"app2\", \"camera\"), &Group{key: NewStreamKey(\"app2\", \"camera\")})\n\n\tok, got := manager.GetGroup(StreamKeyFromStreamName(\"camera\"))\n\tif ok || got != nil {\n\t\tt.Fatal(\"expected ambiguous streamName-only lookup to fail\")\n\t}\n}\n\nfunc TestGroupManagerGetOrCreateGroupReturnsExisting(t *testing.T) {\n\tmanager := NewComplexGroupManager()\n\tkey := NewStreamKey(\"live\", \"camera\")\n\n\tgroup, created := manager.GetOrCreateGroup(key, \"first\", nil, 1, 0)\n\tif !created || group == nil {\n\t\tt.Fatal(\"expected group to be created\")\n\t}\n\n\tgot, created := manager.GetOrCreateGroup(key, \"second\", nil, 1, 0)\n\tif created || got != group {\n\t\tt.Fatal(\"expected existing group to be returned\")\n\t}\n\tif got.UniqueKey() != \"first\" {\n\t\tt.Fatalf(\"unique key = %s, want first\", got.UniqueKey())\n\t}\n}\n\nfunc TestGroupManagerGetOrCreateWaitsForClosedGroupCleanup(t *testing.T) {\n\tmanager := NewComplexGroupManager()\n\tkey := StreamKeyFromStreamName(\"camera\")\n\toldGroup := &Group{key: key}\n\toldGroup.closed.Store(true)\n\tmanager.setGroup(key, oldGroup)\n\n\toldGroup.lifecycleMux.Lock()\n\tdone := make(chan struct {\n\t\tgroup   *Group\n\t\tcreated bool\n\t})\n\tgo func() {\n\t\tgroup, created := manager.GetOrCreateGroup(key, \"new\", nil, 1, 0)\n\t\tdone <- struct {\n\t\t\tgroup   *Group\n\t\t\tcreated bool\n\t\t}{group: group, created: created}\n\t}()\n\n\tselect {\n\tcase <-done:\n\t\tt.Fatal(\"new group should wait for old group cleanup\")\n\tcase <-time.After(50 * time.Millisecond):\n\t}\n\n\toldGroup.lifecycleMux.Unlock()\n\n\tselect {\n\tcase result := <-done:\n\t\tif !result.created || result.group == nil || result.group == oldGroup {\n\t\t\tt.Fatalf(\"unexpected group result: group=%p created=%v\", result.group, result.created)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"new group was not created after old group cleanup\")\n\t}\n}\n\nfunc TestGroupManagerGetOrCreateReturnsReplacementAfterWaitingClosedGroup(t *testing.T) {\n\tmanager := NewComplexGroupManager()\n\tkey := StreamKeyFromStreamName(\"camera\")\n\toldGroup := &Group{key: key}\n\treplacement := &Group{key: key}\n\toldGroup.closed.Store(true)\n\tmanager.setGroup(key, oldGroup)\n\n\toldGroup.lifecycleMux.Lock()\n\tdone := make(chan struct {\n\t\tgroup   *Group\n\t\tcreated bool\n\t})\n\tgo func() {\n\t\tgroup, created := manager.GetOrCreateGroup(key, \"new\", nil, 1, 0)\n\t\tdone <- struct {\n\t\t\tgroup   *Group\n\t\t\tcreated bool\n\t\t}{group: group, created: created}\n\t}()\n\n\ttime.Sleep(50 * time.Millisecond)\n\tmanager.setGroup(key, replacement)\n\toldGroup.lifecycleMux.Unlock()\n\n\tselect {\n\tcase result := <-done:\n\t\tif result.created || result.group != replacement {\n\t\t\tt.Fatalf(\"unexpected group result: group=%p replacement=%p created=%v\", result.group, replacement, result.created)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"replacement group was not returned after old group cleanup\")\n\t}\n}\n\nfunc TestGroupManagerRemoveGroupIfMatchDoesNotRemoveNewGroup(t *testing.T) {\n\tmanager := NewComplexGroupManager()\n\tkey := StreamKeyFromStreamName(\"camera\")\n\toldGroup := &Group{key: key}\n\tnewGroup := &Group{key: key}\n\n\tmanager.setGroup(key, oldGroup)\n\tmanager.setGroup(key, newGroup)\n\tmanager.RemoveGroupIfMatch(key, oldGroup)\n\n\tok, got := manager.GetGroup(key)\n\tif !ok || got != newGroup {\n\t\tt.Fatal(\"old group stop should not remove new group\")\n\t}\n}\n\nfunc TestGroupManagerIterateRemoveDoesNotRemoveReplacement(t *testing.T) {\n\tmanager := NewComplexGroupManager()\n\tkey := StreamKeyFromStreamName(\"camera\")\n\toldGroup := &Group{key: key}\n\tnewGroup := &Group{key: key}\n\n\tmanager.setGroup(key, oldGroup)\n\tmanager.Iterate(func(iterKey StreamKey, group *Group) bool {\n\t\tif iterKey != key || group != oldGroup {\n\t\t\tt.Fatalf(\"unexpected iterate entry, key=%v group=%p\", iterKey, group)\n\t\t}\n\t\tmanager.setGroup(key, newGroup)\n\t\treturn false\n\t})\n\n\tok, got := manager.GetGroup(key)\n\tif !ok || got != newGroup {\n\t\tt.Fatal(\"iterate removal should not remove a replacement group\")\n\t}\n}\n\nfunc TestGopCacheClearReleasesStaleGopPayloads(t *testing.T) {\n\tcache := NewGopCache(1, 0)\n\n\tcache.Feed(videoKeyNalu(1))\n\tcache.Feed(videoInterNalu(2))\n\tcache.Clear()\n\n\tif cache.GetGopCount() != 0 {\n\t\tt.Fatalf(\"gop count = %d, want 0\", cache.GetGopCount())\n\t}\n\tfor i, gop := range cache.data {\n\t\tif gop.data != nil {\n\t\t\tt.Fatalf(\"gop %d data was not released\", i)\n\t\t}\n\t}\n}\n\nfunc TestGopCacheNegativeFrameLimitMeansUnlimited(t *testing.T) {\n\tcache := NewGopCache(1, -1)\n\n\tcache.Feed(videoKeyNalu(1))\n\tcache.Feed(videoInterNalu(2))\n\n\tmsgs := cache.GetGopDataAt(0)\n\tif len(msgs) != 2 {\n\t\tt.Fatalf(\"cached messages = %d, want 2\", len(msgs))\n\t}\n}\n\nfunc TestOnStopIsIdempotentAndClosesSubscribers(t *testing.T) {\n\tgroup := newTestGroup(\"test-stop\")\n\tdefer GetGroupManagerInstance().RemoveGroupByStreamName(\"test-stop\")\n\n\tsub := &recordSubscriber{}\n\tgroup.AddConsumer(\"consumer\", sub)\n\n\tgroup.OnStop()\n\tgroup.OnStop()\n\n\tif sub.stopCountValue() != 1 {\n\t\tt.Fatalf(\"stop count = %d, want 1\", sub.stopCountValue())\n\t}\n\n\tgroup.OnMsg(videoKeyNalu(1))\n\tif sub.len() != 0 {\n\t\tt.Fatalf(\"expected no messages after stop, got %d\", sub.len())\n\t}\n}\n\nfunc TestOnMsgTriggersActiveHookOnceOnFirstMediaPacket(t *testing.T) {\n\tgroup := newTestGroup(\"test-active-hook\")\n\tdefer GetGroupManagerInstance().RemoveGroupByStreamName(\"test-active-hook\")\n\n\tkey := StreamKeyFromStreamName(\"test-active-hook\")\n\tvar got []StreamKey\n\tgroup.BindActiveHook(key, func(k StreamKey) {\n\t\tgot = append(got, k)\n\t})\n\n\tgroup.OnMsg(videoSeqHeader(1))\n\tgroup.OnMsg(aacSeqHeader(2))\n\tif len(got) != 0 {\n\t\tt.Fatalf(\"active hook count after seq header = %d, want 0\", len(got))\n\t}\n\tgroup.OnMsg(videoKeyNalu(3))\n\tgroup.OnMsg(aacRaw(4))\n\n\tif len(got) != 1 {\n\t\tt.Fatalf(\"active hook count = %d, want 1\", len(got))\n\t}\n\tif got[0] != key {\n\t\tt.Fatalf(\"active hook key = %+v, want %+v\", got[0], key)\n\t}\n}\n\nfunc TestAddSubscriberAfterStopIsIgnored(t *testing.T) {\n\tgroup := newTestGroup(\"test-add-after-stop\")\n\tdefer GetGroupManagerInstance().RemoveGroupByStreamName(\"test-add-after-stop\")\n\n\tgroup.OnStop()\n\n\tsub := &recordSubscriber{}\n\tgroup.AddConsumer(\"consumer\", sub)\n\tgroup.OnMsg(videoKeyNalu(1))\n\n\tif sub.len() != 0 {\n\t\tt.Fatalf(\"expected no messages after adding to stopped group, got %d\", sub.len())\n\t}\n\tif len(group.StatSubscribers()) != 0 {\n\t\tt.Fatalf(\"expected no subscribers after adding to stopped group, got %d\", len(group.StatSubscribers()))\n\t}\n}\n\nfunc TestDuplicateSubscriberIDIsIgnored(t *testing.T) {\n\tgroup := newTestGroup(\"test-duplicate\")\n\tdefer GetGroupManagerInstance().RemoveGroupByStreamName(\"test-duplicate\")\n\n\tfirst := &recordSubscriber{}\n\tsecond := &recordSubscriber{}\n\tgroup.AddConsumer(\"consumer\", first)\n\tgroup.AddConsumer(\"consumer\", second)\n\n\tgroup.OnMsg(videoKeyNalu(1))\n\n\tif first.len() != 1 {\n\t\tt.Fatalf(\"first subscriber messages = %d, want 1\", first.len())\n\t}\n\tif second.len() != 0 {\n\t\tt.Fatalf(\"duplicate subscriber messages = %d, want 0\", second.len())\n\t}\n}\n\nfunc TestStatSubscribersRefreshRuntimeStats(t *testing.T) {\n\tgroup := newTestGroup(\"test-stat-refresh\")\n\tdefer GetGroupManagerInstance().RemoveGroupByStreamName(\"test-stat-refresh\")\n\n\tsub := &statSubscriber{}\n\tsub.setStat(SubscriberStat{\n\t\tRemoteAddr:    \"127.0.0.1:9000\",\n\t\tReadBytesSum:  1024,\n\t\tWroteBytesSum: 2048,\n\t})\n\tgroup.AddSubscriber(SubscriberInfo{\n\t\tSubscriberID: \"stat-sub\",\n\t\tProtocol:     SubscriberProtocolWHEP,\n\t}, sub)\n\n\tstate := testSubscriberState(t, group, \"stat-sub\")\n\tstate.UpdateStat(2)\n\n\tsubs := group.StatSubscribers()\n\tif len(subs) != 1 {\n\t\tt.Fatalf(\"subscriber count = %d, want 1\", len(subs))\n\t}\n\n\tstat := subs[0]\n\tif stat.RemoteAddr != \"127.0.0.1:9000\" {\n\t\tt.Fatalf(\"remote addr = %s, want 127.0.0.1:9000\", stat.RemoteAddr)\n\t}\n\tif stat.ReadBytesSum != 1024 {\n\t\tt.Fatalf(\"read bytes = %d, want 1024\", stat.ReadBytesSum)\n\t}\n\tif stat.WroteBytesSum != 2048 {\n\t\tt.Fatalf(\"wrote bytes = %d, want 2048\", stat.WroteBytesSum)\n\t}\n\tif stat.ReadBitrateKbits != 4 {\n\t\tt.Fatalf(\"read bitrate = %d, want 4\", stat.ReadBitrateKbits)\n\t}\n\tif stat.WriteBitrateKbits != 8 {\n\t\tt.Fatalf(\"write bitrate = %d, want 8\", stat.WriteBitrateKbits)\n\t}\n\tif stat.BitrateKbits != 8 {\n\t\tt.Fatalf(\"bitrate = %d, want 8\", stat.BitrateKbits)\n\t}\n\n\tsub.setStat(SubscriberStat{\n\t\tRemoteAddr:    \"127.0.0.1:9001\",\n\t\tReadBytesSum:  1536,\n\t\tWroteBytesSum: 3072,\n\t})\n\tstate.UpdateStat(1)\n\n\tsubs = group.StatSubscribers()\n\tstat = subs[0]\n\tif stat.RemoteAddr != \"127.0.0.1:9001\" {\n\t\tt.Fatalf(\"remote addr = %s, want 127.0.0.1:9001\", stat.RemoteAddr)\n\t}\n\tif stat.ReadBitrateKbits != 4 {\n\t\tt.Fatalf(\"read bitrate after increment = %d, want 4\", stat.ReadBitrateKbits)\n\t}\n\tif stat.WriteBitrateKbits != 8 {\n\t\tt.Fatalf(\"write bitrate after increment = %d, want 8\", stat.WriteBitrateKbits)\n\t}\n}\n"
  },
  {
    "path": "logic/stat_aggregator.go",
    "content": "package logic\n\nimport \"github.com/q191201771/lal/pkg/base\"\n\n// StatAggregator merges lal native group state with lalmax extension subscribers.\ntype StatAggregator struct {\n\tgroupManager IGroupManager\n}\n\ntype StatGroupView struct {\n\tGroup   base.StatGroup\n\tExtSubs []base.StatSub\n}\n\nfunc NewStatAggregator(groupManager IGroupManager) *StatAggregator {\n\tif groupManager == nil {\n\t\tgroupManager = GetGroupManagerInstance()\n\t}\n\treturn &StatAggregator{groupManager: groupManager}\n}\n\nfunc (a *StatAggregator) ExtSubscribers(key StreamKey) []base.StatSub {\n\tif a == nil || a.groupManager == nil || !key.Valid() {\n\t\treturn nil\n\t}\n\n\texist, extGroup := a.groupManager.GetGroup(key)\n\tif !exist || extGroup == nil {\n\t\treturn nil\n\t}\n\n\textSubs := extGroup.StatSubscribers()\n\tif len(extSubs) == 0 {\n\t\treturn nil\n\t}\n\n\tout := make([]base.StatSub, len(extSubs))\n\tcopy(out, extSubs)\n\treturn out\n}\n\nfunc (a *StatAggregator) BuildGroupView(group base.StatGroup) StatGroupView {\n\textSubs := a.ExtSubscribers(NewStreamKey(group.AppName, group.StreamName))\n\tif len(extSubs) != 0 {\n\t\tgroup.StatSubs = append(group.StatSubs, extSubs...)\n\t} else {\n\t\textSubs = make([]base.StatSub, 0)\n\t}\n\n\treturn StatGroupView{\n\t\tGroup:   group,\n\t\tExtSubs: extSubs,\n\t}\n}\n\nfunc (a *StatAggregator) BuildGroupsView(groups []base.StatGroup) []StatGroupView {\n\tif len(groups) == 0 {\n\t\treturn nil\n\t}\n\n\tout := make([]StatGroupView, len(groups))\n\tfor i, group := range groups {\n\t\tout[i] = a.BuildGroupView(group)\n\t}\n\treturn out\n}\n\nfunc (a *StatAggregator) MergeGroup(group base.StatGroup) base.StatGroup {\n\treturn a.BuildGroupView(group).Group\n}\n\nfunc (a *StatAggregator) MergeGroups(groups []base.StatGroup) []base.StatGroup {\n\tif len(groups) == 0 {\n\t\treturn groups\n\t}\n\n\tout := make([]base.StatGroup, len(groups))\n\tfor i, group := range groups {\n\t\tout[i] = a.MergeGroup(group)\n\t}\n\treturn out\n}\n\nfunc (a *StatAggregator) FindGroupView(groups []base.StatGroup, key StreamKey) *StatGroupView {\n\tif !key.Valid() {\n\t\treturn nil\n\t}\n\n\tvar matched *StatGroupView\n\tfor i := range groups {\n\t\tgroup := groups[i]\n\t\tif group.StreamName != key.StreamName {\n\t\t\tcontinue\n\t\t}\n\n\t\tif key.AppName != \"\" {\n\t\t\tif group.AppName != key.AppName {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tview := a.BuildGroupView(group)\n\t\t\treturn &view\n\t\t}\n\n\t\tif matched != nil {\n\t\t\treturn nil\n\t\t}\n\t\tview := a.BuildGroupView(group)\n\t\tmatched = &view\n\t}\n\n\treturn matched\n}\n\nfunc (a *StatAggregator) FindGroup(groups []base.StatGroup, key StreamKey) *base.StatGroup {\n\tview := a.FindGroupView(groups, key)\n\tif view == nil {\n\t\treturn nil\n\t}\n\treturn &view.Group\n}\n"
  },
  {
    "path": "logic/stream_key.go",
    "content": "package logic\n\ntype StreamKey struct {\n\t// AppName 为空表示兼容历史的 streamName 单键查找。\n\tAppName    string\n\tStreamName string\n}\n\nfunc NewStreamKey(appName, streamName string) StreamKey {\n\treturn StreamKey{\n\t\tAppName:    appName,\n\t\tStreamName: streamName,\n\t}\n}\n\nfunc StreamKeyFromStreamName(streamName string) StreamKey {\n\treturn NewStreamKey(\"\", streamName)\n}\n\nfunc (key StreamKey) Valid() bool {\n\treturn key.StreamName != \"\"\n}\n\nfunc (key StreamKey) String() string {\n\tif key.AppName == \"\" {\n\t\treturn key.StreamName\n\t}\n\treturn key.AppName + \"/\" + key.StreamName\n}\n"
  },
  {
    "path": "logic/subscriber_stat.go",
    "content": "package logic\n\n// SubscriberStat is the runtime traffic snapshot for a lalmax external subscriber.\ntype SubscriberStat struct {\n\tRemoteAddr    string\n\tReadBytesSum  uint64\n\tWroteBytesSum uint64\n}\n\n// SubscriberStatProvider exposes runtime traffic stats for ext_subs sessions.\ntype SubscriberStatProvider interface {\n\tGetSubscriberStat() SubscriberStat\n}\n"
  },
  {
    "path": "main.go",
    "content": "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/q191201771/naza/pkg/nazalog\"\n\n\t\"github.com/q191201771/lal/pkg/base\"\n\n\tconfig \"github.com/q191201771/lalmax/config\"\n\n\t\"github.com/q191201771/naza/pkg/bininfo\"\n)\n\nfunc main() {\n\tdefer nazalog.Sync()\n\n\tconfFilename := parseFlag()\n\terr := config.Open(confFilename)\n\tif err != nil {\n\t\tnazalog.Errorf(\"open config failed, configname:%+v\", confFilename)\n\t\treturn\n\t}\n\n\tmaxConf := config.GetConfig()\n\tmaxConf.ConfFilePath = confFilename\n\n\tsvr, err := server.NewLalMaxServer(maxConf)\n\tif err != nil {\n\t\tnazalog.Fatalf(\"create lalmax server failed. err=%+v\", err)\n\t}\n\n\tif err = svr.Run(); err != nil {\n\t\tnazalog.Infof(\"server manager done. err=%+v\", err)\n\t}\n}\n\nfunc parseFlag() string {\n\tbinInfoFlag := flag.Bool(\"v\", false, \"show bin info\")\n\tcf := flag.String(\"c\", \"\", \"specify conf file\")\n\tp := flag.String(\"p\", \"\", \"specify current work directory\")\n\tflag.Parse()\n\n\tif *binInfoFlag {\n\t\t_, _ = fmt.Fprint(os.Stderr, bininfo.StringifyMultiLine())\n\t\t_, _ = fmt.Fprintln(os.Stderr, base.LalFullInfo)\n\t\tos.Exit(0)\n\t}\n\tif *p != \"\" {\n\t\tos.Chdir(*p)\n\t}\n\tif *cf != \"\" {\n\t\treturn *cf\n\t}\n\tnazalog.Warnf(\"config file did not specify in the command line, try to load it in the usual path.\")\n\tdefaultConfigFileList := []string{\n\t\tfilepath.FromSlash(\"lalmax.conf.json\"),\n\t\tfilepath.FromSlash(\"./conf/lalmax.conf.json\"),\n\t\tfilepath.FromSlash(\"../conf/lalmax.conf.json\"),\n\t}\n\tfor _, dcf := range defaultConfigFileList {\n\t\tfi, err := os.Stat(dcf)\n\t\tif err == nil && fi.Size() > 0 && !fi.IsDir() {\n\t\t\tnazalog.Warnf(\"%s exist. using it as config file.\", dcf)\n\t\t\treturn dcf\n\t\t} else {\n\t\t\tnazalog.Warnf(\"%s not exist.\", dcf)\n\t\t}\n\t}\n\n\t// 默认位置都没有，退出程序\n\tflag.Usage()\n\t_, _ = fmt.Fprintf(os.Stderr, `\n\t\t\t\t\t\tExample:\n\t\t\t\t\t\t  %s -c %s\n\t\t\t\t\t\t`, os.Args[0], filepath.FromSlash(\"./conf/lalmax.conf.json\"))\n\tbase.OsExitAndWaitPressIfWindows(1)\n\treturn *cf\n}\n"
  },
  {
    "path": "rtc/jessibucasession.go",
    "content": "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\"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/lal/pkg/httpflv\"\n\t\"github.com/q191201771/lal/pkg/logic\"\n\t\"github.com/q191201771/lal/pkg/remux\"\n\tmaxlogic \"github.com/q191201771/lalmax/logic\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n\t\"github.com/smallnest/chanx\"\n)\n\ntype jessibucaSession struct {\n\tgroup        *maxlogic.Group\n\tpc           *peerConnection\n\tsubscriberId string\n\tlalServer    logic.ILalServer\n\tvideoTrack   *webrtc.TrackLocalStaticRTP\n\taudioTrack   *webrtc.TrackLocalStaticRTP\n\tvideopacker  *Packer\n\taudiopacker  *Packer\n\tmsgChan      *chanx.UnboundedChan[base.RtmpMsg]\n\tcloseChan    chan bool\n\tremoteSafari bool\n\tDC           *webrtc.DataChannel\n\tstreamId     string\n\tcancel       context.CancelFunc\n\tstopOne      sync.Once\n\twroteBytes   atomic.Uint64\n\tremoteAddr   atomic.Value\n}\n\nfunc NewJessibucaSession(appName, streamid string, writeChanSize int, pc *peerConnection, lalServer logic.ILalServer) *jessibucaSession {\n\tok, group := maxlogic.GetGroupManagerInstance().GetGroup(maxlogic.NewStreamKey(appName, streamid))\n\tif !ok {\n\t\tnazalog.Errorf(\"not found stream, appName:%s, streamid:%s\", appName, streamid)\n\t\treturn nil\n\t}\n\n\tu, _ := uuid.NewV4()\n\tctx, cancel := context.WithCancel(context.Background())\n\treturn &jessibucaSession{\n\t\tgroup:        group,\n\t\tpc:           pc,\n\t\tlalServer:    lalServer,\n\t\tsubscriberId: u.String(),\n\t\tstreamId:     streamid,\n\t\tcancel:       cancel,\n\t\tmsgChan:      chanx.NewUnboundedChan[base.RtmpMsg](ctx, writeChanSize),\n\t\tcloseChan:    make(chan bool, 1),\n\t}\n}\nfunc (conn *jessibucaSession) createDataChannel() (err error) {\n\tif conn.DC != nil {\n\t\treturn nil\n\t}\n\tconn.DC, err = conn.pc.CreateDataChannel(conn.streamId, nil)\n\treturn\n}\nfunc (conn *jessibucaSession) GetAnswerSDP(offer string) (sdp string) {\n\tvar err error\n\terr = conn.createDataChannel()\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn\n\t}\n\n\tgatherComplete := webrtc.GatheringCompletePromise(conn.pc.PeerConnection)\n\n\tconn.pc.SetRemoteDescription(webrtc.SessionDescription{\n\t\tType: webrtc.SDPTypeOffer,\n\t\tSDP:  string(offer),\n\t})\n\n\tanswer, err := conn.pc.CreateAnswer(nil)\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn\n\t}\n\n\terr = conn.pc.SetLocalDescription(answer)\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn\n\t}\n\n\t<-gatherComplete\n\n\tsdp = conn.pc.LocalDescription().SDP\n\treturn\n}\n\nfunc (conn *jessibucaSession) Run() {\n\tok, _ := maxlogic.GetGroupManagerInstance().GetGroup(conn.group.Key())\n\tif ok {\n\t\tconn.group.AddSubscriber(maxlogic.SubscriberInfo{\n\t\t\tSubscriberID: conn.subscriberId,\n\t\t\tProtocol:     maxlogic.SubscriberProtocolJessibuca,\n\t\t}, conn)\n\n\t\tconn.pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {\n\t\t\tnazalog.Info(\"peer connection state: \", state.String())\n\n\t\t\tswitch state {\n\t\t\tcase webrtc.PeerConnectionStateConnected:\n\t\t\tcase webrtc.PeerConnectionStateDisconnected:\n\t\t\t\tfallthrough\n\t\t\tcase webrtc.PeerConnectionStateFailed:\n\t\t\t\tfallthrough\n\t\t\tcase webrtc.PeerConnectionStateClosed:\n\t\t\t\tconn.closeChan <- true\n\t\t\t}\n\t\t})\n\t\tif conn.DC != nil {\n\t\t\tconn.DC.OnOpen(func() {\n\t\t\t\tif err := conn.DC.Send(httpflv.FlvHeader); err != nil {\n\t\t\t\t\tnazalog.Warnf(\" stream write videoHeader err:%s\", err.Error())\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconn.wroteBytes.Add(uint64(len(httpflv.FlvHeader)))\n\t\t\t\tconn.refreshRemoteAddr()\n\n\t\t\t\tdefer func() {\n\t\t\t\t\tnazalog.Info(\"RemoveConsumer, connid:\", conn.subscriberId)\n\t\t\t\t\tconn.group.RemoveSubscriber(conn.subscriberId)\n\t\t\t\t\tconn.DC.Close()\n\t\t\t\t\tconn.pc.Close()\n\t\t\t\t\tconn.DC = nil\n\t\t\t\t\tconn.cancel()\n\t\t\t\t}()\n\t\t\t\tfor {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase msg := <-conn.msgChan.Out:\n\t\t\t\t\t\tlazyRtmpMsg2FlvTag := remux.LazyRtmpMsg2FlvTag{}\n\t\t\t\t\t\tlazyRtmpMsg2FlvTag.Init(msg)\n\t\t\t\t\t\tbuf := lazyRtmpMsg2FlvTag.GetEnsureWithoutSdf()\n\t\t\t\t\t\tsendBuf := chunkSlice(buf, math.MaxUint16)\n\t\t\t\t\t\tfor _, v := range sendBuf {\n\t\t\t\t\t\t\tif err := conn.DC.Send(v); err != nil {\n\t\t\t\t\t\t\t\tnazalog.Warnf(\" stream write msg err:%s\", err.Error())\n\t\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconn.wroteBytes.Add(uint64(len(v)))\n\t\t\t\t\t\t}\n\n\t\t\t\t\tcase <-conn.closeChan:\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t})\n\t\t}\n\t}\n\n}\n\nfunc chunkSlice(slice []byte, size int) [][]byte {\n\tvar chunks [][]byte\n\n\tfor i := 0; i < len(slice); i += size {\n\t\tend := i + size\n\n\t\tif end > len(slice) {\n\t\t\tend = len(slice)\n\t\t}\n\n\t\tchunks = append(chunks, slice[i:end])\n\t}\n\n\treturn chunks\n}\n\nfunc (conn *jessibucaSession) OnMsg(msg base.RtmpMsg) {\n\tswitch msg.Header.MsgTypeId {\n\tcase base.RtmpTypeIdMetadata:\n\t\treturn\n\tcase base.RtmpTypeIdAudio:\n\t\tif conn.DC != nil {\n\t\t\tconn.msgChan.In <- msg\n\t\t}\n\tcase base.RtmpTypeIdVideo:\n\t\tif conn.DC != nil {\n\t\t\tconn.msgChan.In <- msg\n\t\t}\n\t}\n}\n\nfunc (conn *jessibucaSession) OnStop() {\n\tconn.stopOne.Do(func() {\n\t\tconn.closeChan <- true\n\t})\n}\n\nfunc (conn *jessibucaSession) Close() {\n\tif conn.DC != nil {\n\t\tconn.DC.Close()\n\t}\n\tif conn.pc != nil {\n\t\tconn.pc.Close()\n\t}\n}\n\nfunc (conn *jessibucaSession) GetSubscriberStat() maxlogic.SubscriberStat {\n\tconn.refreshRemoteAddr()\n\treturn maxlogic.SubscriberStat{\n\t\tRemoteAddr:    conn.loadRemoteAddr(),\n\t\tWroteBytesSum: conn.wroteBytes.Load(),\n\t}\n}\n\nfunc (conn *jessibucaSession) refreshRemoteAddr() {\n\tif remoteAddr := conn.currentRemoteAddr(); remoteAddr != \"\" {\n\t\tconn.remoteAddr.Store(remoteAddr)\n\t}\n}\n\nfunc (conn *jessibucaSession) currentRemoteAddr() string {\n\tif conn.DC != nil && conn.DC.Transport() != nil {\n\t\tif dtls := conn.DC.Transport().Transport(); dtls != nil {\n\t\t\tif remoteAddr := remoteAddrFromDTLSTransport(dtls); remoteAddr != \"\" {\n\t\t\t\treturn remoteAddr\n\t\t\t}\n\t\t}\n\t}\n\tif sctp := conn.pc.SCTP(); sctp != nil {\n\t\treturn remoteAddrFromDTLSTransport(sctp.Transport())\n\t}\n\treturn \"\"\n}\n\nfunc (conn *jessibucaSession) loadRemoteAddr() string {\n\tv := conn.remoteAddr.Load()\n\taddr, _ := v.(string)\n\treturn addr\n}\n"
  },
  {
    "path": "rtc/packer.go",
    "content": "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/pkg/base\"\n\t\"github.com/q191201771/lal/pkg/hevc\"\n\t\"github.com/q191201771/lal/pkg/rtprtcp\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\nconst (\n\tPacketH264 = \"H264\"\n\tPacketHEVC = \"HEVC\"\n\tPacketPCMA = \"PCMA\"\n\tPacketPCMU = \"PCMU\"\n\tPacketOPUS = \"OPUS\"\n)\n\ntype Packer struct {\n\tenc IRtpEncoder\n}\n\nfunc NewPacker(mimeType string, codec []byte) *Packer {\n\tp := &Packer{}\n\n\tswitch mimeType {\n\tcase PacketH264:\n\t\tp.enc = NewH264RtpEncoder(codec)\n\tcase PacketPCMA:\n\t\tp.enc = NewG711RtpEncoder(8)\n\tcase PacketPCMU:\n\t\tp.enc = NewG711RtpEncoder(0)\n\tcase PacketHEVC:\n\t\tp.enc = NewHevcRtpEncoder(codec)\n\tcase PacketOPUS:\n\t\tp.enc = NewOpusRtpEncoder(111)\n\t}\n\treturn p\n}\n\nfunc (p *Packer) Encode(msg base.RtmpMsg) ([]*rtp.Packet, error) {\n\tif p == nil || p.enc == nil {\n\t\treturn nil, fmt.Errorf(\"packer encoder is nil\")\n\t}\n\treturn p.enc.Encode(msg)\n}\n\nfunc (p *Packer) UpdateVideoCodec(vps, sps, pps []byte) {\n\tif p == nil || p.enc == nil {\n\t\treturn\n\t}\n\n\tif h264Encoder, ok := p.enc.(*H264RtpEncoder); ok {\n\t\th264Encoder.UpdateVideoCodec(vps, sps, pps)\n\t\treturn\n\t}\n\n\tif hevcEncoder, ok := p.enc.(*HevcRtpEncoder); ok {\n\t\thevcEncoder.UpdateVideoCodec(vps, sps, pps)\n\t}\n}\n\ntype IRtpEncoder interface {\n\tEncode(msg base.RtmpMsg) ([]*rtp.Packet, error)\n}\n\ntype H264RtpEncoder struct {\n\tIRtpEncoder\n\tsps       []byte\n\tpps       []byte\n\trtpPacker *rtprtcp.RtpPacker\n}\n\nfunc NewH264RtpEncoder(codec []byte) *H264RtpEncoder {\n\tsps, pps, err := avc.ParseSpsPpsFromSeqHeader(codec)\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn nil\n\t}\n\n\tpp := rtprtcp.NewRtpPackerPayloadAvc(func(option *rtprtcp.RtpPackerPayloadAvcHevcOption) {\n\t\toption.Typ = rtprtcp.RtpPackerPayloadAvcHevcTypeAnnexb\n\t})\n\n\treturn &H264RtpEncoder{\n\t\tsps:       sps,\n\t\tpps:       pps,\n\t\trtpPacker: rtprtcp.NewRtpPacker(pp, 90000, 0),\n\t}\n}\n\nfunc (enc *H264RtpEncoder) Encode(msg base.RtmpMsg) ([]*rtp.Packet, error) {\n\tvar out []byte\n\terr := avc.IterateNaluAvcc(msg.Payload[5:], func(nal []byte) {\n\t\tt := avc.ParseNaluType(nal[0])\n\t\tif t == avc.NaluTypeSei {\n\t\t\treturn\n\t\t}\n\n\t\tif t == avc.NaluTypeIdrSlice {\n\t\t\tout = append(out, avc.NaluStartCode3...)\n\t\t\tout = append(out, enc.sps...)\n\t\t\tout = append(out, avc.NaluStartCode3...)\n\t\t\tout = append(out, enc.pps...)\n\t\t}\n\n\t\tout = append(out, avc.NaluStartCode3...)\n\t\tout = append(out, nal...)\n\t})\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Packetize failed\")\n\t}\n\n\tif len(out) == 0 {\n\t\treturn nil, fmt.Errorf(\"Packetize failed\")\n\t}\n\n\tavpacket := base.AvPacket{\n\t\tTimestamp: int64(msg.Dts()),\n\t\tPayload:   out,\n\t}\n\n\tvar pkts []*rtp.Packet\n\trtpPkts := enc.rtpPacker.Pack(avpacket)\n\tfor _, pkt := range rtpPkts {\n\t\tvar newRtpPkt rtp.Packet\n\t\terr := newRtpPkt.Unmarshal(pkt.Raw)\n\t\tif err != nil {\n\t\t\tnazalog.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tpkts = append(pkts, &newRtpPkt)\n\t}\n\n\tif len(pkts) == 0 {\n\t\treturn nil, fmt.Errorf(\"Packetize failed\")\n\t}\n\n\treturn pkts, nil\n}\n\nfunc (enc *H264RtpEncoder) UpdateVideoCodec(_ []byte, sps, pps []byte) {\n\tenc.sps = sps\n\tenc.pps = pps\n}\n\ntype G711RtpEncoder struct {\n\tIRtpEncoder\n\trtpPacker *rtprtcp.RtpPacker\n}\n\nfunc NewG711RtpEncoder(pt uint8) *G711RtpEncoder {\n\t// TODO 暂时采样率设置为8000\n\tpp := rtprtcp.NewRtpPackerPayloadPcm()\n\n\treturn &G711RtpEncoder{\n\t\trtpPacker: rtprtcp.NewRtpPacker(pp, 8000, 0),\n\t}\n}\n\nfunc (enc *G711RtpEncoder) Encode(msg base.RtmpMsg) ([]*rtp.Packet, error) {\n\tavpacket := base.AvPacket{\n\t\tTimestamp: int64(msg.Dts()),\n\t\tPayload:   msg.Payload[1:],\n\t}\n\n\tvar pkts []*rtp.Packet\n\trtpPkts := enc.rtpPacker.Pack(avpacket)\n\tfor _, pkt := range rtpPkts {\n\t\tvar newRtpPkt rtp.Packet\n\t\terr := newRtpPkt.Unmarshal(pkt.Raw)\n\t\tif err != nil {\n\t\t\tnazalog.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tpkts = append(pkts, &newRtpPkt)\n\t}\n\n\tif len(pkts) == 0 {\n\t\treturn nil, fmt.Errorf(\"Packetize failed\")\n\t}\n\n\treturn pkts, nil\n}\n\ntype HevcRtpEncoder struct {\n\tIRtpEncoder\n\tvps       []byte\n\tsps       []byte\n\tpps       []byte\n\trtpPacker *rtprtcp.RtpPacker\n}\n\nfunc NewHevcRtpEncoder(codec []byte) *HevcRtpEncoder {\n\tvps, sps, pps, err := hevc.ParseVpsSpsPpsFromSeqHeader(codec)\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn nil\n\t}\n\n\tpp := rtprtcp.NewRtpPackerPayloadHevc(func(option *rtprtcp.RtpPackerPayloadAvcHevcOption) {\n\t\toption.Typ = rtprtcp.RtpPackerPayloadAvcHevcTypeAnnexb\n\t})\n\n\treturn &HevcRtpEncoder{\n\t\tvps:       vps,\n\t\tsps:       sps,\n\t\tpps:       pps,\n\t\trtpPacker: rtprtcp.NewRtpPacker(pp, 90000, 0),\n\t}\n}\n\nfunc (enc *HevcRtpEncoder) Encode(msg base.RtmpMsg) ([]*rtp.Packet, error) {\n\tvar out []byte\n\terr := avc.IterateNaluAvcc(msg.Payload[5:], func(nal []byte) {\n\t\tt := hevc.ParseNaluType(nal[0])\n\t\tif t == hevc.NaluTypeSei || t == hevc.NaluTypeSeiSuffix {\n\t\t\treturn\n\t\t}\n\n\t\tif hevc.IsIrapNalu(t) {\n\t\t\tout = append(out, avc.NaluStartCode3...)\n\t\t\tout = append(out, enc.vps...)\n\t\t\tout = append(out, avc.NaluStartCode3...)\n\t\t\tout = append(out, enc.sps...)\n\t\t\tout = append(out, avc.NaluStartCode3...)\n\t\t\tout = append(out, enc.pps...)\n\t\t}\n\n\t\tout = append(out, avc.NaluStartCode3...)\n\t\tout = append(out, nal...)\n\t})\n\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Packetize failed\")\n\t}\n\n\tif len(out) == 0 {\n\t\treturn nil, fmt.Errorf(\"Packetize failed\")\n\t}\n\n\tavpacket := base.AvPacket{\n\t\tTimestamp: int64(msg.Dts()),\n\t\tPayload:   out,\n\t}\n\n\tvar pkts []*rtp.Packet\n\trtpPkts := enc.rtpPacker.Pack(avpacket)\n\tfor _, pkt := range rtpPkts {\n\t\tvar newRtpPkt rtp.Packet\n\t\terr := newRtpPkt.Unmarshal(pkt.Raw)\n\t\tif err != nil {\n\t\t\tnazalog.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tpkts = append(pkts, &newRtpPkt)\n\t}\n\n\tif len(pkts) == 0 {\n\t\treturn nil, fmt.Errorf(\"Packetize failed\")\n\t}\n\n\treturn pkts, nil\n}\n\nfunc (enc *HevcRtpEncoder) UpdateVideoCodec(vps, sps, pps []byte) {\n\tenc.vps = vps\n\tenc.sps = sps\n\tenc.pps = pps\n}\n\ntype OpusRtpEncoder struct {\n\tIRtpEncoder\n\trtpPacker *rtprtcp.RtpPacker\n}\n\nfunc NewOpusRtpEncoder(pt uint8) *OpusRtpEncoder {\n\tpp := rtprtcp.NewRtpPackerPayloadOpus()\n\n\treturn &OpusRtpEncoder{\n\t\trtpPacker: rtprtcp.NewRtpPacker(pp, 48000, 0),\n\t}\n}\n\nfunc (enc *OpusRtpEncoder) Encode(msg base.RtmpMsg) ([]*rtp.Packet, error) {\n\tavpacket := base.AvPacket{\n\t\tTimestamp: int64(msg.Dts()),\n\t\tPayload:   msg.Payload[1:],\n\t}\n\n\tvar pkts []*rtp.Packet\n\trtpPkts := enc.rtpPacker.Pack(avpacket)\n\tfor _, pkt := range rtpPkts {\n\t\tvar newRtpPkt rtp.Packet\n\t\terr := newRtpPkt.Unmarshal(pkt.Raw)\n\t\tif err != nil {\n\t\t\tnazalog.Error(err)\n\t\t\tcontinue\n\t\t}\n\n\t\tpkts = append(pkts, &newRtpPkt)\n\t}\n\n\tif len(pkts) == 0 {\n\t\treturn nil, fmt.Errorf(\"Packetize failed\")\n\t}\n\n\treturn pkts, nil\n}\n"
  },
  {
    "path": "rtc/peerConnection.go",
    "content": "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/q191201771/naza/pkg/nazalog\"\n)\n\ntype peerConnection struct {\n\t*webrtc.PeerConnection\n}\n\nfunc newPeerConnection(ips []string, iceUDPMux ice.UDPMux, iceTCPMux ice.TCPMux) (conn *peerConnection, err error) {\n\tconfiguration := webrtc.Configuration{}\n\tsettingsEngine := webrtc.SettingEngine{}\n\n\tif len(ips) != 0 {\n\t\tsettingsEngine.SetNAT1To1IPs(ips, webrtc.ICECandidateTypeHost)\n\t} else {\n\t\tconfiguration.ICEServers = []webrtc.ICEServer{\n\t\t\t{\n\t\t\t\tURLs: []string{\"stun:stun.l.google.com:19302\"},\n\t\t\t},\n\t\t}\n\t}\n\n\tif iceUDPMux != nil {\n\t\tsettingsEngine.SetICEUDPMux(iceUDPMux)\n\t}\n\n\tif iceTCPMux != nil {\n\t\tsettingsEngine.SetICETCPMux(iceTCPMux)\n\t\tsettingsEngine.SetNetworkTypes([]webrtc.NetworkType{webrtc.NetworkTypeTCP4})\n\t}\n\n\tmediaEngine := &webrtc.MediaEngine{}\n\terr = mediaEngine.RegisterCodec(\n\t\twebrtc.RTPCodecParameters{\n\t\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:    webrtc.MimeTypeH264,\n\t\t\t\tClockRate:   90000,\n\t\t\t\tSDPFmtpLine: \"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\",\n\t\t\t},\n\t\t\tPayloadType: 96,\n\t\t},\n\t\twebrtc.RTPCodecTypeVideo)\n\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn\n\t}\n\n\terr = mediaEngine.RegisterCodec(\n\t\twebrtc.RTPCodecParameters{\n\t\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:  webrtc.MimeTypeH264,\n\t\t\t\tClockRate: 90000,\n\t\t\t},\n\t\t\tPayloadType: 103,\n\t\t},\n\t\twebrtc.RTPCodecTypeVideo)\n\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn\n\t}\n\n\terr = mediaEngine.RegisterCodec(\n\t\twebrtc.RTPCodecParameters{\n\t\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:  webrtc.MimeTypeH265,\n\t\t\t\tClockRate: 90000,\n\t\t\t},\n\t\t\tPayloadType: 102,\n\t\t},\n\t\twebrtc.RTPCodecTypeVideo)\n\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn\n\t}\n\n\t// opus\n\terr = mediaEngine.RegisterCodec(\n\t\twebrtc.RTPCodecParameters{\n\t\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:  webrtc.MimeTypeOpus,\n\t\t\t\tClockRate: 48000,\n\t\t\t},\n\t\t\tPayloadType: 111,\n\t\t},\n\t\twebrtc.RTPCodecTypeAudio)\n\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn\n\t}\n\n\t// PCMU\n\terr = mediaEngine.RegisterCodec(\n\t\twebrtc.RTPCodecParameters{\n\t\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:  webrtc.MimeTypePCMU,\n\t\t\t\tClockRate: 8000,\n\t\t\t},\n\t\t\tPayloadType: 0,\n\t\t},\n\t\twebrtc.RTPCodecTypeAudio)\n\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn\n\t}\n\n\t// PCMA\n\terr = mediaEngine.RegisterCodec(\n\t\twebrtc.RTPCodecParameters{\n\t\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:  webrtc.MimeTypePCMA,\n\t\t\t\tClockRate: 8000,\n\t\t\t},\n\t\t\tPayloadType: 8,\n\t\t},\n\t\twebrtc.RTPCodecTypeAudio)\n\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn\n\t}\n\n\tinterceptorRegistry := &interceptor.Registry{}\n\tif err := webrtc.RegisterDefaultInterceptors(mediaEngine, interceptorRegistry); err != nil {\n\t\treturn nil, err\n\t}\n\n\tapi := webrtc.NewAPI(\n\t\twebrtc.WithSettingEngine(settingsEngine),\n\t\twebrtc.WithMediaEngine(mediaEngine),\n\t\twebrtc.WithInterceptorRegistry(interceptorRegistry))\n\n\tpc, err := api.NewPeerConnection(configuration)\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn nil, err\n\t}\n\n\tconn = &peerConnection{\n\t\tPeerConnection: pc,\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "rtc/server.go",
    "content": "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.com/q191201771/lalmax/logic\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/pion/ice/v2\"\n\t\"github.com/pion/webrtc/v3\"\n\t\"github.com/q191201771/lal/pkg/logic\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\n// StreamNotFoundFn 流不存在时的回调，触发 on_stream_not_found 通知上层拉流\ntype StreamNotFoundFn func(app, stream, schema string)\n\ntype RtcServer struct {\n\tconfig           config.RtcConfig\n\tlalServer        logic.ILalServer\n\tudpMux           ice.UDPMux\n\ttcpMux           ice.TCPMux\n\tstreamNotFoundFn StreamNotFoundFn\n}\n\n// SetStreamNotFoundFn 注入流不存在回调\nfunc (s *RtcServer) SetStreamNotFoundFn(fn StreamNotFoundFn) {\n\ts.streamNotFoundFn = fn\n}\n\n// waitStreamReady 触发 on_stream_not_found 后轮询等待流就绪\n// 为什么：WebRTC 播放请求先于 GB28181 设备推流到达，需通知上层拉流后等待\nfunc (s *RtcServer) waitStreamReady(appName, streamid, schema string) bool {\n\tkey := maxlogic.NewStreamKey(appName, streamid)\n\tif ok, _ := maxlogic.GetGroupManagerInstance().GetGroup(key); ok {\n\t\treturn true\n\t}\n\n\tif s.streamNotFoundFn != nil {\n\t\tnazalog.Infof(\"stream not found, triggering on_stream_not_found. app=%s, stream=%s\", appName, streamid)\n\t\ts.streamNotFoundFn(appName, streamid, schema)\n\t}\n\n\tok, _ := maxlogic.GetGroupManagerInstance().WaitGroup(key, 500*time.Millisecond, 5*time.Second)\n\treturn ok\n}\n\nfunc NewRtcServer(config config.RtcConfig, lal logic.ILalServer) (*RtcServer, error) {\n\tvar udpMux ice.UDPMux\n\tvar tcpMux ice.TCPMux\n\n\tif config.ICEUDPMuxPort != 0 {\n\t\tvar udplistener *net.UDPConn\n\n\t\tudplistener, err := net.ListenUDP(\"udp\", &net.UDPAddr{\n\t\t\tIP:   net.IP{0, 0, 0, 0},\n\t\t\tPort: config.ICEUDPMuxPort,\n\t\t})\n\n\t\tif err != nil {\n\t\t\tnazalog.Error(err)\n\t\t\treturn nil, err\n\t\t}\n\t\tnazalog.Infof(\"webrtc ice udp listen. port=%d\", config.ICEUDPMuxPort)\n\t\tudpMux = webrtc.NewICEUDPMux(nil, udplistener)\n\t}\n\tif config.WriteChanSize == 0 {\n\t\tconfig.WriteChanSize = 1024\n\t}\n\tif config.ICETCPMuxPort != 0 {\n\t\tvar tcplistener *net.TCPListener\n\n\t\ttcplistener, err := net.ListenTCP(\"tcp\", &net.TCPAddr{\n\t\t\tIP:   net.IP{0, 0, 0, 0},\n\t\t\tPort: config.ICETCPMuxPort,\n\t\t})\n\n\t\tif err != nil {\n\t\t\tnazalog.Error(err)\n\t\t\treturn nil, err\n\t\t}\n\t\tnazalog.Infof(\"webrtc ice tcp listen. port=%d\", config.ICETCPMuxPort)\n\t\ttcpMux = webrtc.NewICETCPMux(nil, tcplistener, 20)\n\t}\n\n\tsvr := &RtcServer{\n\t\tconfig:    config,\n\t\tlalServer: lal,\n\t\tudpMux:    udpMux,\n\t\ttcpMux:    tcpMux,\n\t}\n\n\treturn svr, nil\n}\n\nfunc (s *RtcServer) HandleWHIP(c *gin.Context) {\n\tstreamid := c.Request.URL.Query().Get(\"streamid\")\n\tif streamid == \"\" {\n\t\tc.Status(http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\n\tbody, err := c.GetRawData()\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\tc.Status(http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif len(body) == 0 {\n\t\tnazalog.Error(\"invalid body\")\n\t\tc.Status(http.StatusNoContent)\n\t\treturn\n\t}\n\n\tpc, err := newPeerConnection(s.config.ICEHostNATToIPs, s.udpMux, s.tcpMux)\n\tif err != nil {\n\t\tc.Status(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\twhipsession := NewWhipSession(streamid, pc, s.lalServer)\n\tif whipsession == nil {\n\t\tc.Status(http.StatusInternalServerError)\n\t\tpc.Close()\n\t\treturn\n\t}\n\n\tc.Header(\"Location\", fmt.Sprintf(\"whip/%s\", whipsession.subscriberId))\n\n\tsdp := whipsession.GetAnswerSDP(string(body))\n\tif sdp == \"\" {\n\t\tc.Status(http.StatusInternalServerError)\n\t\twhipsession.Close()\n\t\treturn\n\t}\n\n\tgo whipsession.Run()\n\n\tc.Data(http.StatusCreated, \"application/sdp\", []byte(sdp))\n}\n\n// ServeWHIPPublishPage 返回内嵌推流页：浏览器直接打开 WHIP URL 即可通过 WHIP POST 建立 WebRTC 推流（与 ServeWHEPPlayPage 对称）。\nfunc (s *RtcServer) ServeWHIPPublishPage(c *gin.Context) {\n\tif c.Request.URL.Query().Get(\"streamid\") == \"\" {\n\t\tc.Header(\"Content-Type\", \"text/html; charset=utf-8\")\n\t\tc.String(http.StatusBadRequest, \"<!doctype html><meta charset=utf-8><title>WHIP</title><p>缺少查询参数 <code>streamid</code>。示例：<code>/webrtc/whip?streamid=test110</code></p>\")\n\t\treturn\n\t}\n\tc.Header(\"Cache-Control\", \"no-store\")\n\tc.Header(\"Accept-Post\", \"application/sdp\")\n\tc.Header(\"Access-Control-Allow-Origin\", \"*\")\n\tc.Header(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, OPTIONS\")\n\tc.Header(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization\")\n\tc.Header(\"Access-Control-Expose-Headers\", \"Location\")\n\tc.Data(http.StatusOK, \"text/html; charset=utf-8\", []byte(buildWHIPPublishHTML()))\n}\n\nfunc buildWHIPPublishHTML() string {\n\treturn \"<!doctype html><html><head><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\"><title>WHIP Publisher</title><style>body{margin:0;background:#0f172a;color:#e2e8f0;font:14px/1.4 system-ui}main{max-width:960px;margin:0 auto;padding:24px}video{width:100%;max-height:360px;background:#000;border-radius:12px}pre{white-space:pre-wrap;background:#111827;padding:12px;border-radius:12px;min-height:72px}</style></head><body><main><p>本页使用摄像头/麦克风通过 WHIP 推流（H264+Opus）。请允许浏览器媒体权限。</p><video id=\\\"preview\\\" autoplay muted playsinline></video><pre id=\\\"log\\\">connecting...</pre></main><script>(async()=>{const log=(m)=>{document.getElementById('log').textContent=m};const preview=document.getElementById('preview');try{if(!navigator.mediaDevices||!navigator.mediaDevices.getUserMedia){log('当前环境不支持 getUserMedia');return}const stream=await navigator.mediaDevices.getUserMedia({video:true,audio:true});preview.srcObject=stream;const pc=new RTCPeerConnection();stream.getTracks().forEach(t=>pc.addTrack(t,stream));const offer=await pc.createOffer();await pc.setLocalDescription(offer);await new Promise(r=>{if(pc.iceGatheringState==='complete')return r();pc.addEventListener('icegatheringstatechange',()=>{if(pc.iceGatheringState==='complete')r()})});const res=await fetch(location.href,{method:'POST',headers:{'Content-Type':'application/sdp'},body:pc.localDescription.sdp});if(!res.ok){log('WHIP 失败: '+res.status+' '+await res.text());return}const answer=await res.text();await pc.setRemoteDescription({type:'answer',sdp:answer});log('WHIP 已连接，正在推流: '+location.href)}catch(e){log('错误: '+(e&&e.message?e.message:String(e)))}})();</script></body></html>\"\n}\n\nfunc (s *RtcServer) HandleJessibuca(c *gin.Context) {\n\tstreamid := c.Param(\"streamid\")\n\tif streamid == \"\" {\n\t\tc.Status(http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\tappName := c.Query(\"app_name\")\n\n\tbody, err := c.GetRawData()\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\tc.Status(http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif len(body) == 0 {\n\t\tnazalog.Error(\"invalid body\")\n\t\tc.Status(http.StatusNoContent)\n\t\treturn\n\t}\n\n\tif !s.waitStreamReady(appName, streamid, \"rtsp\") {\n\t\tnazalog.Errorf(\"stream not ready after waiting. app=%s, stream=%s\", appName, streamid)\n\t\tc.Status(http.StatusNotFound)\n\t\treturn\n\t}\n\n\tpc, err := newPeerConnection(s.config.ICEHostNATToIPs, s.udpMux, s.tcpMux)\n\tif err != nil {\n\t\tc.Status(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tjessibucaSession := NewJessibucaSession(appName, streamid, s.config.WriteChanSize, pc, s.lalServer)\n\tif jessibucaSession == nil {\n\t\tc.Status(http.StatusInternalServerError)\n\t\tpc.Close()\n\t\treturn\n\t}\n\n\tc.Header(\"Location\", fmt.Sprintf(\"jessibucaflv/%s\", jessibucaSession.subscriberId))\n\n\tsdp := jessibucaSession.GetAnswerSDP(string(body))\n\tif sdp == \"\" {\n\t\tc.Status(http.StatusInternalServerError)\n\t\tjessibucaSession.Close()\n\t\treturn\n\t}\n\n\tgo jessibucaSession.Run()\n\n\tc.Data(http.StatusCreated, \"application/sdp\", []byte(sdp))\n}\n\n// ServeWHEPPlayPage 返回内嵌播放页（与 topsmedia/pkg/httpflv handleWHEPPage + buildWHEPPage 对齐）。规范地址：GET /webrtc/whep?streamid=...\nfunc (s *RtcServer) ServeWHEPPlayPage(c *gin.Context) {\n\tif c.Request.URL.Query().Get(\"streamid\") == \"\" {\n\t\tc.Header(\"Content-Type\", \"text/html; charset=utf-8\")\n\t\tc.String(http.StatusBadRequest, \"<!doctype html><meta charset=utf-8><title>WHEP</title><p>缺少查询参数 <code>streamid</code>。示例：<code>/webrtc/whep?streamid=test110</code> 或带 <code>app_name</code>：<code>/webrtc/whep?streamid=live/test110&amp;app_name=live</code></p>\")\n\t\treturn\n\t}\n\t// 与 httpflv.handleWHEPPage 响应头一致（Gin 由框架管理 Connection，不设 close）\n\tc.Header(\"Cache-Control\", \"no-store\")\n\tc.Header(\"Access-Control-Allow-Origin\", \"*\")\n\tc.Header(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, OPTIONS\")\n\tc.Header(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization\")\n\tc.Header(\"Access-Control-Expose-Headers\", \"Location\")\n\tc.Header(\"Accept-Post\", \"application/sdp\")\n\tc.Data(http.StatusOK, \"text/html; charset=utf-8\", []byte(buildWHEPPlayHTML()))\n}\n\n// buildWHEPPlayHTML 与 topsmedia/pkg/httpflv buildWHEPPage 内嵌脚本与结构保持一致。\nfunc buildWHEPPlayHTML() string {\n\treturn \"<!doctype html><html><head><meta charset=\\\"utf-8\\\"><meta name=\\\"viewport\\\" content=\\\"width=device-width,initial-scale=1\\\"><title>WHEP Player</title><style>body{margin:0;background:#0f172a;color:#e2e8f0;font:14px/1.4 system-ui}main{max-width:960px;margin:0 auto;padding:24px}video{width:100%;background:#000;border-radius:12px}pre{white-space:pre-wrap;background:#111827;padding:12px;border-radius:12px;min-height:72px}</style></head><body><main><video id=\\\"video\\\" autoplay playsinline controls muted></video><pre id=\\\"log\\\">connecting...</pre></main><script>(async()=>{const log=(m)=>document.getElementById('log').textContent=m;const video=document.getElementById('video');const pc=new RTCPeerConnection();pc.ontrack=(e)=>{video.srcObject=e.streams[0];};pc.addTransceiver('video',{direction:'recvonly'});pc.addTransceiver('audio',{direction:'recvonly'});const offer=await pc.createOffer();await pc.setLocalDescription(offer);await new Promise(r=>{if(pc.iceGatheringState==='complete')return r();pc.addEventListener('icegatheringstatechange',()=>pc.iceGatheringState==='complete'&&r(),{once:false});});const res=await fetch(location.href,{method:'POST',headers:{'Content-Type':'application/sdp'},body:pc.localDescription.sdp});if(!res.ok){log('whep failed: '+res.status+' '+await res.text());return;}const answer=await res.text();await pc.setRemoteDescription({type:'answer',sdp:answer});log('connected: '+location.href);})();</script></body></html>\"\n}\n\nfunc (s *RtcServer) HandleWHEP(c *gin.Context) {\n\tstreamid := c.Request.URL.Query().Get(\"streamid\")\n\tif streamid == \"\" {\n\t\tc.Status(http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\tappName := c.Request.URL.Query().Get(\"app_name\")\n\n\tbody, err := c.GetRawData()\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\tc.Status(http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif len(body) == 0 {\n\t\tnazalog.Error(\"invalid body\")\n\t\tc.Status(http.StatusNoContent)\n\t\treturn\n\t}\n\n\tif !s.waitStreamReady(appName, streamid, \"rtsp\") {\n\t\tnazalog.Errorf(\"stream not ready after waiting. app=%s, stream=%s\", appName, streamid)\n\t\tc.Status(http.StatusNotFound)\n\t\treturn\n\t}\n\n\tpc, err := newPeerConnection(s.config.ICEHostNATToIPs, s.udpMux, s.tcpMux)\n\tif err != nil {\n\t\tc.Status(http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\twhepsession := NewWhepSession(appName, streamid, s.config.WriteChanSize, pc, s.lalServer)\n\tif whepsession == nil {\n\t\tc.Status(http.StatusInternalServerError)\n\t\tpc.Close()\n\t\treturn\n\t}\n\n\tc.Header(\"Location\", fmt.Sprintf(\"whep/%s\", whepsession.subscriberId))\n\n\tsdp := whepsession.GetAnswerSDP(string(body))\n\tif sdp == \"\" {\n\t\tc.Status(http.StatusInternalServerError)\n\t\twhepsession.Close()\n\t\treturn\n\t}\n\n\tgo whepsession.Run()\n\n\tc.Data(http.StatusCreated, \"application/sdp\", []byte(sdp))\n}\n\n// HandleZlmWebrtcPlay ZLM 兼容 WebRTC 播放，返回 SDP answer\n// 为什么独立方法：ZLM 信令格式为 JSON {\"code\":0,\"sdp\":\"...\"}，与 WHEP 纯 SDP 不同\nfunc (s *RtcServer) HandleZlmWebrtcPlay(app, stream, offer string) (string, error) {\n\tif !s.waitStreamReady(app, stream, \"rtsp\") {\n\t\treturn \"\", fmt.Errorf(\"stream not found: %s/%s\", app, stream)\n\t}\n\n\tpc, err := newPeerConnection(s.config.ICEHostNATToIPs, s.udpMux, s.tcpMux)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"create peer connection: %w\", err)\n\t}\n\n\tsession := NewWhepSession(app, stream, s.config.WriteChanSize, pc, s.lalServer)\n\tif session == nil {\n\t\tpc.Close()\n\t\treturn \"\", fmt.Errorf(\"create session failed: %s/%s\", app, stream)\n\t}\n\n\tsdp := session.GetAnswerSDP(offer)\n\tif sdp == \"\" {\n\t\tsession.Close()\n\t\treturn \"\", fmt.Errorf(\"generate answer sdp failed\")\n\t}\n\n\tgo session.Run()\n\treturn sdp, nil\n}\n"
  },
  {
    "path": "rtc/subscriber_stat.go",
    "content": "package rtc\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pion/webrtc/v3\"\n)\n\nfunc remoteAddrFromDTLSTransport(dtls *webrtc.DTLSTransport) string {\n\tif dtls == nil {\n\t\treturn \"\"\n\t}\n\treturn remoteAddrFromICETransport(dtls.ICETransport())\n}\n\nfunc remoteAddrFromICETransport(iceTransport *webrtc.ICETransport) string {\n\tif iceTransport == nil {\n\t\treturn \"\"\n\t}\n\n\tpair, err := iceTransport.GetSelectedCandidatePair()\n\tif err != nil || pair == nil || pair.Remote == nil {\n\t\treturn \"\"\n\t}\n\n\treturn fmt.Sprintf(\"%s:%d\", pair.Remote.Address, pair.Remote.Port)\n}\n"
  },
  {
    "path": "rtc/unpacker.go",
    "content": "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/gortsplib/v4/pkg/format/rtph264\"\n\t\"github.com/bluenviron/gortsplib/v4/pkg/format/rtph265\"\n\t\"github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm\"\n\t\"github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio\"\n\t\"github.com/bluenviron/gortsplib/v4/pkg/rtpreorderer\"\n\t\"github.com/bluenviron/gortsplib/v4/pkg/rtptime\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/pion/webrtc/v3\"\n\t\"github.com/q191201771/lal/pkg/avc\"\n\t\"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\nvar ErrNeedMoreFrames = errors.New(\"need more frames\")\n\ntype UnPacker struct {\n\treorderer   *rtpreorderer.Reorderer\n\tpayloadType base.AvPacketPt\n\tclockRate   uint32\n\tpktChan     chan<- base.AvPacket\n\ttimeDecoder *rtptime.GlobalDecoder\n\tformat      format.Format\n\tdec         IRtpDecoder\n}\n\nfunc NewUnPacker(mimeType string, clockRate uint32, pktChan chan<- base.AvPacket) *UnPacker {\n\tun := &UnPacker{\n\t\treorderer:   rtpreorderer.New(),\n\t\tclockRate:   clockRate,\n\t\tpktChan:     pktChan,\n\t\ttimeDecoder: rtptime.NewGlobalDecoder(),\n\t}\n\n\tswitch mimeType {\n\tcase webrtc.MimeTypeH264:\n\t\tun.payloadType = base.AvPacketPtAvc\n\t\tun.format = &format.H264{}\n\t\tun.dec = NewH264RtpDecoder(un.format)\n\tcase webrtc.MimeTypePCMA:\n\t\tun.payloadType = base.AvPacketPtG711A\n\t\tun.format = &format.G711{}\n\tcase webrtc.MimeTypePCMU:\n\t\tun.payloadType = base.AvPacketPtG711U\n\t\tun.format = &format.G711{}\n\tcase webrtc.MimeTypeOpus:\n\t\tun.payloadType = base.AvPacketPtOpus\n\t\tun.format = &format.Opus{}\n\t\tun.dec = NewOpusRtpDecoder(un.format)\n\tcase webrtc.MimeTypeH265:\n\t\tun.payloadType = base.AvPacketPtHevc\n\t\tun.format = &format.H265{}\n\t\tun.dec = NewH265RtpDecoder(un.format)\n\tdefault:\n\t\tnazalog.Error(\"unsupport mineType:\", mimeType)\n\t}\n\n\tnazalog.Info(\"create rtp unpacker, mimeType:\", mimeType)\n\n\treturn un\n}\n\nfunc (un *UnPacker) UnPack(pkt *rtp.Packet) (err error) {\n\tpackets, lost := un.reorderer.Process(pkt)\n\tif lost != 0 {\n\t\tnazalog.Error(\"rtp lost\")\n\t\treturn\n\t}\n\n\tfor _, rtppkt := range packets {\n\t\tpts, ok := un.timeDecoder.Decode(un.format, rtppkt)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tframe, err := un.dec.Decode(rtppkt)\n\t\tif err != nil {\n\t\t\tif err != ErrNeedMoreFrames {\n\t\t\t\tnazalog.Error(\"rtp dec Decode failed:\", err)\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tcontinue\n\t\t}\n\n\t\tvar pkt base.AvPacket\n\t\tpkt.PayloadType = un.payloadType\n\t\tpkt.Timestamp = int64(pts / time.Millisecond)\n\t\tpkt.Pts = pkt.Timestamp\n\t\tpkt.Payload = append(pkt.Payload, frame...)\n\n\t\tun.pktChan <- pkt\n\t}\n\n\treturn\n}\n\ntype IRtpDecoder interface {\n\tDecode(pkt *rtp.Packet) ([]byte, error)\n}\n\ntype H264RtpDecoder struct {\n\tIRtpDecoder\n\tdec *rtph264.Decoder\n}\n\nfunc NewH264RtpDecoder(f format.Format) *H264RtpDecoder {\n\tdec, _ := f.(*format.H264).CreateDecoder()\n\treturn &H264RtpDecoder{\n\t\tdec: dec,\n\t}\n}\n\nfunc (r *H264RtpDecoder) Decode(pkt *rtp.Packet) ([]byte, error) {\n\tnalus, err := r.dec.Decode(pkt)\n\tif err != nil {\n\t\treturn nil, ErrNeedMoreFrames\n\t}\n\n\tif len(nalus) == 0 {\n\t\terr = fmt.Errorf(\"invalid frame\")\n\t\treturn nil, err\n\t}\n\n\tvar frame []byte\n\tfor _, nalu := range nalus {\n\t\tframe = append(frame, avc.NaluStartCode4...)\n\t\tframe = append(frame, nalu...)\n\t}\n\n\treturn frame, nil\n}\n\ntype G711RtpDecoder struct {\n\tIRtpDecoder\n\tdec *rtplpcm.Decoder\n}\n\nfunc NewG711RtpDecoder(f format.Format) *G711RtpDecoder {\n\tdec, _ := f.(*format.G711).CreateDecoder()\n\treturn &G711RtpDecoder{\n\t\tdec: dec,\n\t}\n}\n\nfunc (r *G711RtpDecoder) Decode(pkt *rtp.Packet) ([]byte, error) {\n\tframe, err := r.dec.Decode(pkt)\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn nil, err\n\t}\n\n\treturn frame, nil\n}\n\ntype OpusRtpDecoder struct {\n\tIRtpDecoder\n\tdec *rtpsimpleaudio.Decoder\n}\n\nfunc NewOpusRtpDecoder(f format.Format) *OpusRtpDecoder {\n\tdec, _ := f.(*format.Opus).CreateDecoder()\n\treturn &OpusRtpDecoder{\n\t\tdec: dec,\n\t}\n}\n\nfunc (r *OpusRtpDecoder) Decode(pkt *rtp.Packet) ([]byte, error) {\n\tframe, err := r.dec.Decode(pkt)\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn nil, err\n\t}\n\n\treturn frame, nil\n}\n\ntype H265RtpDecoder struct {\n\tIRtpDecoder\n\tdec *rtph265.Decoder\n}\n\nfunc NewH265RtpDecoder(f format.Format) *H265RtpDecoder {\n\tdec, _ := f.(*format.H265).CreateDecoder()\n\treturn &H265RtpDecoder{\n\t\tdec: dec,\n\t}\n}\n\nfunc (r *H265RtpDecoder) Decode(pkt *rtp.Packet) ([]byte, error) {\n\tnalus, err := r.dec.Decode(pkt)\n\tif err != nil {\n\t\treturn nil, ErrNeedMoreFrames\n\t}\n\n\tif len(nalus) == 0 {\n\t\terr = fmt.Errorf(\"invalid frame\")\n\t\treturn nil, err\n\t}\n\n\tvar frame []byte\n\tfor _, nalu := range nalus {\n\t\tframe = append(frame, avc.NaluStartCode4...)\n\t\tframe = append(frame, nalu...)\n\t}\n\n\treturn frame, nil\n}\n"
  },
  {
    "path": "rtc/whepsession.go",
    "content": "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\"\n\t\"github.com/smallnest/chanx\"\n\n\t\"github.com/gofrs/uuid\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/pion/webrtc/v3\"\n\t\"github.com/q191201771/lal/pkg/avc\"\n\t\"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/lal/pkg/hevc\"\n\t\"github.com/q191201771/lal/pkg/logic\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\nconst whepMaxReplayPaceDelay = 5 * time.Millisecond\n\ntype whepSession struct {\n\tgroup          *maxlogic.Group\n\tpc             *peerConnection\n\tsubscriberId   string\n\tlalServer      logic.ILalServer\n\tvideoTrack     *webrtc.TrackLocalStaticRTP\n\taudioTrack     *webrtc.TrackLocalStaticRTP\n\tvideoSender    *webrtc.RTPSender\n\taudioSender    *webrtc.RTPSender\n\tvideopacker    *Packer\n\taudiopacker    *Packer\n\tmsgChan        *chanx.UnboundedChan[base.RtmpMsg]\n\tcloseChan      chan bool\n\tconnectedChan  chan struct{}\n\tconnectedOnce  sync.Once\n\tpaceBaseDts    uint32\n\tpaceBaseAt     time.Time\n\tpaceStarted    bool\n\treplayingCache bool\n\twroteBytes     atomic.Uint64\n\tremoteAddr     atomic.Value\n}\n\nfunc NewWhepSession(appName, streamid string, writeChanSize int, pc *peerConnection, lalServer logic.ILalServer) *whepSession {\n\tok, group := maxlogic.GetGroupManagerInstance().GetGroup(maxlogic.NewStreamKey(appName, streamid))\n\tif !ok {\n\t\tnazalog.Errorf(\"not found stream, appName:%s, streamid:%s\", appName, streamid)\n\t\treturn nil\n\t}\n\n\tu, _ := uuid.NewV4()\n\treturn &whepSession{\n\t\tgroup:         group,\n\t\tpc:            pc,\n\t\tlalServer:     lalServer,\n\t\tsubscriberId:  u.String(),\n\t\tmsgChan:       chanx.NewUnboundedChan[base.RtmpMsg](context.Background(), writeChanSize),\n\t\tcloseChan:     make(chan bool, 2),\n\t\tconnectedChan: make(chan struct{}, 1),\n\t}\n}\n\nfunc (conn *whepSession) GetAnswerSDP(offer string) (sdp string) {\n\tvar err error\n\n\tvideoHeader := conn.group.GetVideoSeqHeaderMsg()\n\tif videoHeader != nil {\n\t\tif videoHeader.IsAvcKeySeqHeader() {\n\t\t\tconn.videoTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264}, \"video\", \"lalmax\")\n\t\t\tif err != nil {\n\t\t\t\tnazalog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconn.videoSender, err = conn.pc.AddTrack(conn.videoTrack)\n\t\t\tif err != nil {\n\t\t\t\tnazalog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconn.videopacker = NewPacker(PacketH264, videoHeader.Payload)\n\t\t} else if videoHeader.IsHevcKeySeqHeader() {\n\t\t\tconn.videoTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH265}, \"video\", \"lalmax\")\n\t\t\tif err != nil {\n\t\t\t\tnazalog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconn.videoSender, err = conn.pc.AddTrack(conn.videoTrack)\n\t\t\tif err != nil {\n\t\t\t\tnazalog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconn.videopacker = NewPacker(PacketHEVC, videoHeader.Payload)\n\t\t}\n\t}\n\n\taudioHeader := conn.group.GetAudioSeqHeaderMsg()\n\tif audioHeader != nil {\n\t\tvar mimeType string\n\t\taudioId := audioHeader.AudioCodecId()\n\t\tswitch audioId {\n\t\tcase base.RtmpSoundFormatG711A:\n\t\t\tconn.audioTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypePCMA}, \"audio\", \"lalmax\")\n\t\t\tif err != nil {\n\t\t\t\tnazalog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tmimeType = PacketPCMA\n\t\tcase base.RtmpSoundFormatG711U:\n\t\t\tconn.audioTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypePCMU}, \"audio\", \"lalmax\")\n\t\t\tif err != nil {\n\t\t\t\tnazalog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tmimeType = PacketPCMU\n\t\tcase base.RtmpSoundFormatOpus:\n\t\t\tconn.audioTrack, err = webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, \"audio\", \"lalmax\")\n\t\t\tif err != nil {\n\t\t\t\tnazalog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tmimeType = PacketOPUS\n\t\tdefault:\n\t\t\tnazalog.Error(\"unsupport audio codeid:\", audioId)\n\t\t}\n\n\t\tif conn.audioTrack != nil {\n\t\t\tconn.audioSender, err = conn.pc.AddTrack(conn.audioTrack)\n\t\t\tif err != nil {\n\t\t\t\tnazalog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tconn.audiopacker = NewPacker(mimeType, nil)\n\t\t}\n\t}\n\n\tgatherComplete := webrtc.GatheringCompletePromise(conn.pc.PeerConnection)\n\n\tconn.pc.SetRemoteDescription(webrtc.SessionDescription{\n\t\tType: webrtc.SDPTypeOffer,\n\t\tSDP:  string(offer),\n\t})\n\n\tanswer, err := conn.pc.CreateAnswer(nil)\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn\n\t}\n\n\terr = conn.pc.SetLocalDescription(answer)\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn\n\t}\n\n\t<-gatherComplete\n\n\tsdp = conn.pc.LocalDescription().SDP\n\treturn\n}\n\nfunc (conn *whepSession) Run() {\n\tconn.pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {\n\t\tnazalog.Info(\"peer connection state: \", state.String())\n\n\t\tswitch state {\n\t\tcase webrtc.PeerConnectionStateConnected:\n\t\t\tconn.signalConnected()\n\t\tcase webrtc.PeerConnectionStateDisconnected:\n\t\t\tfallthrough\n\t\tcase webrtc.PeerConnectionStateFailed:\n\t\t\tfallthrough\n\t\tcase webrtc.PeerConnectionStateClosed:\n\t\t\tconn.closeChan <- true\n\t\t}\n\t})\n\n\tif conn.pc.ConnectionState() == webrtc.PeerConnectionStateConnected {\n\t\tconn.signalConnected()\n\t}\n\n\tfor {\n\t\tselect {\n\t\tcase <-conn.connectedChan:\n\t\t\tconn.group.AddSubscriber(maxlogic.SubscriberInfo{\n\t\t\t\tSubscriberID: conn.subscriberId,\n\t\t\t\tProtocol:     maxlogic.SubscriberProtocolWHEP,\n\t\t\t}, conn)\n\t\t\tgoto connected\n\t\tcase <-conn.closeChan:\n\t\t\tnazalog.Info(\"RemoveConsumer, connid:\", conn.subscriberId)\n\t\t\tconn.group.RemoveSubscriber(conn.subscriberId)\n\t\t\treturn\n\t\t}\n\t}\n\nconnected:\n\tfor {\n\t\tselect {\n\t\tcase msg := <-conn.msgChan.Out:\n\t\t\tif msg.Header.MsgTypeId == 0 {\n\t\t\t\tconn.replayingCache = false\n\t\t\t\tconn.paceBaseAt = time.Time{}\n\t\t\t\tconn.paceStarted = false\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif conn.replayingCache {\n\t\t\t\tconn.paceReplayMsg(msg)\n\t\t\t}\n\t\t\tif msg.Header.MsgTypeId == base.RtmpTypeIdAudio && conn.audioTrack != nil {\n\t\t\t\tconn.sendAudio(msg)\n\t\t\t} else if msg.Header.MsgTypeId == base.RtmpTypeIdVideo && conn.videoTrack != nil {\n\t\t\t\tconn.sendVideo(msg)\n\t\t\t}\n\t\tcase <-conn.closeChan:\n\t\t\tnazalog.Info(\"RemoveConsumer, connid:\", conn.subscriberId)\n\t\t\tconn.group.RemoveSubscriber(conn.subscriberId)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (conn *whepSession) signalConnected() {\n\tconn.connectedOnce.Do(func() {\n\t\tconn.refreshRemoteAddr()\n\t\tconn.connectedChan <- struct{}{}\n\t})\n}\n\nfunc (conn *whepSession) OnReplayStart() {\n\tconn.replayingCache = true\n\tconn.paceBaseAt = time.Time{}\n\tconn.paceBaseDts = 0\n\tconn.paceStarted = false\n}\n\nfunc (conn *whepSession) OnReplayStop() {\n\tconn.msgChan.In <- base.RtmpMsg{}\n}\n\nfunc (conn *whepSession) paceReplayMsg(msg base.RtmpMsg) {\n\tif msg.Header.MsgTypeId != base.RtmpTypeIdAudio && msg.Header.MsgTypeId != base.RtmpTypeIdVideo {\n\t\treturn\n\t}\n\tif msg.IsVideoKeySeqHeader() || msg.IsAacSeqHeader() {\n\t\treturn\n\t}\n\n\tif !conn.paceStarted {\n\t\tconn.paceBaseDts = msg.Dts()\n\t\tconn.paceBaseAt = time.Now()\n\t\tconn.paceStarted = true\n\t\treturn\n\t}\n\n\tdtsDelta := int64(msg.Dts()) - int64(conn.paceBaseDts)\n\tif dtsDelta <= 0 {\n\t\treturn\n\t}\n\n\tmediaElapsed := time.Duration(dtsDelta) * time.Millisecond\n\tdelay := time.Until(conn.paceBaseAt.Add(mediaElapsed))\n\tif delay <= 0 {\n\t\treturn\n\t}\n\tif delay > whepMaxReplayPaceDelay {\n\t\tdelay = whepMaxReplayPaceDelay\n\t}\n\n\ttime.Sleep(delay)\n}\n\nfunc (conn *whepSession) OnMsg(msg base.RtmpMsg) {\n\tswitch msg.Header.MsgTypeId {\n\tcase base.RtmpTypeIdMetadata:\n\t\treturn\n\tcase base.RtmpTypeIdAudio:\n\t\tif conn.audioTrack != nil {\n\t\t\tconn.msgChan.In <- msg\n\t\t}\n\tcase base.RtmpTypeIdVideo:\n\t\tif msg.IsVideoKeySeqHeader() {\n\t\t\tconn.updateVideoCodec(msg)\n\t\t\treturn\n\t\t}\n\t\tif conn.videoTrack != nil {\n\t\t\tconn.msgChan.In <- msg\n\t\t}\n\t}\n}\n\nfunc (conn *whepSession) OnStop() {\n\tconn.closeChan <- true\n}\n\nfunc (conn *whepSession) sendAudio(msg base.RtmpMsg) {\n\tif conn.audiopacker != nil {\n\t\tpkts, err := conn.audiopacker.Encode(msg)\n\t\tif err != nil {\n\t\t\tnazalog.Error(err)\n\t\t\treturn\n\t\t}\n\n\t\tfor _, pkt := range pkts {\n\t\t\tif err := conn.audioTrack.WriteRTP(pkt); err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconn.recordSentRTP(pkt)\n\t\t}\n\t}\n}\n\nfunc (conn *whepSession) sendVideo(msg base.RtmpMsg) {\n\tif conn.videopacker != nil {\n\n\t\tpkts, err := conn.videopacker.Encode(msg)\n\t\tif err != nil {\n\t\t\tnazalog.Error(err)\n\t\t\treturn\n\t\t}\n\n\t\tfor _, pkt := range pkts {\n\t\t\tif err := conn.videoTrack.WriteRTP(pkt); err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tconn.recordSentRTP(pkt)\n\t\t}\n\t}\n}\n\nfunc (conn *whepSession) updateVideoCodec(msg base.RtmpMsg) {\n\tif conn.videopacker == nil {\n\t\treturn\n\t}\n\n\tif msg.IsAvcKeySeqHeader() {\n\t\tsps, pps, err := avc.ParseSpsPpsFromSeqHeader(msg.Payload)\n\t\tif err != nil {\n\t\t\tnazalog.Error(\"ParseSpsPpsFromSeqHeader err:\", err)\n\t\t\treturn\n\t\t}\n\n\t\tif h264Encoder, ok := conn.videopacker.enc.(*H264RtpEncoder); ok {\n\t\t\tif bytes.Equal(h264Encoder.sps, sps) && bytes.Equal(h264Encoder.pps, pps) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tconn.videopacker.UpdateVideoCodec(nil, sps, pps)\n\t\treturn\n\t}\n\n\tif msg.IsHevcKeySeqHeader() {\n\t\tvps, sps, pps, err := hevc.ParseVpsSpsPpsFromSeqHeader(msg.Payload)\n\t\tif err != nil {\n\t\t\tnazalog.Error(\"ParseVpsSpsPpsFromSeqHeader err:\", err)\n\t\t\treturn\n\t\t}\n\n\t\tif hevcEncoder, ok := conn.videopacker.enc.(*HevcRtpEncoder); ok {\n\t\t\tif bytes.Equal(hevcEncoder.vps, vps) && bytes.Equal(hevcEncoder.sps, sps) && bytes.Equal(hevcEncoder.pps, pps) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tconn.videopacker.UpdateVideoCodec(vps, sps, pps)\n\t}\n}\n\nfunc (conn *whepSession) Close() {\n\tif conn.pc != nil {\n\t\tconn.pc.Close()\n\t}\n}\n\nfunc (conn *whepSession) GetSubscriberStat() maxlogic.SubscriberStat {\n\tconn.refreshRemoteAddr()\n\treturn maxlogic.SubscriberStat{\n\t\tRemoteAddr:    conn.loadRemoteAddr(),\n\t\tWroteBytesSum: conn.wroteBytes.Load(),\n\t}\n}\n\nfunc (conn *whepSession) recordSentRTP(pkt *rtp.Packet) {\n\tif pkt == nil {\n\t\treturn\n\t}\n\tconn.wroteBytes.Add(uint64(pkt.MarshalSize()))\n}\n\nfunc (conn *whepSession) refreshRemoteAddr() {\n\tif remoteAddr := conn.currentRemoteAddr(); remoteAddr != \"\" {\n\t\tconn.remoteAddr.Store(remoteAddr)\n\t}\n}\n\nfunc (conn *whepSession) currentRemoteAddr() string {\n\tif conn.videoSender != nil {\n\t\tif remoteAddr := remoteAddrFromDTLSTransport(conn.videoSender.Transport()); remoteAddr != \"\" {\n\t\t\treturn remoteAddr\n\t\t}\n\t}\n\tif conn.audioSender != nil {\n\t\tif remoteAddr := remoteAddrFromDTLSTransport(conn.audioSender.Transport()); remoteAddr != \"\" {\n\t\t\treturn remoteAddr\n\t\t}\n\t}\n\tif sctp := conn.pc.SCTP(); sctp != nil {\n\t\treturn remoteAddrFromDTLSTransport(sctp.Transport())\n\t}\n\treturn \"\"\n}\n\nfunc (conn *whepSession) loadRemoteAddr() string {\n\tv := conn.remoteAddr.Load()\n\taddr, _ := v.(string)\n\treturn addr\n}\n"
  },
  {
    "path": "rtc/whipsession.go",
    "content": "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\"github.com/q191201771/lal/pkg/logic\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\ntype whipSession struct {\n\tstreamid      string\n\tpc            *peerConnection\n\tlalServer     logic.ILalServer\n\tlalSession    logic.ICustomizePubSessionContext\n\tvideoUnpacker *UnPacker\n\taudioUnpacker *UnPacker\n\tpktChan       chan base.AvPacket\n\tcloseChan     chan bool\n\tsubscriberId  string\n}\n\nfunc NewWhipSession(streamid string, pc *peerConnection, lalServer logic.ILalServer) *whipSession {\n\tsession, err := lalServer.AddCustomizePubSession(streamid)\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn nil\n\t}\n\n\tsession.WithOption(func(option *base.AvPacketStreamOption) {\n\t\toption.VideoFormat = base.AvPacketStreamVideoFormatAnnexb\n\t})\n\n\tu, _ := uuid.NewV4()\n\n\treturn &whipSession{\n\t\tstreamid:     streamid,\n\t\tpc:           pc,\n\t\tlalServer:    lalServer,\n\t\tlalSession:   session,\n\t\tpktChan:      make(chan base.AvPacket, 100),\n\t\tcloseChan:    make(chan bool, 2),\n\t\tsubscriberId: u.String(),\n\t}\n}\n\nfunc (conn *whipSession) GetAnswerSDP(offer string) (sdp string) {\n\tgatherComplete := webrtc.GatheringCompletePromise(conn.pc.PeerConnection)\n\n\tconn.pc.SetRemoteDescription(webrtc.SessionDescription{\n\t\tType: webrtc.SDPTypeOffer,\n\t\tSDP:  string(offer),\n\t})\n\n\tanswer, err := conn.pc.CreateAnswer(nil)\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn\n\t}\n\n\terr = conn.pc.SetLocalDescription(answer)\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t\treturn\n\t}\n\n\t<-gatherComplete\n\n\tsdp = conn.pc.LocalDescription().SDP\n\treturn\n}\n\nfunc (conn *whipSession) Run() {\n\n\tconn.pc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {\n\t\tnazalog.Info(\"peer connection state: \", state.String())\n\n\t\tswitch state {\n\t\tcase webrtc.PeerConnectionStateConnected:\n\t\tcase webrtc.PeerConnectionStateDisconnected:\n\t\t\tfallthrough\n\t\tcase webrtc.PeerConnectionStateFailed:\n\t\t\tfallthrough\n\t\tcase webrtc.PeerConnectionStateClosed:\n\t\t\tconn.closeChan <- true\n\t\t}\n\t})\n\n\tvar videoPt webrtc.PayloadType\n\tconn.pc.OnTrack(func(tr *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {\n\t\tswitch tr.Kind() {\n\t\tcase webrtc.RTPCodecTypeVideo:\n\t\t\tconn.videoUnpacker = NewUnPacker(tr.Codec().MimeType, tr.Codec().ClockRate, conn.pktChan)\n\t\t\tvideoPt = tr.PayloadType()\n\t\tcase webrtc.RTPCodecTypeAudio:\n\t\t\tmimeType := tr.Codec().MimeType\n\t\t\tif tr.Codec().MimeType == \"\" {\n\t\t\t\t// pt为0或者8按照G711U和G711A处理,提高兼容性\n\t\t\t\tif tr.PayloadType() == 0 {\n\t\t\t\t\tmimeType = webrtc.MimeTypePCMU\n\t\t\t\t} else if tr.PayloadType() == 8 {\n\t\t\t\t\tmimeType = webrtc.MimeTypePCMA\n\t\t\t\t}\n\t\t\t}\n\t\t\tconn.audioUnpacker = NewUnPacker(mimeType, tr.Codec().ClockRate, conn.pktChan)\n\t\t}\n\n\t\tfor {\n\t\t\tpkt, _, err := tr.ReadRTP()\n\t\t\tif err != nil {\n\t\t\t\tnazalog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif conn.videoUnpacker != nil && pkt.Header.PayloadType == uint8(videoPt) {\n\t\t\t\tconn.videoUnpacker.UnPack(pkt)\n\t\t\t} else if conn.audioUnpacker != nil {\n\t\t\t\tconn.audioUnpacker.UnPack(pkt)\n\t\t\t}\n\t\t}\n\t})\n\n\tfor {\n\t\tselect {\n\t\tcase <-conn.closeChan:\n\t\t\tnazalog.Info(\"whip connect close, streamid:\", conn.streamid)\n\t\t\tconn.lalServer.DelCustomizePubSession(conn.lalSession)\n\t\t\treturn\n\t\tcase pkt := <-conn.pktChan:\n\t\t\tconn.lalSession.FeedAvPacket(pkt)\n\t\t}\n\t}\n}\n\nfunc (conn *whipSession) Close() {\n\tif conn.lalServer != nil {\n\t\tconn.lalServer.DelCustomizePubSession(conn.lalSession)\n\t}\n\tif conn.pc != nil {\n\t\tconn.pc.Close()\n\t}\n}\n"
  },
  {
    "path": "run.sh",
    "content": "./lalmax -c conf/lalmax.conf.json"
  },
  {
    "path": "server/hook_builtin_http_plugin.go",
    "content": "package server\n\nimport \"fmt\"\n\ntype hookBuiltinHTTPPlugin struct {\n\tname string\n\thub  *HttpNotify\n}\n\nfunc (p *hookBuiltinHTTPPlugin) Name() string {\n\treturn p.name\n}\n\nfunc (p *hookBuiltinHTTPPlugin) OnHookEvent(event HookEvent) error {\n\tif p == nil || p.hub == nil {\n\t\treturn nil\n\t}\n\tif !p.hub.cfg.Enable {\n\t\treturn nil\n\t}\n\n\tswitch event.Event {\n\tcase HookEventServerStart:\n\t\tif p.hub.cfg.OnServerStart != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.OnServerStart, event)\n\t\t}\n\t\tif p.hub.cfg.ZlmOnServerStarted != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.ZlmOnServerStarted, event)\n\t\t}\n\tcase HookEventUpdate:\n\t\tif p.hub.cfg.OnUpdate != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.OnUpdate, event)\n\t\t}\n\tcase HookEventGroupStart:\n\t\tif p.hub.cfg.OnGroupStart != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.OnGroupStart, event)\n\t\t}\n\tcase HookEventGroupStop:\n\t\tif p.hub.cfg.OnGroupStop != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.OnGroupStop, event)\n\t\t}\n\tcase HookEventStreamActive:\n\t\tif p.hub.cfg.OnStreamActive != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.OnStreamActive, event)\n\t\t}\n\tcase HookEventPubStart:\n\t\tif p.hub.cfg.OnPubStart != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.OnPubStart, event)\n\t\t}\n\tcase HookEventPubStop:\n\t\tif p.hub.cfg.OnPubStop != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.OnPubStop, event)\n\t\t}\n\tcase HookEventSubStart:\n\t\tif p.hub.cfg.OnSubStart != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.OnSubStart, event)\n\t\t}\n\tcase HookEventSubStop:\n\t\tif p.hub.cfg.OnSubStop != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.OnSubStop, event)\n\t\t}\n\tcase HookEventRelayPullStart:\n\t\tif p.hub.cfg.OnRelayPullStart != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.OnRelayPullStart, event)\n\t\t}\n\tcase HookEventRelayPullStop:\n\t\tif p.hub.cfg.OnRelayPullStop != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.OnRelayPullStop, event)\n\t\t}\n\tcase HookEventRtmpConnect:\n\t\tif p.hub.cfg.OnRtmpConnect != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.OnRtmpConnect, event)\n\t\t}\n\tcase HookEventHlsMakeTs:\n\t\tif p.hub.cfg.OnHlsMakeTs != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.OnHlsMakeTs, event)\n\t\t}\n\tcase HookEventStreamChanged:\n\t\tif p.hub.cfg.ZlmOnStreamChanged != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.ZlmOnStreamChanged, event)\n\t\t}\n\tcase HookEventServerKeepalive:\n\t\tif p.hub.cfg.ZlmOnServerKeepalive != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.ZlmOnServerKeepalive, event)\n\t\t}\n\tcase HookEventStreamNoneReader:\n\t\tif p.hub.cfg.ZlmOnStreamNoneReader != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.ZlmOnStreamNoneReader, event)\n\t\t}\n\tcase HookEventRtpServerTimeout:\n\t\tif p.hub.cfg.ZlmOnRtpServerTimeout != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.ZlmOnRtpServerTimeout, event)\n\t\t}\n\tcase HookEventRecordMp4:\n\t\tif p.hub.cfg.ZlmOnRecordMp4 != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.ZlmOnRecordMp4, event)\n\t\t}\n\tcase HookEventPublish:\n\t\tif p.hub.cfg.ZlmOnPublish != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.ZlmOnPublish, event)\n\t\t}\n\tcase HookEventPlay:\n\t\tif p.hub.cfg.ZlmOnPlay != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.ZlmOnPlay, event)\n\t\t}\n\tcase HookEventStreamNotFound:\n\t\tif p.hub.cfg.ZlmOnStreamNotFound != \"\" {\n\t\t\tp.hub.asyncPostEvent(p.hub.cfg.ZlmOnStreamNotFound, event)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (h *HttpNotify) mustRegisterBuiltinHTTPPlugin() {\n\tif h == nil {\n\t\treturn\n\t}\n\n\t_, err := h.RegisterPlugin(&hookBuiltinHTTPPlugin{\n\t\tname: \"builtin-http-notify\",\n\t\thub:  h,\n\t}, HookPluginOptions{})\n\tif err != nil {\n\t\tpanic(fmt.Sprintf(\"register builtin http hook plugin failed: %v\", err))\n\t}\n}\n"
  },
  {
    "path": "server/hook_filter.go",
    "content": "package server\n\nimport (\n\t\"strings\"\n\n\tmaxlogic \"github.com/q191201771/lalmax/logic\"\n)\n\ntype HookEventFilter struct {\n\tEventNames map[string]struct{}\n\tAppName    string\n\tStreamName string\n\tSessionID  string\n}\n\nfunc NewHookEventFilter(appName, streamName, sessionID string, eventNames []string) HookEventFilter {\n\tfilter := HookEventFilter{\n\t\tAppName:    appName,\n\t\tStreamName: streamName,\n\t\tSessionID:  sessionID,\n\t}\n\n\tif len(eventNames) != 0 {\n\t\tfilter.EventNames = make(map[string]struct{}, len(eventNames))\n\t\tfor _, eventName := range eventNames {\n\t\t\tif eventName == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tfilter.EventNames[eventName] = struct{}{}\n\t\t}\n\t}\n\n\treturn filter\n}\n\nfunc ParseHookEventNames(raw string) []string {\n\tif raw == \"\" {\n\t\treturn nil\n\t}\n\n\tparts := strings.Split(raw, \",\")\n\tout := make([]string, 0, len(parts))\n\tfor _, part := range parts {\n\t\tpart = strings.TrimSpace(part)\n\t\tif part == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, part)\n\t}\n\treturn out\n}\n\nfunc (f HookEventFilter) Match(event HookEvent) bool {\n\tif len(f.EventNames) != 0 {\n\t\tif _, ok := f.EventNames[event.Event]; !ok {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tif f.SessionID != \"\" && event.sessionID != f.SessionID {\n\t\treturn false\n\t}\n\n\tif f.AppName == \"\" && f.StreamName == \"\" {\n\t\treturn true\n\t}\n\n\tif event.streamName != \"\" || event.appName != \"\" {\n\t\treturn matchStreamKey(maxlogic.NewStreamKey(event.appName, event.streamName), f.AppName, f.StreamName)\n\t}\n\n\tfor _, key := range event.groupKeys {\n\t\tif matchStreamKey(key, f.AppName, f.StreamName) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc matchStreamKey(key maxlogic.StreamKey, appName, streamName string) bool {\n\tif streamName != \"\" && key.StreamName != streamName {\n\t\treturn false\n\t}\n\n\tif appName != \"\" && key.AppName != appName {\n\t\treturn false\n\t}\n\n\tif streamName == \"\" && appName != \"\" && key.AppName == \"\" {\n\t\treturn false\n\t}\n\n\treturn key.StreamName != \"\" || key.AppName != \"\"\n}\n"
  },
  {
    "path": "server/hook_plugin.go",
    "content": "package server\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n)\n\nconst defaultHookPluginBufferSize = 64\n\ntype HookPlugin interface {\n\tName() string\n\tOnHookEvent(event HookEvent) error\n}\n\ntype HookPluginOptions struct {\n\tBufferSize int\n\tFilter     HookEventFilter\n}\n\ntype hookPluginEntry struct {\n\tplugin HookPlugin\n\tfilter HookEventFilter\n\tqueue  chan HookEvent\n}\n\nfunc (h *HttpNotify) RegisterPlugin(plugin HookPlugin, options HookPluginOptions) (func(), error) {\n\tif h == nil {\n\t\treturn nil, fmt.Errorf(\"hook hub is nil\")\n\t}\n\tif plugin == nil {\n\t\treturn nil, fmt.Errorf(\"hook plugin is nil\")\n\t}\n\tif plugin.Name() == \"\" {\n\t\treturn nil, fmt.Errorf(\"hook plugin name is empty\")\n\t}\n\n\tbufferSize := options.BufferSize\n\tif bufferSize <= 0 {\n\t\tbufferSize = defaultHookPluginBufferSize\n\t}\n\n\tentry := &hookPluginEntry{\n\t\tplugin: plugin,\n\t\tfilter: options.Filter,\n\t\tqueue:  make(chan HookEvent, bufferSize),\n\t}\n\n\th.pluginMux.Lock()\n\tif _, exists := h.plugins[plugin.Name()]; exists {\n\t\th.pluginMux.Unlock()\n\t\treturn nil, fmt.Errorf(\"hook plugin already exists: %s\", plugin.Name())\n\t}\n\th.plugins[plugin.Name()] = entry\n\th.pluginMux.Unlock()\n\n\tgo h.runPlugin(entry)\n\n\tvar once sync.Once\n\tcancel := func() {\n\t\tonce.Do(func() {\n\t\t\th.unregisterPlugin(plugin.Name())\n\t\t})\n\t}\n\n\treturn cancel, nil\n}\n\nfunc (h *HttpNotify) runPlugin(entry *hookPluginEntry) {\n\tfor event := range entry.queue {\n\t\tif err := entry.plugin.OnHookEvent(event); err != nil {\n\t\t\tLog.Errorf(\"hook plugin handle error. plugin=%s, event=%s, err=%+v\", entry.plugin.Name(), event.Event, err)\n\t\t}\n\t}\n}\n\nfunc (h *HttpNotify) dispatchPlugins(event HookEvent) {\n\th.pluginMux.RLock()\n\tdefer h.pluginMux.RUnlock()\n\n\tfor _, entry := range h.plugins {\n\t\tif !entry.filter.Match(event) {\n\t\t\tcontinue\n\t\t}\n\n\t\tselect {\n\t\tcase entry.queue <- event:\n\t\tdefault:\n\t\t\tLog.Warnf(\"hook plugin queue full. plugin=%s, event=%s\", entry.plugin.Name(), event.Event)\n\t\t}\n\t}\n}\n\nfunc (h *HttpNotify) unregisterPlugin(name string) {\n\th.pluginMux.Lock()\n\tdefer h.pluginMux.Unlock()\n\n\tentry, ok := h.plugins[name]\n\tif !ok {\n\t\treturn\n\t}\n\n\tdelete(h.plugins, name)\n\tclose(entry.queue)\n}\n"
  },
  {
    "path": "server/http_notify.go",
    "content": "// Copyright 2020, Chef.  All rights reserved.\n// https://github.com/q191201771/lal\n//\n// Use of this source code is governed by a MIT-style license\n// that can be found in the License file.\n//\n// Author: Chef (191201771@qq.com)\n\npackage server\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\tmaxlogic \"github.com/q191201771/lalmax/logic\"\n\n\tconfig \"github.com/q191201771/lalmax/config\"\n\n\t\"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/naza/pkg/nazahttp\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\n// TODO(chef): refactor 配置参数供外部传入\n// TODO(chef): refactor maxTaskLen修改为能表示是阻塞任务的意思\nvar (\n\tmaxTaskLen                  = 1024\n\tnotifyTimeoutSec            = 3\n\thookHistorySize             = 256\n\thookSubBufSize              = 64\n\thookHTTPPostWorkerIdleAfter = time.Minute\n)\n\nvar Log = nazalog.GetGlobalLogger()\n\ntype hookHTTPPostTask struct {\n\turl       string\n\torderKey  string\n\teventName string\n\tpayload   []byte\n}\n\ntype hookHTTPPostWorker struct {\n\tqueue chan hookHTTPPostTask\n}\n\ntype HookGroupInfo struct {\n\tbase.EventCommonInfo\n\tAppName    string `json:\"app_name\"`\n\tStreamName string `json:\"stream_name\"`\n}\n\ntype HookEvent struct {\n\tID        int64           `json:\"id\"`\n\tEvent     string          `json:\"event\"`\n\tTimestamp string          `json:\"timestamp\"`\n\tPayload   json.RawMessage `json:\"payload\"`\n\n\tsessionID  string\n\tstreamName string\n\tappName    string\n\tgroupKeys  []maxlogic.StreamKey\n}\n\nconst (\n\tHookEventServerStart      = \"on_server_start\"\n\tHookEventUpdate           = \"on_update\"\n\tHookEventGroupStart       = \"on_group_start\"\n\tHookEventGroupStop        = \"on_group_stop\"\n\tHookEventStreamActive     = \"on_stream_active\"\n\tHookEventPubStart         = \"on_pub_start\"\n\tHookEventPubStop          = \"on_pub_stop\"\n\tHookEventSubStart         = \"on_sub_start\"\n\tHookEventSubStop          = \"on_sub_stop\"\n\tHookEventRelayPullStart   = \"on_relay_pull_start\"\n\tHookEventRelayPullStop    = \"on_relay_pull_stop\"\n\tHookEventRtmpConnect      = \"on_rtmp_connect\"\n\tHookEventHlsMakeTs        = \"on_hls_make_ts\"\n\tHookEventStreamChanged    = \"on_stream_changed\"\n\tHookEventServerKeepalive  = \"on_server_keepalive\"\n\tHookEventStreamNoneReader = \"on_stream_none_reader\"\n\tHookEventRtpServerTimeout = \"on_rtp_server_timeout\"\n\tHookEventRecordMp4        = \"on_record_mp4\"\n\tHookEventPublish          = \"on_publish\"\n\tHookEventPlay             = \"on_play\"\n\tHookEventStreamNotFound   = \"on_stream_not_found\"\n)\n\n// SubCountFn 查询指定流当前的 sub 数量\n// 为什么用回调：避免 HttpNotify 直接依赖 lalsvr，保持解耦\ntype SubCountFn func(streamName string) int\n\ntype HttpNotify struct {\n\tcfg config.HttpNotifyConfig\n\n\tserverId string\n\tstats    *maxlogic.StatAggregator\n\n\tclient *http.Client\n\n\tsubCountFn SubCountFn\n\n\teventID     atomic.Int64\n\tsubID       atomic.Int64\n\thistoryMux  sync.RWMutex\n\thistory     []HookEvent\n\tsubscriberM sync.RWMutex\n\tsubscribers map[int64]chan HookEvent\n\tpluginMux   sync.RWMutex\n\tplugins     map[string]*hookPluginEntry\n\thttpPostMux sync.Mutex\n\thttpPosts   map[string]*hookHTTPPostWorker\n}\n\n// SetSubCountFn 注入 sub 数量查询函数，用于 on_stream_none_reader 判断\nfunc (h *HttpNotify) SetSubCountFn(fn SubCountFn) {\n\th.subCountFn = fn\n}\n\n// UpdateZlmHookConfig 运行时更新 ZLM 兼容 hook 配置\n// 为什么：gb28181 通过 setServerConfig 动态设置 hook URL，需要立即生效\n// 为什么清零原有字段：ZLM 回调与 lalmax 原有回调互斥，避免双重触发\nfunc (h *HttpNotify) UpdateZlmHookConfig(zlmCfg config.ZlmCompatHookConfig) {\n\th.cfg.ZlmCompatHookConfig = zlmCfg\n\th.cfg.Enable = true\n\n\tif h.cfg.HookTimeoutSec > 0 {\n\t\th.client.Timeout = time.Duration(h.cfg.HookTimeoutSec) * time.Second\n\t}\n\n\th.cfg.OnServerStart = \"\"\n\th.cfg.OnUpdate = \"\"\n\th.cfg.OnGroupStart = \"\"\n\th.cfg.OnGroupStop = \"\"\n\th.cfg.OnStreamActive = \"\"\n\th.cfg.OnPubStart = \"\"\n\th.cfg.OnPubStop = \"\"\n\th.cfg.OnSubStart = \"\"\n\th.cfg.OnSubStop = \"\"\n\th.cfg.OnRelayPullStart = \"\"\n\th.cfg.OnRelayPullStop = \"\"\n\th.cfg.OnRtmpConnect = \"\"\n\th.cfg.OnHlsMakeTs = \"\"\n\n\tLog.Infof(\"zlm compat hook config updated. timeout=%ds, on_stream_changed=%s, on_server_keepalive=%s, on_publish=%s, on_play=%s\",\n\t\th.cfg.HookTimeoutSec, zlmCfg.ZlmOnStreamChanged, zlmCfg.ZlmOnServerKeepalive, zlmCfg.ZlmOnPublish, zlmCfg.ZlmOnPlay)\n}\n\nfunc NewHttpNotify(cfg config.HttpNotifyConfig, serverId string) *HttpNotify {\n\ttimeout := notifyTimeoutSec\n\tif cfg.HookTimeoutSec > 0 {\n\t\ttimeout = cfg.HookTimeoutSec\n\t}\n\thttpNotify := &HttpNotify{\n\t\tcfg:         cfg,\n\t\tserverId:    serverId,\n\t\tstats:       maxlogic.NewStatAggregator(maxlogic.GetGroupManagerInstance()),\n\t\thistory:     make([]HookEvent, 0, hookHistorySize),\n\t\tsubscribers: make(map[int64]chan HookEvent),\n\t\tplugins:     make(map[string]*hookPluginEntry),\n\t\thttpPosts:   make(map[string]*hookHTTPPostWorker),\n\t\tclient: &http.Client{\n\t\t\tTimeout: time.Duration(timeout) * time.Second,\n\t\t},\n\t}\n\thttpNotify.mustRegisterBuiltinHTTPPlugin()\n\n\treturn httpNotify\n}\n\n// TODO(chef): Dispose\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nfunc (h *HttpNotify) NotifyServerStart(info base.LalInfo) {\n\tinfo.ServerId = h.serverId\n\th.publish(HookEventServerStart, info)\n}\n\nfunc (h *HttpNotify) NotifyUpdate(info base.UpdateInfo) {\n\tinfo.ServerId = h.serverId\n\tinfo.Groups = h.stats.MergeGroups(info.Groups)\n\th.publish(HookEventUpdate, info)\n}\n\nfunc (h *HttpNotify) NotifyGroupStart(info HookGroupInfo) {\n\tinfo.ServerId = h.serverId\n\th.publish(HookEventGroupStart, info)\n}\n\nfunc (h *HttpNotify) NotifyGroupStop(info HookGroupInfo) {\n\tinfo.ServerId = h.serverId\n\th.publish(HookEventGroupStop, info)\n}\n\nfunc (h *HttpNotify) NotifyStreamActive(info HookGroupInfo) {\n\tinfo.ServerId = h.serverId\n\th.publish(HookEventStreamActive, info)\n}\n\nfunc (h *HttpNotify) NotifyPubStart(info base.PubStartInfo) {\n\tinfo.ServerId = h.serverId\n\th.publish(HookEventPubStart, info)\n\n\tif !h.cfg.HasZlmHooks() {\n\t\treturn\n\t}\n\t// --- ZLM 兼容：派生 on_publish + on_stream_changed ---\n\th.publish(HookEventPublish, ZlmOnPublishPayload{\n\t\tMediaServerID: h.serverId,\n\t\tApp:           info.AppName,\n\t\tSchema:        info.Protocol,\n\t\tStream:        info.StreamName,\n\t\tVhost:         \"__defaultVhost__\",\n\t})\n\th.publish(HookEventStreamChanged, ZlmOnStreamChangedPayload{\n\t\tRegist:        true,\n\t\tApp:           info.AppName,\n\t\tStream:        info.StreamName,\n\t\tAppName:       info.AppName,\n\t\tStreamName:    info.StreamName,\n\t\tSchema:        info.Protocol,\n\t\tMediaServerID: h.serverId,\n\t\tVhost:         \"__defaultVhost__\",\n\t})\n}\n\nfunc (h *HttpNotify) NotifyPubStop(info base.PubStopInfo) {\n\tinfo.ServerId = h.serverId\n\th.publish(HookEventPubStop, info)\n\n\tif !h.cfg.HasZlmHooks() {\n\t\treturn\n\t}\n\t// --- ZLM 兼容：派生 on_stream_changed(regist=false) ---\n\th.publish(HookEventStreamChanged, ZlmOnStreamChangedPayload{\n\t\tRegist:        false,\n\t\tApp:           info.AppName,\n\t\tStream:        info.StreamName,\n\t\tAppName:       info.AppName,\n\t\tStreamName:    info.StreamName,\n\t\tSchema:        info.Protocol,\n\t\tMediaServerID: h.serverId,\n\t\tVhost:         \"__defaultVhost__\",\n\t})\n}\n\nfunc (h *HttpNotify) NotifySubStart(info base.SubStartInfo) {\n\tinfo.ServerId = h.serverId\n\th.publish(HookEventSubStart, info)\n\n\tif !h.cfg.HasZlmHooks() {\n\t\treturn\n\t}\n\t// --- ZLM 兼容：派生 on_play ---\n\th.publish(HookEventPlay, ZlmOnPlayPayload{\n\t\tMediaServerID: h.serverId,\n\t\tApp:           info.AppName,\n\t\tSchema:        info.Protocol,\n\t\tStream:        info.StreamName,\n\t\tVhost:         \"__defaultVhost__\",\n\t})\n}\n\nfunc (h *HttpNotify) NotifySubStop(info base.SubStopInfo) {\n\tinfo.ServerId = h.serverId\n\th.publish(HookEventSubStop, info)\n\n\tif h.cfg.ZlmOnStreamNoneReader == \"\" || h.subCountFn == nil {\n\t\treturn\n\t}\n\t// 检查该流是否已无观看者，触发 on_stream_none_reader\n\tif h.subCountFn(info.StreamName) <= 0 {\n\t\th.NotifyStreamNoneReader(ZlmOnStreamNoneReaderPayload{\n\t\t\tApp:    info.AppName,\n\t\t\tSchema: info.Protocol,\n\t\t\tStream: info.StreamName,\n\t\t\tVhost:  \"__defaultVhost__\",\n\t\t})\n\t}\n}\n\nfunc (h *HttpNotify) NotifyPullStart(info base.PullStartInfo) {\n\tinfo.ServerId = h.serverId\n\th.publish(HookEventRelayPullStart, info)\n}\n\nfunc (h *HttpNotify) NotifyPullStop(info base.PullStopInfo) {\n\tinfo.ServerId = h.serverId\n\th.publish(HookEventRelayPullStop, info)\n}\n\nfunc (h *HttpNotify) NotifyRtmpConnect(info base.RtmpConnectInfo) {\n\tinfo.ServerId = h.serverId\n\th.publish(HookEventRtmpConnect, info)\n}\n\nfunc (h *HttpNotify) NotifyOnHlsMakeTs(info base.HlsMakeTsInfo) {\n\tinfo.ServerId = h.serverId\n\th.publish(HookEventHlsMakeTs, info)\n}\n\nfunc (h *HttpNotify) NotifyStreamChanged(info ZlmOnStreamChangedPayload) {\n\tif info.MediaServerID == \"\" {\n\t\tinfo.MediaServerID = h.serverId\n\t}\n\th.publish(HookEventStreamChanged, info)\n}\n\nfunc (h *HttpNotify) NotifyServerKeepalive() {\n\th.publish(HookEventServerKeepalive, ZlmOnServerKeepalivePayload{\n\t\tMediaServerID: h.serverId,\n\t})\n}\n\nfunc (h *HttpNotify) NotifyStreamNoneReader(info ZlmOnStreamNoneReaderPayload) {\n\tif info.MediaServerID == \"\" {\n\t\tinfo.MediaServerID = h.serverId\n\t}\n\th.publish(HookEventStreamNoneReader, info)\n}\n\nfunc (h *HttpNotify) NotifyRtpServerTimeout(info ZlmOnRtpServerTimeoutPayload) {\n\tif info.MediaServerID == \"\" {\n\t\tinfo.MediaServerID = h.serverId\n\t}\n\th.publish(HookEventRtpServerTimeout, info)\n}\n\nfunc (h *HttpNotify) NotifyRecordMp4(info ZlmOnRecordMp4Payload) {\n\tif info.MediaServerID == \"\" {\n\t\tinfo.MediaServerID = h.serverId\n\t}\n\th.publish(HookEventRecordMp4, info)\n}\n\nfunc (h *HttpNotify) NotifyPublish(info ZlmOnPublishPayload) {\n\tif info.MediaServerID == \"\" {\n\t\tinfo.MediaServerID = h.serverId\n\t}\n\th.publish(HookEventPublish, info)\n}\n\nfunc (h *HttpNotify) NotifyPlay(info ZlmOnPlayPayload) {\n\tif info.MediaServerID == \"\" {\n\t\tinfo.MediaServerID = h.serverId\n\t}\n\th.publish(HookEventPlay, info)\n}\n\nfunc (h *HttpNotify) NotifyStreamNotFound(info ZlmOnStreamNotFoundPayload) {\n\tif info.MediaServerID == \"\" {\n\t\tinfo.MediaServerID = h.serverId\n\t}\n\th.publish(HookEventStreamNotFound, info)\n}\n\n// ----- implement INotifyHandler interface ----------------------------------------------------------------------------\n\nfunc (h *HttpNotify) OnServerStart(info base.LalInfo) {\n\th.NotifyServerStart(info)\n}\n\nfunc (h *HttpNotify) OnUpdate(info base.UpdateInfo) {\n\th.NotifyUpdate(info)\n}\n\nfunc (h *HttpNotify) OnGroupStart(info HookGroupInfo) {\n\th.NotifyGroupStart(info)\n}\n\nfunc (h *HttpNotify) OnGroupStop(info HookGroupInfo) {\n\th.NotifyGroupStop(info)\n}\n\nfunc (h *HttpNotify) OnStreamActive(info HookGroupInfo) {\n\th.NotifyStreamActive(info)\n}\n\nfunc (h *HttpNotify) OnPubStart(info base.PubStartInfo) {\n\th.NotifyPubStart(info)\n}\n\nfunc (h *HttpNotify) OnPubStop(info base.PubStopInfo) {\n\th.NotifyPubStop(info)\n}\n\nfunc (h *HttpNotify) OnSubStart(info base.SubStartInfo) {\n\th.NotifySubStart(info)\n}\n\nfunc (h *HttpNotify) OnSubStop(info base.SubStopInfo) {\n\th.NotifySubStop(info)\n}\n\nfunc (h *HttpNotify) OnRelayPullStart(info base.PullStartInfo) {\n\th.NotifyPullStart(info)\n}\n\nfunc (h *HttpNotify) OnRelayPullStop(info base.PullStopInfo) {\n\th.NotifyPullStop(info)\n}\n\nfunc (h *HttpNotify) OnRtmpConnect(info base.RtmpConnectInfo) {\n\th.NotifyRtmpConnect(info)\n}\n\nfunc (h *HttpNotify) OnHlsMakeTs(info base.HlsMakeTsInfo) {\n\th.NotifyOnHlsMakeTs(info)\n}\n\nfunc (h *HttpNotify) asyncPostEvent(url string, event HookEvent) {\n\tif !h.cfg.Enable || url == \"\" {\n\t\treturn\n\t}\n\n\th.dispatchHTTPPost(h.newHookHTTPPostTask(url, event))\n}\n\nfunc (h *HttpNotify) newHookHTTPPostTask(url string, event HookEvent) hookHTTPPostTask {\n\treturn hookHTTPPostTask{\n\t\turl:       url,\n\t\torderKey:  buildHookHTTPOrderKey(url, event),\n\t\teventName: event.Event,\n\t\tpayload:   append([]byte(nil), event.Payload...),\n\t}\n}\n\nfunc buildHookHTTPOrderKey(url string, event HookEvent) string {\n\tif event.Event == HookEventUpdate {\n\t\treturn url + \"|__update__\"\n\t}\n\tif len(event.groupKeys) == 1 {\n\t\tkey := event.groupKeys[0]\n\t\tif key.AppName != \"\" && key.StreamName != \"\" {\n\t\t\treturn fmt.Sprintf(\"__stream__|%s|%s\", key.AppName, key.StreamName)\n\t\t}\n\t}\n\tif event.appName != \"\" && event.streamName != \"\" {\n\t\treturn fmt.Sprintf(\"__stream__|%s|%s\", event.appName, event.streamName)\n\t}\n\treturn url + \"|__global__\"\n}\n\nfunc (h *HttpNotify) dispatchHTTPPost(task hookHTTPPostTask) {\n\th.httpPostMux.Lock()\n\tworker, ok := h.httpPosts[task.orderKey]\n\tif !ok {\n\t\tworker = &hookHTTPPostWorker{\n\t\t\tqueue: make(chan hookHTTPPostTask, maxTaskLen),\n\t\t}\n\t\th.httpPosts[task.orderKey] = worker\n\t\tgo h.runHTTPPostWorker(task.orderKey, worker)\n\t}\n\n\tselect {\n\tcase worker.queue <- task:\n\tdefault:\n\t\tLog.Warnf(\"http notify queue full. key=%s, event=%s, url=%s\", task.orderKey, task.eventName, task.url)\n\t}\n\th.httpPostMux.Unlock()\n}\n\nfunc (h *HttpNotify) runHTTPPostWorker(orderKey string, worker *hookHTTPPostWorker) {\n\ttimer := time.NewTimer(hookHTTPPostWorkerIdleAfter)\n\tdefer timer.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase task, ok := <-worker.queue:\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif !timer.Stop() {\n\t\t\t\tselect {\n\t\t\t\tcase <-timer.C:\n\t\t\t\tdefault:\n\t\t\t\t}\n\t\t\t}\n\t\t\th.postRaw(task.url, task.payload)\n\t\t\ttimer.Reset(hookHTTPPostWorkerIdleAfter)\n\t\tcase <-timer.C:\n\t\t\th.httpPostMux.Lock()\n\t\t\tcurrent, exists := h.httpPosts[orderKey]\n\t\t\tif exists && current == worker && len(worker.queue) == 0 {\n\t\t\t\tdelete(h.httpPosts, orderKey)\n\t\t\t\tclose(worker.queue)\n\t\t\t\th.httpPostMux.Unlock()\n\t\t\t\treturn\n\t\t\t}\n\t\t\th.httpPostMux.Unlock()\n\t\t\ttimer.Reset(hookHTTPPostWorkerIdleAfter)\n\t\t}\n\t}\n}\n\nfunc (h *HttpNotify) postRaw(url string, payload []byte) {\n\tif h == nil || url == \"\" || len(payload) == 0 {\n\t\treturn\n\t}\n\n\tbody := bytes.NewBuffer(payload)\n\tclient := h.client\n\tif client == nil {\n\t\tclient = http.DefaultClient\n\t}\n\tresp, err := client.Post(url, nazahttp.HeaderFieldContentType, body)\n\tif err != nil {\n\t\tLog.Errorf(\"http notify post raw payload error. err=%+v, url=%s, payload=%s\", err, url, string(payload))\n\t\treturn\n\t}\n\tif resp != nil && resp.Body != nil {\n\t\t_, _ = io.Copy(io.Discard, resp.Body)\n\t\t_ = resp.Body.Close()\n\t}\n}\n\nfunc (h *HttpNotify) Recent(limit int) []HookEvent {\n\th.historyMux.RLock()\n\tdefer h.historyMux.RUnlock()\n\n\tif limit <= 0 || limit > len(h.history) {\n\t\tlimit = len(h.history)\n\t}\n\n\tstart := len(h.history) - limit\n\tout := make([]HookEvent, limit)\n\tcopy(out, h.history[start:])\n\treturn out\n}\n\nfunc (h *HttpNotify) RecentFiltered(limit int, filter HookEventFilter) []HookEvent {\n\th.historyMux.RLock()\n\tdefer h.historyMux.RUnlock()\n\n\tif limit <= 0 || limit > len(h.history) {\n\t\tlimit = len(h.history)\n\t}\n\n\tout := make([]HookEvent, 0, limit)\n\tfor i := len(h.history) - 1; i >= 0 && len(out) < limit; i-- {\n\t\tif !filter.Match(h.history[i]) {\n\t\t\tcontinue\n\t\t}\n\t\tout = append(out, h.history[i])\n\t}\n\n\tfor i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {\n\t\tout[i], out[j] = out[j], out[i]\n\t}\n\treturn out\n}\n\nfunc (h *HttpNotify) Subscribe(buffer int) (int64, <-chan HookEvent, func()) {\n\tif buffer <= 0 {\n\t\tbuffer = hookSubBufSize\n\t}\n\n\tid := h.subID.Add(1)\n\tch := make(chan HookEvent, buffer)\n\n\th.subscriberM.Lock()\n\th.subscribers[id] = ch\n\th.subscriberM.Unlock()\n\n\tcancel := func() {\n\t\th.subscriberM.Lock()\n\t\tif sub, ok := h.subscribers[id]; ok {\n\t\t\tdelete(h.subscribers, id)\n\t\t\tclose(sub)\n\t\t}\n\t\th.subscriberM.Unlock()\n\t}\n\n\treturn id, ch, cancel\n}\n\nfunc (h *HttpNotify) publish(event string, info interface{}) {\n\tif h == nil {\n\t\treturn\n\t}\n\n\tpayload, err := json.Marshal(info)\n\tif err != nil {\n\t\tLog.Errorf(\"marshal hook event failed. event=%s, err=%+v\", event, err)\n\t\treturn\n\t}\n\n\thookEvent := HookEvent{\n\t\tID:        h.eventID.Add(1),\n\t\tEvent:     event,\n\t\tTimestamp: time.Now().Format(time.RFC3339Nano),\n\t\tPayload:   payload,\n\t}\n\tpopulateHookEventMeta(&hookEvent, info)\n\n\th.historyMux.Lock()\n\th.history = append(h.history, hookEvent)\n\tif len(h.history) > hookHistorySize {\n\t\th.history = append([]HookEvent(nil), h.history[len(h.history)-hookHistorySize:]...)\n\t}\n\th.historyMux.Unlock()\n\n\th.dispatchPlugins(hookEvent)\n\n\th.subscriberM.RLock()\n\tstale := make([]int64, 0)\n\tfor id, ch := range h.subscribers {\n\t\tselect {\n\t\tcase ch <- hookEvent:\n\t\tdefault:\n\t\t\tstale = append(stale, id)\n\t\t}\n\t}\n\th.subscriberM.RUnlock()\n\n\tif len(stale) == 0 {\n\t\treturn\n\t}\n\n\th.subscriberM.Lock()\n\tfor _, id := range stale {\n\t\tif ch, ok := h.subscribers[id]; ok {\n\t\t\tdelete(h.subscribers, id)\n\t\t\tclose(ch)\n\t\t}\n\t}\n\th.subscriberM.Unlock()\n}\n\nfunc populateHookEventMeta(event *HookEvent, info interface{}) {\n\tif event == nil || info == nil {\n\t\treturn\n\t}\n\n\tswitch v := info.(type) {\n\tcase base.UpdateInfo:\n\t\tevent.groupKeys = make([]maxlogic.StreamKey, 0, len(v.Groups))\n\t\tfor _, group := range v.Groups {\n\t\t\tevent.groupKeys = append(event.groupKeys, maxlogic.NewStreamKey(group.AppName, group.StreamName))\n\t\t}\n\tcase HookGroupInfo:\n\t\tevent.streamName = v.StreamName\n\t\tevent.appName = v.AppName\n\tcase base.PubStartInfo:\n\t\tpopulateHookSessionMeta(event, v.SessionEventCommonInfo)\n\tcase base.PubStopInfo:\n\t\tpopulateHookSessionMeta(event, v.SessionEventCommonInfo)\n\tcase base.SubStartInfo:\n\t\tpopulateHookSessionMeta(event, v.SessionEventCommonInfo)\n\tcase base.SubStopInfo:\n\t\tpopulateHookSessionMeta(event, v.SessionEventCommonInfo)\n\tcase base.PullStartInfo:\n\t\tpopulateHookSessionMeta(event, v.SessionEventCommonInfo)\n\tcase base.PullStopInfo:\n\t\tpopulateHookSessionMeta(event, v.SessionEventCommonInfo)\n\tcase base.RtmpConnectInfo:\n\t\tevent.sessionID = v.SessionId\n\t\tevent.appName = v.App\n\tcase base.HlsMakeTsInfo:\n\t\tevent.streamName = v.StreamName\n\tcase ZlmOnStreamChangedPayload:\n\t\tevent.appName = v.App\n\t\tevent.streamName = v.Stream\n\t\tif event.appName == \"\" {\n\t\t\tevent.appName = v.AppName\n\t\t}\n\t\tif event.streamName == \"\" {\n\t\t\tevent.streamName = v.StreamName\n\t\t}\n\tcase ZlmOnStreamNoneReaderPayload:\n\t\tevent.appName = v.App\n\t\tevent.streamName = v.Stream\n\tcase ZlmOnRtpServerTimeoutPayload:\n\t\tevent.streamName = v.StreamID\n\tcase ZlmOnRecordMp4Payload:\n\t\tevent.appName = v.App\n\t\tevent.streamName = v.Stream\n\tcase ZlmOnPublishPayload:\n\t\tevent.appName = v.App\n\t\tevent.streamName = v.Stream\n\tcase ZlmOnPlayPayload:\n\t\tevent.appName = v.App\n\t\tevent.streamName = v.Stream\n\tcase ZlmOnStreamNotFoundPayload:\n\t\tevent.appName = v.App\n\t\tevent.streamName = v.Stream\n\t\tif event.appName == \"\" {\n\t\t\tevent.appName = v.AppName\n\t\t}\n\t\tif event.streamName == \"\" {\n\t\t\tevent.streamName = v.StreamName\n\t\t}\n\t}\n}\n\nfunc populateHookSessionMeta(event *HookEvent, info base.SessionEventCommonInfo) {\n\tif event == nil {\n\t\treturn\n\t}\n\n\tevent.sessionID = info.SessionId\n\tevent.streamName = info.StreamName\n\tevent.appName = info.AppName\n}\n"
  },
  {
    "path": "server/middle.go",
    "content": "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 *LalMaxServer) Cors() gin.HandlerFunc {\n\treturn func(c *gin.Context) {\n\t\tmethod := c.Request.Method\n\t\torigin := c.GetHeader(\"Origin\")\n\t\tif len(origin) == 0 {\n\t\t\tc.Header(\"Access-Control-Allow-Origin\", \"*\")\n\t\t} else {\n\t\t\tc.Header(\"Access-Control-Allow-Origin\", origin)\n\t\t}\n\t\t//服务器支持的所有跨域请求的方法\n\t\tc.Header(\"Access-Control-Allow-Methods\", \"POST, GET, OPTIONS, PUT, DELETE,UPDATE\")\n\t\t//允许跨域设置可以返回其他子段，可以自定义字段\n\t\tc.Header(\"Access-Control-Allow-Headers\", \"*\")\n\t\tc.Header(\"Access-Control-Allow-Headers\", \"Content-Type,Access-Token\")\n\t\tc.Header(\"Access-Control-Allow-Credentials\", \"true\")\n\t\tc.Header(\"Cross-Origin-Resource-Policy\", \"cross-origin\")\n\n\t\t//允许类型校验\n\t\tif method == \"OPTIONS\" {\n\t\t\tc.AbortWithStatus(http.StatusNoContent)\n\t\t}\n\t\tc.Next()\n\t}\n}\n\n// Authentication 接口鉴权\nfunc Authentication(secrets, ips []string) gin.HandlerFunc {\n\tout := base.ApiRespBasic{\n\t\tErrorCode: http.StatusUnauthorized,\n\t\tDesp:      http.StatusText(http.StatusUnauthorized),\n\t}\n\treturn func(c *gin.Context) {\n\t\tif !authentication(c.Query(\"token\"), c.ClientIP(), secrets, ips) {\n\t\t\tc.JSON(200, out)\n\t\t\treturn\n\t\t}\n\t\tc.Next()\n\t}\n}\n\n// authentication 判断是否符合要求，返回 false 表示鉴权失败\nfunc authentication(reqToken, clientIP string, secrets, ips []string) bool {\n\t// 秘钥过滤\n\tif len(secrets) > 0 && !containFn(secrets, reqToken) {\n\t\treturn false\n\t}\n\t// ip 白名单过滤\n\tif len(ips) > 0 && !containFn(ips, clientIP) {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc containFn[T comparable](ts []T, t T) bool {\n\tfor _, v := range ts {\n\t\tif v == t {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "server/router.go",
    "content": "package server\n\nimport \"github.com/gin-gonic/gin\"\n\nfunc (s *LalMaxServer) InitRouter(router *gin.Engine) {\n\tif router == nil {\n\t\treturn\n\t}\n\trouter.Use(s.Cors())\n\n\ts.initRtcRouter(router)\n\ts.initFmp4Router(router)\n\n\tauth := Authentication(s.conf.HttpConfig.CtrlAuthWhitelist.Secrets, s.conf.HttpConfig.CtrlAuthWhitelist.IPs)\n\ts.initHookRouter(router, auth)\n\ts.initStatRouter(router, auth)\n\ts.initCtrlRouter(router, auth)\n\ts.initZlmCompatRouter(router, auth)\n\n\ts.initFlvProxy(router)\n}\n"
  },
  {
    "path": "server/router_ctrl.go",
    "content": "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/q191201771/lal/pkg/logic\"\n)\n\nfunc (s *LalMaxServer) initCtrlRouter(router *gin.Engine, handlers ...gin.HandlerFunc) {\n\tctrl := router.Group(\"/api/ctrl\", handlers...)\n\tctrl.POST(\"/start_relay_pull\", s.ctrlStartRelayPullHandler)\n\tctrl.GET(\"/stop_relay_pull\", s.ctrlStopRelayPullHandler)\n\tctrl.POST(\"/stop_relay_pull\", s.ctrlStopRelayPullHandler)\n\tctrl.POST(\"/kick_session\", s.ctrlKickSessionHandler)\n\tctrl.POST(\"/start_rtp_pub\", s.ctrlStartRtpPubHandler)\n\tctrl.POST(\"/stop_rtp_pub\", s.ctrlStopRtpPubHandler)\n}\n\nfunc (s *LalMaxServer) ctrlStartRelayPullHandler(c *gin.Context) {\n\tvar info base.ApiCtrlStartRelayPullReq\n\tvar v base.ApiCtrlStartRelayPullResp\n\tj, err := unmarshalRequestJSONBody(c.Request, &info, \"url\")\n\tif err != nil {\n\t\tLog.Warnf(\"http api start pull error. err=%+v\", err)\n\t\tv.ErrorCode = base.ErrorCodeParamMissing\n\t\tv.Desp = base.DespParamMissing\n\t\tc.JSON(http.StatusOK, v)\n\t\treturn\n\t}\n\n\tif !j.Exist(\"pull_timeout_ms\") {\n\t\tinfo.PullTimeoutMs = logic.DefaultApiCtrlStartRelayPullReqPullTimeoutMs\n\t}\n\tif !j.Exist(\"pull_retry_num\") {\n\t\tinfo.PullRetryNum = base.PullRetryNumNever\n\t}\n\tif !j.Exist(\"auto_stop_pull_after_no_out_ms\") {\n\t\tinfo.AutoStopPullAfterNoOutMs = base.AutoStopPullAfterNoOutMsNever\n\t}\n\tif !j.Exist(\"rtsp_mode\") {\n\t\tinfo.RtspMode = base.RtspModeTcp\n\t}\n\n\tLog.Infof(\"http api start pull. req info=%+v\", info)\n\n\tresp := s.lalsvr.CtrlStartRelayPull(info)\n\tc.JSON(http.StatusOK, resp)\n}\n\nfunc (s *LalMaxServer) ctrlStopRelayPullHandler(c *gin.Context) {\n\tvar v base.ApiCtrlStopRelayPullResp\n\tstreamName := c.Query(\"stream_name\")\n\tif streamName == \"\" {\n\t\tv.ErrorCode = base.ErrorCodeParamMissing\n\t\tv.Desp = base.DespParamMissing\n\t\tc.JSON(http.StatusOK, v)\n\t\treturn\n\t}\n\n\tLog.Infof(\"http api stop pull. stream_name=%s\", streamName)\n\n\tresp := s.lalsvr.CtrlStopRelayPull(streamName)\n\tc.JSON(http.StatusOK, resp)\n}\n\nfunc (s *LalMaxServer) ctrlKickSessionHandler(c *gin.Context) {\n\tvar v base.ApiCtrlKickSessionResp\n\tvar info base.ApiCtrlKickSessionReq\n\n\t_, err := unmarshalRequestJSONBody(c.Request, &info, \"stream_name\", \"session_id\")\n\tif err != nil {\n\t\tLog.Warnf(\"http api kick session error. err=%+v\", err)\n\t\tv.ErrorCode = base.ErrorCodeParamMissing\n\t\tv.Desp = base.DespParamMissing\n\t\tc.JSON(http.StatusOK, v)\n\t\treturn\n\t}\n\n\tLog.Infof(\"http api kick session. req info=%+v\", info)\n\n\tresp := s.lalsvr.CtrlKickSession(info)\n\tc.JSON(http.StatusOK, resp)\n}\n\nfunc (s *LalMaxServer) ctrlStartRtpPubHandler(c *gin.Context) {\n\tvar v base.ApiCtrlStartRtpPubResp\n\tvar info base.ApiCtrlStartRtpPubReq\n\n\tj, err := unmarshalRequestJSONBody(c.Request, &info, \"stream_name\")\n\tif err != nil {\n\t\tLog.Warnf(\"http api start rtp pub error. err=%+v\", err)\n\t\tv.ErrorCode = base.ErrorCodeParamMissing\n\t\tv.Desp = base.DespParamMissing\n\t\tc.JSON(http.StatusOK, v)\n\t\treturn\n\t}\n\n\tif !j.Exist(\"timeout_ms\") {\n\t\tinfo.TimeoutMs = logic.DefaultApiCtrlStartRtpPubReqTimeoutMs\n\t}\n\n\tLog.Infof(\"http api start rtp pub. req info=%+v\", info)\n\n\tresp := s.rtpPubMgr.Start(info)\n\tc.JSON(http.StatusOK, resp)\n}\n\nfunc (s *LalMaxServer) ctrlStopRtpPubHandler(c *gin.Context) {\n\tvar v base.ApiCtrlStopRelayPullResp\n\tstreamName := c.Query(\"stream_name\")\n\tsessionID := c.Query(\"session_id\")\n\n\tif streamName == \"\" && sessionID == \"\" {\n\t\tvar info base.ApiCtrlKickSessionReq\n\t\tif _, err := unmarshalRequestJSONBody(c.Request, &info); err == nil {\n\t\t\tstreamName = info.StreamName\n\t\t\tsessionID = info.SessionId\n\t\t}\n\t}\n\n\tif streamName == \"\" && sessionID == \"\" {\n\t\tv.ErrorCode = base.ErrorCodeParamMissing\n\t\tv.Desp = base.DespParamMissing\n\t\tc.JSON(http.StatusOK, v)\n\t\treturn\n\t}\n\n\tLog.Infof(\"http api stop rtp pub. stream_name=%s, session_id=%s\", streamName, sessionID)\n\n\tsession, err := s.rtpPubMgr.Stop(streamName, sessionID)\n\tif err != nil {\n\t\tv.ErrorCode = base.ErrorCodeSessionNotFound\n\t\tv.Desp = err.Error()\n\t\tc.JSON(http.StatusOK, v)\n\t\treturn\n\t}\n\n\tv.ErrorCode = base.ErrorCodeSucc\n\tv.Desp = base.DespSucc\n\tv.Data.SessionId = session.ID\n\tc.JSON(http.StatusOK, v)\n}\n"
  },
  {
    "path": "server/router_flv_proxy.go",
    "content": "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/q191201771/naza/pkg/nazalog\"\n)\n\n// initFlvProxy 注册 NoRoute 兜底，将 .flv 请求代理到 lal 的 httpflv 服务\n// 为什么：ZLM 的 FLV 拉流路径是 /{app}/{stream}.live.flv，lal 的 httpflv 在独立端口，\n// lalmax 不直接提供 httpflv，通过反向代理让外部只需访问 lalmax 单一端口\nfunc (s *LalMaxServer) initFlvProxy(router *gin.Engine) {\n\trouter.NoRoute(func(c *gin.Context) {\n\t\tpath := c.Request.URL.Path\n\t\tif !strings.HasSuffix(path, \".flv\") {\n\t\t\tc.Status(http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\n\t\tlalHTTPAddr := s.getLalHttpflvAddr()\n\t\tif lalHTTPAddr == \"\" {\n\t\t\tc.Status(http.StatusBadGateway)\n\t\t\treturn\n\t\t}\n\n\t\ttargetURL := \"http://\" + lalHTTPAddr + path\n\t\tif c.Request.URL.RawQuery != \"\" {\n\t\t\ttargetURL += \"?\" + c.Request.URL.RawQuery\n\t\t}\n\n\t\tnazalog.Debugf(\"flv proxy. path=%s, target=%s\", path, targetURL)\n\n\t\treq, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, targetURL, nil)\n\t\tif err != nil {\n\t\t\tnazalog.Errorf(\"flv proxy create request failed. err=%v\", err)\n\t\t\tc.Status(http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tfor k, vs := range c.Request.Header {\n\t\t\tfor _, v := range vs {\n\t\t\t\treq.Header.Add(k, v)\n\t\t\t}\n\t\t}\n\n\t\tresp, err := http.DefaultClient.Do(req)\n\t\tif err != nil {\n\t\t\tnazalog.Errorf(\"flv proxy request failed. target=%s, err=%v\", targetURL, err)\n\t\t\tc.Status(http.StatusBadGateway)\n\t\t\treturn\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tfor k, vs := range resp.Header {\n\t\t\tfor _, v := range vs {\n\t\t\t\tc.Header(k, v)\n\t\t\t}\n\t\t}\n\t\tc.Status(resp.StatusCode)\n\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\treturn\n\t\t}\n\n\t\tc.Header(\"Transfer-Encoding\", \"chunked\")\n\t\tc.Writer.Flush()\n\t\tio.Copy(c.Writer, resp.Body)\n\t})\n}\n\n// getLalHttpflvAddr 从 lal 原始配置中提取 httpflv 服务地址\nfunc (s *LalMaxServer) getLalHttpflvAddr() string {\n\tif len(s.conf.LalRawContent) == 0 {\n\t\treturn \"\"\n\t}\n\n\tvar raw struct {\n\t\tDefaultHTTP struct {\n\t\t\tAddr string `json:\"http_listen_addr\"`\n\t\t} `json:\"default_http\"`\n\t}\n\n\tif err := json.Unmarshal(s.conf.LalRawContent, &raw); err != nil {\n\t\treturn \"\"\n\t}\n\n\taddr := raw.DefaultHTTP.Addr\n\tif addr == \"\" {\n\t\treturn \"\"\n\t}\n\tif addr[0] == ':' {\n\t\treturn \"127.0.0.1\" + addr\n\t}\n\treturn addr\n}\n"
  },
  {
    "path": "server/router_fmp4.go",
    "content": "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 *LalMaxServer) initFmp4Router(router *gin.Engine) {\n\trouter.GET(\"/live/m4s/:streamid\", s.HandleHttpFmp4)\n\trouter.GET(\"/live/hls/:streamid/:type\", s.HandleHls)\n}\n\nfunc (s *LalMaxServer) HandleHls(c *gin.Context) {\n\tif s.hlssvr != nil {\n\t\ts.hlssvr.HandleRequest(c)\n\t} else {\n\t\tnazalog.Error(\"hls is disable\")\n\t\tc.Status(http.StatusNotFound)\n\t}\n}\n\nfunc (s *LalMaxServer) HandleHttpFmp4(c *gin.Context) {\n\tif s.httpfmp4svr != nil {\n\t\ts.httpfmp4svr.HandleRequest(c)\n\t} else {\n\t\tnazalog.Error(\"http-fmp4 is disable\")\n\t\tc.Status(http.StatusNotFound)\n\t}\n}\n"
  },
  {
    "path": "server/router_helper.go",
    "content": "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/q191201771/naza/pkg/nazajson\"\n)\n\nfunc unmarshalRequestJSONBody(r *http.Request, info interface{}, keyFieldList ...string) (nazajson.Json, error) {\n\tbody, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\treturn nazajson.Json{}, err\n\t}\n\n\tj, err := nazajson.New(body)\n\tif err != nil {\n\t\treturn j, err\n\t}\n\tfor _, kf := range keyFieldList {\n\t\tif !j.Exist(kf) {\n\t\t\treturn j, nazahttp.ErrParamMissing\n\t\t}\n\t}\n\n\treturn j, json.Unmarshal(body, info)\n}\n"
  },
  {
    "path": "server/router_hook.go",
    "content": "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\"\n)\n\nfunc (s *LalMaxServer) initHookRouter(router *gin.Engine, handlers ...gin.HandlerFunc) {\n\thook := router.Group(\"/api/hook\", handlers...)\n\thook.GET(\"/recent\", s.hookRecentHandler)\n\thook.GET(\"/stream\", s.hookStreamHandler)\n}\n\nfunc (s *LalMaxServer) hookRecentHandler(c *gin.Context) {\n\tvar out struct {\n\t\tbase.ApiRespBasic\n\t\tData struct {\n\t\t\tEvents []HookEvent `json:\"events\"`\n\t\t} `json:\"data\"`\n\t}\n\n\tlimit := 20\n\tif v := c.Query(\"limit\"); v != \"\" {\n\t\tif parsed, err := strconv.Atoi(v); err == nil && parsed > 0 {\n\t\t\tlimit = parsed\n\t\t}\n\t}\n\teventNames := ParseHookEventNames(c.Query(\"events\"))\n\tif eventName := c.Query(\"event\"); eventName != \"\" {\n\t\teventNames = append(eventNames, eventName)\n\t}\n\tfilter := NewHookEventFilter(c.Query(\"app_name\"), c.Query(\"stream_name\"), c.Query(\"session_id\"), eventNames)\n\n\tout.ErrorCode = base.ErrorCodeSucc\n\tout.Desp = base.DespSucc\n\tout.Data.Events = s.notifyHub.RecentFiltered(limit, filter)\n\tc.JSON(http.StatusOK, out)\n}\n\nfunc (s *LalMaxServer) hookStreamHandler(c *gin.Context) {\n\tif s.notifyHub == nil {\n\t\tc.JSON(http.StatusOK, base.ApiRespBasic{\n\t\t\tErrorCode: http.StatusInternalServerError,\n\t\t\tDesp:      \"hook hub not initialized\",\n\t\t})\n\t\treturn\n\t}\n\n\tflusher, ok := c.Writer.(http.Flusher)\n\tif !ok {\n\t\tc.JSON(http.StatusOK, base.ApiRespBasic{\n\t\t\tErrorCode: http.StatusInternalServerError,\n\t\t\tDesp:      \"streaming unsupported\",\n\t\t})\n\t\treturn\n\t}\n\n\tc.Header(\"Content-Type\", \"text/event-stream\")\n\tc.Header(\"Cache-Control\", \"no-cache\")\n\tc.Header(\"Connection\", \"keep-alive\")\n\tc.Header(\"X-Accel-Buffering\", \"no\")\n\tc.Status(http.StatusOK)\n\n\t_, ch, cancel := s.notifyHub.Subscribe(0)\n\tdefer cancel()\n\n\teventNames := ParseHookEventNames(c.Query(\"events\"))\n\tif eventName := c.Query(\"event\"); eventName != \"\" {\n\t\teventNames = append(eventNames, eventName)\n\t}\n\tfilter := NewHookEventFilter(c.Query(\"app_name\"), c.Query(\"stream_name\"), c.Query(\"session_id\"), eventNames)\n\n\thistory := s.notifyHub.RecentFiltered(20, filter)\n\tlastHistoryID := int64(0)\n\tfor _, event := range history {\n\t\tif event.ID > lastHistoryID {\n\t\t\tlastHistoryID = event.ID\n\t\t}\n\t\tif err := writeHookEventSSE(c.Writer, event); err != nil {\n\t\t\treturn\n\t\t}\n\t\tflusher.Flush()\n\t}\n\n\tfor {\n\t\tselect {\n\t\tcase <-c.Request.Context().Done():\n\t\t\treturn\n\t\tcase event, ok := <-ch:\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif event.ID <= lastHistoryID {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !filter.Match(event) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err := writeHookEventSSE(c.Writer, event); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tflusher.Flush()\n\t\t}\n\t}\n}\n\nfunc writeHookEventSSE(w http.ResponseWriter, event HookEvent) error {\n\tif _, err := fmt.Fprintf(w, \"id: %d\\n\", event.ID); err != nil {\n\t\treturn err\n\t}\n\tif _, err := fmt.Fprintf(w, \"event: %s\\n\", event.Event); err != nil {\n\t\treturn err\n\t}\n\tif _, err := fmt.Fprintf(w, \"data: %s\\n\\n\", event.Payload); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "server/router_rtc.go",
    "content": "package server\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc (s *LalMaxServer) initRtcRouter(router *gin.Engine) {\n\trtc := router.Group(\"/webrtc\")\n\trtc.GET(\"/whip\", s.HandleWHIP)\n\trtc.POST(\"/whip\", s.HandleWHIP)\n\trtc.OPTIONS(\"/whip\", s.HandleWHIP)\n\trtc.DELETE(\"/whip\", s.HandleWHIP)\n\n\trtc.GET(\"/whep\", s.HandleWHEP)\n\trtc.POST(\"/whep\", s.HandleWHEP)\n\trtc.OPTIONS(\"/whep\", s.HandleWHEP)\n\trtc.DELETE(\"/whep\", s.HandleWHEP)\n\n\trtc.POST(\"/play/live/:streamid\", s.HandleJessibuca)\n\trtc.DELETE(\"/play/live/:streamid\", s.HandleJessibuca)\n}\n\nfunc (s *LalMaxServer) HandleWHIP(c *gin.Context) {\n\tswitch c.Request.Method {\n\tcase \"GET\":\n\t\tif s.rtcsvr != nil {\n\t\t\ts.rtcsvr.ServeWHIPPublishPage(c)\n\t\t} else {\n\t\t\tc.Header(\"Content-Type\", \"text/html; charset=utf-8\")\n\t\t\tc.String(http.StatusServiceUnavailable, \"<!doctype html><meta charset=utf-8><title>WHIP</title><p>RTC 未启用：请在配置中将 <code>lalmax.rtc_config.enable</code> 设为 <code>true</code> 并重启服务。</p><p>推流地址示例：<code>/webrtc/whip?streamid=test110</code></p>\")\n\t\t}\n\tcase \"POST\":\n\t\tif s.rtcsvr != nil {\n\t\t\ts.rtcsvr.HandleWHIP(c)\n\t\t} else {\n\t\t\tc.String(http.StatusServiceUnavailable, \"rtc disabled\")\n\t\t}\n\tcase \"OPTIONS\":\n\t\tc.Header(\"Access-Control-Allow-Origin\", \"*\")\n\t\tc.Header(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, OPTIONS\")\n\t\tc.Header(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization\")\n\t\tc.Header(\"Access-Control-Expose-Headers\", \"Location\")\n\t\tc.Header(\"Access-Control-Max-Age\", \"86400\")\n\t\tc.Header(\"Accept-Post\", \"application/sdp\")\n\t\tc.Status(http.StatusNoContent)\n\tcase \"DELETE\":\n\t\t// TODO 实现 DELETE\n\t\tc.Status(http.StatusOK)\n\t}\n}\n\nfunc (s *LalMaxServer) HandleWHEP(c *gin.Context) {\n\tswitch c.Request.Method {\n\tcase \"GET\":\n\t\tif s.rtcsvr != nil {\n\t\t\ts.rtcsvr.ServeWHEPPlayPage(c)\n\t\t} else {\n\t\t\tc.Header(\"Content-Type\", \"text/html; charset=utf-8\")\n\t\t\tc.String(http.StatusServiceUnavailable, \"<!doctype html><meta charset=utf-8><title>WHEP</title><p>RTC 未启用：请在配置中将 <code>lalmax.rtc_config.enable</code> 设为 <code>true</code> 并重启服务。</p><p>播放地址示例：<code>http://127.0.0.1:1290/webrtc/whep?streamid=test110</code>（端口以 <code>http_listen_addr</code> 为准）</p>\")\n\t\t}\n\tcase \"POST\":\n\t\tif s.rtcsvr != nil {\n\t\t\ts.rtcsvr.HandleWHEP(c)\n\t\t} else {\n\t\t\tc.String(http.StatusServiceUnavailable, \"rtc disabled\")\n\t\t}\n\tcase \"OPTIONS\":\n\t\tc.Header(\"Access-Control-Allow-Origin\", \"*\")\n\t\tc.Header(\"Access-Control-Allow-Methods\", \"GET, POST, DELETE, OPTIONS\")\n\t\tc.Header(\"Access-Control-Allow-Headers\", \"Content-Type, Authorization\")\n\t\tc.Header(\"Access-Control-Expose-Headers\", \"Location\")\n\t\tc.Header(\"Access-Control-Max-Age\", \"86400\")\n\t\tc.Header(\"Accept-Post\", \"application/sdp\")\n\t\tc.Status(http.StatusNoContent)\n\tcase \"DELETE\":\n\t\t// TODO 实现 DELETE\n\t\tc.Status(http.StatusOK)\n\t}\n}\n\nfunc (s *LalMaxServer) HandleJessibuca(c *gin.Context) {\n\tswitch c.Request.Method {\n\tcase \"POST\":\n\t\tif s.rtcsvr != nil {\n\t\t\ts.rtcsvr.HandleJessibuca(c)\n\t\t}\n\tcase \"DELETE\":\n\t\t// TODO 实现 DELETE\n\t\tc.Status(http.StatusOK)\n\t}\n}\n"
  },
  {
    "path": "server/router_stat.go",
    "content": "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 \"github.com/q191201771/lalmax/logic\"\n)\n\nfunc (s *LalMaxServer) initStatRouter(router *gin.Engine, handlers ...gin.HandlerFunc) {\n\tstat := router.Group(\"/api/stat\", handlers...)\n\tstat.GET(\"/group\", s.statGroupHandler)\n\tstat.GET(\"/all_group\", s.statAllGroupHandler)\n\tstat.GET(\"/lal_info\", s.statLalInfoHandler)\n}\n\nfunc (s *LalMaxServer) statGroupHandler(c *gin.Context) {\n\tvar v ApiStatGroupResp\n\tstreamName := c.Query(\"stream_name\")\n\tif streamName == \"\" {\n\t\tv.ErrorCode = base.ErrorCodeParamMissing\n\t\tv.Desp = base.DespParamMissing\n\t\tc.JSON(http.StatusOK, v)\n\t\treturn\n\t}\n\tappName := c.Query(\"app_name\")\n\tview := s.stats.FindGroupView(s.lalsvr.StatAllGroup(), maxlogic.NewStreamKey(appName, streamName))\n\tif view == nil {\n\t\tv.ErrorCode = base.ErrorCodeGroupNotFound\n\t\tv.Desp = base.DespGroupNotFound\n\t\tc.JSON(http.StatusOK, v)\n\t\treturn\n\t}\n\tgroup := newLalmaxStatGroup(*view)\n\tv.Data = &group\n\tv.ErrorCode = base.ErrorCodeSucc\n\tv.Desp = base.DespSucc\n\tc.JSON(http.StatusOK, v)\n}\n\nfunc (s *LalMaxServer) statAllGroupHandler(c *gin.Context) {\n\tvar out ApiStatAllGroupResp\n\tout.ErrorCode = base.ErrorCodeSucc\n\tout.Desp = base.DespSucc\n\tout.Data.Groups = newLalmaxStatGroups(s.stats.BuildGroupsView(s.lalsvr.StatAllGroup()))\n\tc.JSON(http.StatusOK, out)\n}\n\nfunc (s *LalMaxServer) statLalInfoHandler(c *gin.Context) {\n\tvar v base.ApiStatLalInfoResp\n\tv.ErrorCode = base.ErrorCodeSucc\n\tv.Desp = base.DespSucc\n\tv.Data = s.lalsvr.StatLalInfo()\n\tc.JSON(http.StatusOK, v)\n}\n"
  },
  {
    "path": "server/router_test.go",
    "content": "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/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\tmaxlogic \"github.com/q191201771/lalmax/logic\"\n\n\tconfig \"github.com/q191201771/lalmax/config\"\n\n\t\"github.com/q191201771/lal/pkg/base\"\n\tbaseLogic \"github.com/q191201771/lal/pkg/logic\"\n)\n\ntype testHookPlugin struct {\n\tname   string\n\tevents chan HookEvent\n}\n\ntype maxlogicTestSubscriber struct {\n\tstat maxlogic.SubscriberStat\n}\n\ntype hookHTTPPayload struct {\n\tSessionID  string `json:\"session_id\"`\n\tAppName    string `json:\"app_name\"`\n\tStreamName string `json:\"stream_name\"`\n}\n\nfunc (s *maxlogicTestSubscriber) OnMsg(msg base.RtmpMsg) {}\n\nfunc (s *maxlogicTestSubscriber) OnStop() {}\n\nfunc (s *maxlogicTestSubscriber) GetSubscriberStat() maxlogic.SubscriberStat {\n\treturn s.stat\n}\n\nfunc (p *testHookPlugin) Name() string {\n\treturn p.name\n}\n\nfunc (p *testHookPlugin) OnHookEvent(event HookEvent) error {\n\tp.events <- event\n\treturn nil\n}\n\nvar max *LalMaxServer\nvar onUpdateHook func(base.UpdateInfo)\nvar onUpdateHookMu sync.RWMutex\nvar testSeq atomic.Int64\n\nconst httpNotifyAddr = \":55559\"\n\nfunc uniqueTestName(prefix string) string {\n\treturn fmt.Sprintf(\"%s_%d\", prefix, testSeq.Add(1))\n}\n\nfunc findTestGroup(groups []LalmaxStatGroup, streamName string) *LalmaxStatGroup {\n\tfor i := range groups {\n\t\tif groups[i].StreamName == streamName {\n\t\t\treturn &groups[i]\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc TestMain(m *testing.M) {\n\tvar err error\n\tmax, err = NewLalMaxServer(&config.Config{\n\t\tFmp4Config: config.Fmp4Config{\n\t\t\tHttp: config.Fmp4HttpConfig{Enable: true},\n\t\t},\n\t\tLalRawContent: []byte(`{\"rtmp\":{\"enable\":false},\"rtsp\":{\"enable\":false},\"http_api\":{\"enable\":false},\"pprof\":{\"enable\":false}}`),\n\t\tHttpConfig: config.HttpConfig{\n\t\t\tListenAddr: \":52349\",\n\t\t},\n\t\tHttpNotifyConfig: config.HttpNotifyConfig{\n\t\t\tEnable:            true,\n\t\t\tUpdateIntervalSec: 2,\n\t\t\tOnUpdate:          fmt.Sprintf(\"http://127.0.0.1%s/on_update\", httpNotifyAddr),\n\t\t},\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\thttp.HandleFunc(\"/on_update\", func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer r.Body.Close()\n\n\t\tvar out base.UpdateInfo\n\t\tif err := json.NewDecoder(r.Body).Decode(&out); err != nil {\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tonUpdateHookMu.RLock()\n\t\thook := onUpdateHook\n\t\tonUpdateHookMu.RUnlock()\n\t\tif hook != nil {\n\t\t\thook(out)\n\t\t}\n\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\n\tln, err := net.Listen(\"tcp\", httpNotifyAddr)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tgo func() {\n\t\t_ = http.Serve(ln, nil)\n\t}()\n\n\tgo max.Run()\n\tos.Exit(m.Run())\n}\n\nfunc TestAllGroup(t *testing.T) {\n\tstreamName := uniqueTestName(\"test_all_group\")\n\t_, err := max.lalsvr.AddCustomizePubSession(streamName)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tt.Run(\"no consumer\", func(t *testing.T) {\n\t\tr := httptest.NewRecorder()\n\t\treq := httptest.NewRequest(\"GET\", \"/api/stat/all_group\", nil)\n\t\tmax.router.ServeHTTP(r, req)\n\t\tresp := r.Result()\n\t\tif resp.StatusCode != 200 {\n\t\t\tt.Fatal(resp.Status)\n\t\t}\n\t\tvar out ApiStatAllGroupResp\n\t\tif err := json.NewDecoder(resp.Body).Decode(&out); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tgroup := findTestGroup(out.Data.Groups, streamName)\n\t\tif group == nil {\n\t\t\tt.Fatal(\"no group\")\n\t\t}\n\t\tif len(group.StatSubs) != 0 {\n\t\t\tt.Fatal(\"subs err\")\n\t\t}\n\t\tif len(group.Lalmax.ExtSubs) != 0 {\n\t\t\tt.Fatal(\"lalmax ext_subs err\")\n\t\t}\n\t})\n\n\tt.Run(\"has consumer\", func(t *testing.T) {\n\t\tss, _ := maxlogic.GetGroupManagerInstance().GetOrCreateGroupByStreamName(streamName, streamName, max.hlssvr, 1, 0)\n\t\tss.AddConsumer(\"consumer1\", nil)\n\n\t\tr := httptest.NewRecorder()\n\t\treq := httptest.NewRequest(\"GET\", \"/api/stat/all_group\", nil)\n\t\tmax.router.ServeHTTP(r, req)\n\t\tresp := r.Result()\n\t\tif resp.StatusCode != 200 {\n\t\t\tt.Fatal(resp.Status)\n\t\t}\n\t\tvar out ApiStatAllGroupResp\n\t\tif err := json.NewDecoder(resp.Body).Decode(&out); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\t\tgroup := findTestGroup(out.Data.Groups, streamName)\n\t\tif group == nil {\n\t\t\tt.Fatal(\"no group\")\n\t\t}\n\t\tif len(group.StatSubs) <= 0 {\n\t\t\tt.Fatal(\"subs err\")\n\t\t}\n\t\tif len(group.Lalmax.ExtSubs) != 1 {\n\t\t\tt.Fatalf(\"unexpected lalmax ext_subs len: %d\", len(group.Lalmax.ExtSubs))\n\t\t}\n\t\tif group.StatSubs[0].SessionId != \"consumer1\" {\n\t\t\tt.Fatal(\"SessionId err\")\n\t\t}\n\t\tif group.Lalmax.ExtSubs[0].SessionId != \"consumer1\" {\n\t\t\tt.Fatal(\"lalmax ext SessionId err\")\n\t\t}\n\t})\n}\n\nfunc TestNotifyUpdate(t *testing.T) {\n\tstreamName := uniqueTestName(\"notify_test\")\n\tconsumerID := uniqueTestName(\"consumer_notify\")\n\tmatched := make(chan struct{}, 1)\n\n\tonUpdateHookMu.Lock()\n\tonUpdateHook = func(out base.UpdateInfo) {\n\t\tfor _, group := range out.Groups {\n\t\t\tfor _, sub := range group.StatSubs {\n\t\t\t\tif sub.SessionId == consumerID {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase matched <- struct{}{}:\n\t\t\t\t\tdefault:\n\t\t\t\t\t}\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tonUpdateHookMu.Unlock()\n\tt.Cleanup(func() {\n\t\tonUpdateHookMu.Lock()\n\t\tonUpdateHook = nil\n\t\tonUpdateHookMu.Unlock()\n\t})\n\n\t_, err := max.lalsvr.AddCustomizePubSession(streamName)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tss, _ := maxlogic.GetGroupManagerInstance().GetOrCreateGroupByStreamName(streamName, streamName, max.hlssvr, 1, 0)\n\tss.AddConsumer(consumerID, nil)\n\n\tselect {\n\tcase <-matched:\n\tcase <-time.After(5 * time.Second):\n\t\tt.Fatal(\"did not receive on_update with expected SessionId\")\n\t}\n}\n\nfunc TestRtpPubStartStop(t *testing.T) {\n\tbody := bytes.NewBufferString(`{\"stream_name\":\"rtp_pub_test\",\"port\":0,\"timeout_ms\":0}`)\n\tr := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"POST\", \"/api/ctrl/start_rtp_pub\", body)\n\tmax.router.ServeHTTP(r, req)\n\tresp := r.Result()\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatal(resp.Status)\n\t}\n\n\tvar startResp base.ApiCtrlStartRtpPubResp\n\tif err := json.NewDecoder(resp.Body).Decode(&startResp); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif startResp.ErrorCode != base.ErrorCodeSucc {\n\t\tt.Fatalf(\"start_rtp_pub failed, code=%d desp=%s\", startResp.ErrorCode, startResp.Desp)\n\t}\n\tif startResp.Data.StreamName != \"rtp_pub_test\" || startResp.Data.SessionId == \"\" || startResp.Data.Port == 0 {\n\t\tt.Fatalf(\"unexpected start_rtp_pub data: %+v\", startResp.Data)\n\t}\n\n\tr = httptest.NewRecorder()\n\treq = httptest.NewRequest(\"POST\", \"/api/ctrl/stop_rtp_pub?stream_name=rtp_pub_test\", nil)\n\tmax.router.ServeHTTP(r, req)\n\tresp = r.Result()\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatal(resp.Status)\n\t}\n\n\tvar stopResp base.ApiCtrlStopRelayPullResp\n\tif err := json.NewDecoder(resp.Body).Decode(&stopResp); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif stopResp.ErrorCode != base.ErrorCodeSucc {\n\t\tt.Fatalf(\"stop_rtp_pub failed, code=%d desp=%s\", stopResp.ErrorCode, stopResp.Desp)\n\t}\n\tif stopResp.Data.SessionId != startResp.Data.SessionId {\n\t\tt.Fatalf(\"stop_rtp_pub session id = %s, want %s\", stopResp.Data.SessionId, startResp.Data.SessionId)\n\t}\n}\n\nfunc TestStatGroupWithAppName(t *testing.T) {\n\tr := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"GET\", \"/api/stat/group?stream_name=test&app_name=missing\", nil)\n\tmax.router.ServeHTTP(r, req)\n\tresp := r.Result()\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatal(resp.Status)\n\t}\n\n\tvar out ApiStatGroupResp\n\tif err := json.NewDecoder(resp.Body).Decode(&out); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif out.ErrorCode != base.ErrorCodeGroupNotFound {\n\t\tt.Fatalf(\"unexpected error code: %+v\", out)\n\t}\n}\n\nfunc TestStatGroupIncludesLalmaxExtSubs(t *testing.T) {\n\tstreamName := uniqueTestName(\"test_stat_group_ext\")\n\n\t_, err := max.lalsvr.AddCustomizePubSession(streamName)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tss, _ := maxlogic.GetGroupManagerInstance().GetOrCreateGroupByStreamName(streamName, streamName, max.hlssvr, 1, 0)\n\tss.AddConsumer(\"consumer-stat-group\", nil)\n\n\tr := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"GET\", \"/api/stat/group?stream_name=\"+streamName, nil)\n\tmax.router.ServeHTTP(r, req)\n\tresp := r.Result()\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatal(resp.Status)\n\t}\n\n\tvar out ApiStatGroupResp\n\tif err := json.NewDecoder(resp.Body).Decode(&out); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif out.ErrorCode != base.ErrorCodeSucc {\n\t\tt.Fatalf(\"unexpected response: %+v\", out)\n\t}\n\tif out.Data == nil {\n\t\tt.Fatal(\"group data is nil\")\n\t}\n\tif len(out.Data.StatSubs) == 0 {\n\t\tt.Fatal(\"subs err\")\n\t}\n\tif len(out.Data.Lalmax.ExtSubs) != 1 {\n\t\tt.Fatalf(\"unexpected lalmax ext_subs len: %d\", len(out.Data.Lalmax.ExtSubs))\n\t}\n\tif out.Data.Lalmax.ExtSubs[0].SessionId != \"consumer-stat-group\" {\n\t\tt.Fatalf(\"unexpected ext sub: %+v\", out.Data.Lalmax.ExtSubs[0])\n\t}\n}\n\nfunc TestStatGroupIncludesLalmaxExtSubsRuntimeFields(t *testing.T) {\n\tstreamName := uniqueTestName(\"test_stat_group_runtime\")\n\n\t_, err := max.lalsvr.AddCustomizePubSession(streamName)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tss, _ := maxlogic.GetGroupManagerInstance().GetOrCreateGroupByStreamName(streamName, streamName, max.hlssvr, 1, 0)\n\tsub := &maxlogicTestSubscriber{\n\t\tstat: maxlogic.SubscriberStat{\n\t\t\tRemoteAddr:    \"10.0.0.1:9000\",\n\t\t\tReadBytesSum:  1024,\n\t\t\tWroteBytesSum: 2048,\n\t\t},\n\t}\n\tss.AddSubscriber(maxlogic.SubscriberInfo{\n\t\tSubscriberID: \"consumer-runtime\",\n\t\tProtocol:     maxlogic.SubscriberProtocolSRT,\n\t}, sub)\n\n\tr := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"GET\", \"/api/stat/group?stream_name=\"+streamName, nil)\n\tmax.router.ServeHTTP(r, req)\n\tresp := r.Result()\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatal(resp.Status)\n\t}\n\n\tvar out ApiStatGroupResp\n\tif err := json.NewDecoder(resp.Body).Decode(&out); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif out.ErrorCode != base.ErrorCodeSucc {\n\t\tt.Fatalf(\"unexpected response: %+v\", out)\n\t}\n\tif out.Data == nil {\n\t\tt.Fatal(\"group data is nil\")\n\t}\n\tif len(out.Data.Lalmax.ExtSubs) != 1 {\n\t\tt.Fatalf(\"unexpected lalmax ext_subs len: %d\", len(out.Data.Lalmax.ExtSubs))\n\t}\n\n\tstat := out.Data.Lalmax.ExtSubs[0]\n\tif stat.RemoteAddr != \"10.0.0.1:9000\" {\n\t\tt.Fatalf(\"remote addr = %s, want 10.0.0.1:9000\", stat.RemoteAddr)\n\t}\n\tif stat.ReadBytesSum != 1024 || stat.WroteBytesSum != 2048 {\n\t\tt.Fatalf(\"unexpected bytes stat: %+v\", stat)\n\t}\n}\n\nfunc TestStopRelayPullAllowsGet(t *testing.T) {\n\tr := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"GET\", \"/api/ctrl/stop_relay_pull?stream_name=missing\", nil)\n\tmax.router.ServeHTTP(r, req)\n\tresp := r.Result()\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatal(resp.Status)\n\t}\n\n\tvar out base.ApiCtrlStopRelayPullResp\n\tif err := json.NewDecoder(resp.Body).Decode(&out); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif out.ErrorCode != base.ErrorCodeGroupNotFound {\n\t\tt.Fatalf(\"unexpected response: %+v\", out)\n\t}\n}\n\nfunc TestHookHubRecentAndSubscribe(t *testing.T) {\n\thub := NewHttpNotify(config.HttpNotifyConfig{}, \"hub-test\")\n\t// NotifyPubStart 会派生 on_stream_changed，需要足够缓冲\n\t_, ch, cancel := hub.Subscribe(8)\n\tdefer cancel()\n\n\thub.NotifyPubStart(base.PubStartInfo{})\n\n\tselect {\n\tcase event := <-ch:\n\t\tif event.Event != HookEventPubStart {\n\t\t\tt.Fatalf(\"unexpected event: %+v\", event)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"wait hook event timeout\")\n\t}\n\n\tevents := hub.Recent(0)\n\tfound := false\n\tfor _, e := range events {\n\t\tif e.Event == HookEventPubStart {\n\t\t\tfound = true\n\t\t\tbreak\n\t\t}\n\t}\n\tif !found {\n\t\tt.Fatalf(\"on_pub_start not found in recent events\")\n\t}\n}\n\nfunc TestHookGroupEventsFromDirectLifecycle(t *testing.T) {\n\tsvr, err := NewLalMaxServer(&config.Config{\n\t\tLalRawContent: []byte(`{\"rtmp\":{\"enable\":false},\"rtsp\":{\"enable\":false},\"http_api\":{\"enable\":false},\"pprof\":{\"enable\":false}}`),\n\t\tHttpConfig: config.HttpConfig{\n\t\t\tListenAddr: \":52353\",\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tsvr.lalsvr.WithOnHookSession(func(uniqueKey string, streamName string) baseLogic.ICustomizeHookSessionContext {\n\t\tkey := maxlogic.StreamKeyFromStreamName(streamName)\n\t\tgroup, created := maxlogic.GetGroupManagerInstance().GetOrCreateGroupByStreamName(uniqueKey, streamName, svr.hlssvr, svr.conf.LogicConfig.GopCacheNum, svr.conf.LogicConfig.SingleGopMaxFrameNum)\n\t\tgroup.BindStopHook(key, func(stopKey maxlogic.StreamKey) {\n\t\t\tsvr.notifyHub.NotifyGroupStop(HookGroupInfo{\n\t\t\t\tAppName:    stopKey.AppName,\n\t\t\t\tStreamName: stopKey.StreamName,\n\t\t\t})\n\t\t})\n\t\tif created {\n\t\t\tsvr.notifyHub.NotifyGroupStart(HookGroupInfo{\n\t\t\t\tAppName:    key.AppName,\n\t\t\t\tStreamName: key.StreamName,\n\t\t\t})\n\t\t}\n\t\treturn group\n\t})\n\n\tstreamName := \"direct-group-lifecycle\"\n\tsession, err := svr.lalsvr.AddCustomizePubSession(streamName)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tsvr.lalsvr.DelCustomizePubSession(session)\n\n\tfilter := NewHookEventFilter(\"\", streamName, \"\", []string{HookEventGroupStart, HookEventGroupStop})\n\tevents := svr.notifyHub.RecentFiltered(10, filter)\n\tif len(events) != 2 {\n\t\tt.Fatalf(\"unexpected event len: %d\", len(events))\n\t}\n\tif events[0].Event != HookEventGroupStart {\n\t\tt.Fatalf(\"unexpected first event: %+v\", events[0])\n\t}\n\tif events[1].Event != HookEventGroupStop {\n\t\tt.Fatalf(\"unexpected second event: %+v\", events[1])\n\t}\n\n\tvar start HookGroupInfo\n\tif err := json.Unmarshal(events[0].Payload, &start); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif start.StreamName != streamName {\n\t\tt.Fatalf(\"unexpected start payload: %+v\", start)\n\t}\n\n\tvar stop HookGroupInfo\n\tif err := json.Unmarshal(events[1].Payload, &stop); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif stop.StreamName != streamName {\n\t\tt.Fatalf(\"unexpected stop payload: %+v\", stop)\n\t}\n}\n\nfunc TestHookHubStreamActiveEvent(t *testing.T) {\n\thub := NewHttpNotify(config.HttpNotifyConfig{}, \"hub-test\")\n\n\thub.NotifyStreamActive(HookGroupInfo{\n\t\tAppName:    \"live\",\n\t\tStreamName: \"stream-active\",\n\t})\n\n\tfilter := NewHookEventFilter(\"live\", \"stream-active\", \"\", []string{HookEventStreamActive})\n\tevents := hub.RecentFiltered(10, filter)\n\tif len(events) != 1 {\n\t\tt.Fatalf(\"unexpected event len: %d\", len(events))\n\t}\n\tif events[0].Event != HookEventStreamActive {\n\t\tt.Fatalf(\"unexpected event: %+v\", events[0])\n\t}\n\n\tvar payload HookGroupInfo\n\tif err := json.Unmarshal(events[0].Payload, &payload); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif payload.AppName != \"live\" || payload.StreamName != \"stream-active\" {\n\t\tt.Fatalf(\"unexpected payload: %+v\", payload)\n\t}\n}\n\nfunc TestBuiltinHTTPPluginRespectsEnableFlag(t *testing.T) {\n\tvar requestCount atomic.Int32\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\trequestCount.Add(1)\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer ts.Close()\n\n\thub := NewHttpNotify(config.HttpNotifyConfig{\n\t\tEnable:     false,\n\t\tOnPubStart: ts.URL,\n\t}, \"hub-test\")\n\n\thub.NotifyPubStart(base.PubStartInfo{})\n\ttime.Sleep(200 * time.Millisecond)\n\n\tif got := requestCount.Load(); got != 0 {\n\t\tt.Fatalf(\"unexpected webhook request count: %d\", got)\n\t}\n}\n\nfunc TestBuiltinHTTPPluginPreservesOrderPerStream(t *testing.T) {\n\tfirstStarted := make(chan struct{})\n\tsecondStarted := make(chan struct{})\n\tallowFirstFinish := make(chan struct{})\n\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer r.Body.Close()\n\n\t\tvar payload hookHTTPPayload\n\t\tif err := json.NewDecoder(r.Body).Decode(&payload); err != nil {\n\t\t\tt.Errorf(\"decode payload failed: %v\", err)\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tswitch payload.SessionID {\n\t\tcase \"first\":\n\t\t\tclose(firstStarted)\n\t\t\t<-allowFirstFinish\n\t\tcase \"second\":\n\t\t\tclose(secondStarted)\n\t\t}\n\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer ts.Close()\n\n\thub := NewHttpNotify(config.HttpNotifyConfig{\n\t\tEnable:     true,\n\t\tOnPubStart: ts.URL,\n\t\tOnPubStop:  ts.URL,\n\t}, \"hub-test\")\n\n\thub.NotifyPubStart(base.PubStartInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tSessionId:  \"first\",\n\t\t\tAppName:    \"live\",\n\t\t\tStreamName: \"same-stream\",\n\t\t},\n\t})\n\thub.NotifyPubStop(base.PubStopInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tSessionId:  \"second\",\n\t\t\tAppName:    \"live\",\n\t\t\tStreamName: \"same-stream\",\n\t\t},\n\t})\n\n\tselect {\n\tcase <-firstStarted:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"first webhook did not start in time\")\n\t}\n\n\tselect {\n\tcase <-secondStarted:\n\t\tt.Fatal(\"second webhook started before the first one finished\")\n\tcase <-time.After(200 * time.Millisecond):\n\t}\n\n\tclose(allowFirstFinish)\n\n\tselect {\n\tcase <-secondStarted:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"second webhook did not start after the first one finished\")\n\t}\n}\n\nfunc TestBuiltinHTTPPluginAllowsParallelAcrossStreams(t *testing.T) {\n\tfirstStarted := make(chan struct{})\n\tsecondStreamStarted := make(chan struct{})\n\tallowFirstFinish := make(chan struct{})\n\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer r.Body.Close()\n\n\t\tvar payload hookHTTPPayload\n\t\tif err := json.NewDecoder(r.Body).Decode(&payload); err != nil {\n\t\t\tt.Errorf(\"decode payload failed: %v\", err)\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tswitch payload.StreamName {\n\t\tcase \"stream-a\":\n\t\t\tclose(firstStarted)\n\t\t\t<-allowFirstFinish\n\t\tcase \"stream-b\":\n\t\t\tclose(secondStreamStarted)\n\t\t}\n\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer ts.Close()\n\n\thub := NewHttpNotify(config.HttpNotifyConfig{\n\t\tEnable:     true,\n\t\tOnPubStart: ts.URL,\n\t}, \"hub-test\")\n\n\thub.NotifyPubStart(base.PubStartInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tSessionId:  \"stream-a-session\",\n\t\t\tAppName:    \"live\",\n\t\t\tStreamName: \"stream-a\",\n\t\t},\n\t})\n\thub.NotifyPubStart(base.PubStartInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tSessionId:  \"stream-b-session\",\n\t\t\tAppName:    \"live\",\n\t\t\tStreamName: \"stream-b\",\n\t\t},\n\t})\n\n\tselect {\n\tcase <-firstStarted:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"first stream webhook did not start in time\")\n\t}\n\n\tselect {\n\tcase <-secondStreamStarted:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"second stream webhook was blocked by the first stream\")\n\t}\n\n\tclose(allowFirstFinish)\n}\n\nfunc TestBuiltinHTTPPluginPreservesOrderAcrossDifferentURLsForSameStream(t *testing.T) {\n\tfirstStarted := make(chan struct{})\n\tsecondStarted := make(chan struct{})\n\tallowFirstFinish := make(chan struct{})\n\n\tstartTS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer r.Body.Close()\n\n\t\tvar payload hookHTTPPayload\n\t\tif err := json.NewDecoder(r.Body).Decode(&payload); err != nil {\n\t\t\tt.Errorf(\"decode start payload failed: %v\", err)\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tclose(firstStarted)\n\t\t<-allowFirstFinish\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer startTS.Close()\n\n\tstopTS := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer r.Body.Close()\n\n\t\tvar payload hookHTTPPayload\n\t\tif err := json.NewDecoder(r.Body).Decode(&payload); err != nil {\n\t\t\tt.Errorf(\"decode stop payload failed: %v\", err)\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tclose(secondStarted)\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer stopTS.Close()\n\n\thub := NewHttpNotify(config.HttpNotifyConfig{\n\t\tEnable:     true,\n\t\tOnPubStart: startTS.URL,\n\t\tOnPubStop:  stopTS.URL,\n\t}, \"hub-test\")\n\n\thub.NotifyPubStart(base.PubStartInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tSessionId:  \"first\",\n\t\t\tAppName:    \"live\",\n\t\t\tStreamName: \"same-stream\",\n\t\t},\n\t})\n\thub.NotifyPubStop(base.PubStopInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tSessionId:  \"second\",\n\t\t\tAppName:    \"live\",\n\t\t\tStreamName: \"same-stream\",\n\t\t},\n\t})\n\n\tselect {\n\tcase <-firstStarted:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"start webhook did not start in time\")\n\t}\n\n\tselect {\n\tcase <-secondStarted:\n\t\tt.Fatal(\"stop webhook started before start webhook finished\")\n\tcase <-time.After(200 * time.Millisecond):\n\t}\n\n\tclose(allowFirstFinish)\n\n\tselect {\n\tcase <-secondStarted:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"stop webhook did not start after start webhook finished\")\n\t}\n}\n\nfunc TestHookRecentEndpoint(t *testing.T) {\n\tsvr, err := NewLalMaxServer(&config.Config{\n\t\tLalRawContent: []byte(`{\"rtmp\":{\"enable\":false},\"rtsp\":{\"enable\":false},\"http_api\":{\"enable\":false},\"pprof\":{\"enable\":false}}`),\n\t\tHttpConfig: config.HttpConfig{\n\t\t\tListenAddr: \":52350\",\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tsvr.notifyHub.NotifyPubStop(base.PubStopInfo{})\n\n\tr := httptest.NewRecorder()\n\t// 用 event filter 精确查询，因为 NotifyPubStop 会派生 on_stream_changed\n\treq := httptest.NewRequest(\"GET\", \"/api/hook/recent?limit=10&event=on_pub_stop\", nil)\n\tsvr.router.ServeHTTP(r, req)\n\tresp := r.Result()\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatal(resp.Status)\n\t}\n\n\tvar out struct {\n\t\tbase.ApiRespBasic\n\t\tData struct {\n\t\t\tEvents []HookEvent `json:\"events\"`\n\t\t} `json:\"data\"`\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(&out); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif out.ErrorCode != base.ErrorCodeSucc {\n\t\tt.Fatalf(\"unexpected response: %+v\", out)\n\t}\n\tif len(out.Data.Events) < 1 {\n\t\tt.Fatalf(\"expected at least 1 on_pub_stop event, got: %d\", len(out.Data.Events))\n\t}\n\tif out.Data.Events[0].Event != HookEventPubStop {\n\t\tt.Fatalf(\"unexpected event: %+v\", out.Data.Events[0])\n\t}\n}\n\nfunc TestHookRecentEndpointFilterByEventAndStream(t *testing.T) {\n\tsvr, err := NewLalMaxServer(&config.Config{\n\t\tLalRawContent: []byte(`{\"rtmp\":{\"enable\":false},\"rtsp\":{\"enable\":false},\"http_api\":{\"enable\":false},\"pprof\":{\"enable\":false}}`),\n\t\tHttpConfig: config.HttpConfig{\n\t\t\tListenAddr: \":52351\",\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tsvr.notifyHub.NotifyPubStart(base.PubStartInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tSessionId:  \"pub-1\",\n\t\t\tStreamName: \"stream-a\",\n\t\t\tAppName:    \"live\",\n\t\t},\n\t})\n\tsvr.notifyHub.NotifyPubStop(base.PubStopInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tSessionId:  \"pub-2\",\n\t\t\tStreamName: \"stream-b\",\n\t\t\tAppName:    \"live\",\n\t\t},\n\t})\n\n\tr := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"GET\", \"/api/hook/recent?limit=10&stream_name=stream-a&event=on_pub_start\", nil)\n\tsvr.router.ServeHTTP(r, req)\n\tresp := r.Result()\n\tif resp.StatusCode != http.StatusOK {\n\t\tt.Fatal(resp.Status)\n\t}\n\n\tvar out struct {\n\t\tbase.ApiRespBasic\n\t\tData struct {\n\t\t\tEvents []HookEvent `json:\"events\"`\n\t\t} `json:\"data\"`\n\t}\n\tif err := json.NewDecoder(resp.Body).Decode(&out); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif len(out.Data.Events) != 1 {\n\t\tt.Fatalf(\"unexpected event count: %d\", len(out.Data.Events))\n\t}\n\tif out.Data.Events[0].Event != HookEventPubStart {\n\t\tt.Fatalf(\"unexpected event: %+v\", out.Data.Events[0])\n\t}\n}\n\nfunc TestHookEventFilterBySessionID(t *testing.T) {\n\tfilter := NewHookEventFilter(\"\", \"\", \"sess-2\", nil)\n\n\tpubStart := HookEvent{Event: HookEventPubStart, sessionID: \"sess-1\"}\n\tpubStop := HookEvent{Event: HookEventPubStop, sessionID: \"sess-2\"}\n\n\tif filter.Match(pubStart) {\n\t\tt.Fatalf(\"session filter unexpectedly matched: %+v\", pubStart)\n\t}\n\tif !filter.Match(pubStop) {\n\t\tt.Fatalf(\"session filter did not match: %+v\", pubStop)\n\t}\n}\n\nfunc TestHookEventFilterByUpdateGroup(t *testing.T) {\n\tfilter := NewHookEventFilter(\"live\", \"stream-a\", \"\", []string{HookEventUpdate})\n\tevent := HookEvent{\n\t\tEvent: HookEventUpdate,\n\t\tgroupKeys: []maxlogic.StreamKey{\n\t\t\tmaxlogic.NewStreamKey(\"live\", \"stream-a\"),\n\t\t\tmaxlogic.NewStreamKey(\"live\", \"stream-b\"),\n\t\t},\n\t}\n\n\tif !filter.Match(event) {\n\t\tt.Fatalf(\"update filter did not match: %+v\", event)\n\t}\n}\n\nfunc TestHookEventFilterByGroupLifecycle(t *testing.T) {\n\tfilter := NewHookEventFilter(\"live\", \"stream-a\", \"\", []string{HookEventGroupStart})\n\tevent := HookEvent{\n\t\tEvent:      HookEventGroupStart,\n\t\tappName:    \"live\",\n\t\tstreamName: \"stream-a\",\n\t}\n\n\tif !filter.Match(event) {\n\t\tt.Fatalf(\"group lifecycle filter did not match: %+v\", event)\n\t}\n}\n\nfunc TestHookPluginReceivesFilteredEvents(t *testing.T) {\n\thub := NewHttpNotify(config.HttpNotifyConfig{}, \"plugin-test\")\n\tplugin := &testHookPlugin{\n\t\tname:   \"stream-a-plugin\",\n\t\tevents: make(chan HookEvent, 2),\n\t}\n\n\tcancel, err := hub.RegisterPlugin(plugin, HookPluginOptions{\n\t\tFilter: NewHookEventFilter(\"live\", \"stream-a\", \"\", []string{HookEventPubStart}),\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer cancel()\n\n\thub.NotifyPubStart(base.PubStartInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tSessionId:  \"pub-a\",\n\t\t\tStreamName: \"stream-a\",\n\t\t\tAppName:    \"live\",\n\t\t},\n\t})\n\thub.NotifyPubStop(base.PubStopInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tSessionId:  \"pub-a\",\n\t\t\tStreamName: \"stream-a\",\n\t\t\tAppName:    \"live\",\n\t\t},\n\t})\n\n\tselect {\n\tcase event := <-plugin.events:\n\t\tif event.Event != HookEventPubStart {\n\t\t\tt.Fatalf(\"unexpected plugin event: %+v\", event)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"wait plugin event timeout\")\n\t}\n\n\tselect {\n\tcase event := <-plugin.events:\n\t\tt.Fatalf(\"unexpected extra plugin event: %+v\", event)\n\tcase <-time.After(200 * time.Millisecond):\n\t}\n}\n\nfunc TestRegisterHookPluginFromServer(t *testing.T) {\n\tsvr, err := NewLalMaxServer(&config.Config{\n\t\tLalRawContent: []byte(`{\"rtmp\":{\"enable\":false},\"rtsp\":{\"enable\":false},\"http_api\":{\"enable\":false},\"pprof\":{\"enable\":false}}`),\n\t\tHttpConfig: config.HttpConfig{\n\t\t\tListenAddr: \":52352\",\n\t\t},\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tplugin := &testHookPlugin{\n\t\tname:   \"server-plugin\",\n\t\tevents: make(chan HookEvent, 1),\n\t}\n\tcancel, err := svr.RegisterHookPlugin(plugin, HookPluginOptions{\n\t\tFilter: NewHookEventFilter(\"\", \"\", \"\", []string{HookEventPubStop}),\n\t})\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer cancel()\n\n\tsvr.notifyHub.NotifyPubStop(base.PubStopInfo{})\n\n\tselect {\n\tcase event := <-plugin.events:\n\t\tif event.Event != HookEventPubStop {\n\t\t\tt.Fatalf(\"unexpected event: %+v\", event)\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"wait server plugin event timeout\")\n\t}\n}\n\nfunc TestAuthentication(t *testing.T) {\n\tt.Run(\"无须鉴权\", func(t *testing.T) {\n\t\tif !authentication(\"12\", \"192.168.0.2\", nil, nil) {\n\t\t\tt.Fatal(\"期望通过， 但实际未通过\")\n\t\t}\n\t})\n\tt.Run(\"Token 鉴权失败\", func(t *testing.T) {\n\t\tif authentication(\"1\", \"192.168.0.2\", []string{\"12\"}, nil) {\n\t\t\tt.Fatal(\"期望不通过， 但实际通过\")\n\t\t}\n\t})\n\tt.Run(\"token 鉴权成功\", func(t *testing.T) {\n\t\tif !authentication(\"12\", \"192.168.0.2\", []string{\"12\"}, nil) {\n\t\t\tt.Fatal(\"期望通过， 但实际不通过\")\n\t\t}\n\t})\n\tt.Run(\"ip 白名单鉴权失败\", func(t *testing.T) {\n\t\tif authentication(\"12\", \"192.168.0.2\", nil, []string{\"192.168.1.2\"}) {\n\t\t\tt.Fatal(\"期望不通过， 但实际通过\")\n\t\t}\n\t})\n\tt.Run(\"ip 白名单鉴权成功\", func(t *testing.T) {\n\t\tif !authentication(\"12\", \"192.168.0.2\", []string{\"12\"}, []string{\"192.168.0.2\"}) {\n\t\t\tt.Fatal(\"期望通过， 但实际不通过\")\n\t\t}\n\t})\n\tt.Run(\"两种模式结合鉴权通过\", func(t *testing.T) {\n\t\tif !authentication(\"12\", \"192.168.0.2\", []string{\"12\"}, []string{\"192.168.0.2\"}) {\n\t\t\tt.Fatal(\"期望通过， 但实际不通过\")\n\t\t}\n\t})\n}\n\n// TestWHIPGETNot404 确保浏览器 GET 能命中路由（无 RTC 时为 503，不应为 Gin 默认 404）。\nfunc TestWHIPGETNot404(t *testing.T) {\n\tpaths := []string{\"/webrtc/whip?streamid=test110\"}\n\tfor _, p := range paths {\n\t\tt.Run(p, func(t *testing.T) {\n\t\t\tr := httptest.NewRecorder()\n\t\t\treq := httptest.NewRequest(http.MethodGet, p, nil)\n\t\t\tmax.router.ServeHTTP(r, req)\n\t\t\tresp := r.Result()\n\t\t\tdefer resp.Body.Close()\n\t\t\tif resp.StatusCode == http.StatusNotFound {\n\t\t\t\tt.Fatalf(\"GET %s 不应返回 404，请检查 initRtcRouter 是否注册 GET\", p)\n\t\t\t}\n\t\t\tif resp.StatusCode != http.StatusServiceUnavailable {\n\t\t\t\tt.Fatalf(\"测试环境未启用 RTC，期望 503，实际 %d\", resp.StatusCode)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// TestWHEPGETCanonicalPath 规范播放地址 GET /webrtc/whep 应命中路由（无 RTC 时为 503）。\nfunc TestWHEPGETCanonicalPath(t *testing.T) {\n\tr := httptest.NewRecorder()\n\treq := httptest.NewRequest(http.MethodGet, \"/webrtc/whep?streamid=test110\", nil)\n\tmax.router.ServeHTTP(r, req)\n\tresp := r.Result()\n\tdefer resp.Body.Close()\n\tif resp.StatusCode == http.StatusNotFound {\n\t\tt.Fatal(\"GET /webrtc/whep 不应 404\")\n\t}\n\tif resp.StatusCode != http.StatusServiceUnavailable {\n\t\tt.Fatalf(\"测试环境未启用 RTC，期望 503，实际 %d\", resp.StatusCode)\n\t}\n}\n"
  },
  {
    "path": "server/router_zlm_compat.go",
    "content": "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/lal/pkg/base\"\n\t\"github.com/q191201771/lal/pkg/logic\"\n\tconfig \"github.com/q191201771/lalmax/config\"\n)\n\n// initZlmCompatRouter 注册 /index/api/* ZLM 兼容路由\n// 为什么独立文件：隔离 ZLM 兼容层，不影响现有 lalmax API\nfunc (s *LalMaxServer) initZlmCompatRouter(router *gin.Engine, handlers ...gin.HandlerFunc) {\n\tzlm := router.Group(\"/index/api\", handlers...)\n\tzlm.POST(\"/openRtpServer\", s.zlmOpenRtpServerHandler)\n\tzlm.POST(\"/closeRtpServer\", s.zlmCloseRtpServerHandler)\n\tzlm.POST(\"/close_streams\", s.zlmCloseStreamsHandler)\n\tzlm.POST(\"/getServerConfig\", s.zlmGetServerConfigHandler)\n\tzlm.POST(\"/setServerConfig\", s.zlmSetServerConfigHandler)\n\tzlm.POST(\"/restartServer\", s.zlmRestartServerHandler)\n\tzlm.POST(\"/startRecord\", s.zlmStartRecordHandler)\n\tzlm.POST(\"/stopRecord\", s.zlmStopRecordHandler)\n\tzlm.POST(\"/addStreamProxy\", s.zlmAddStreamProxyHandler)\n\tzlm.POST(\"/getSnap\", s.zlmGetSnapHandler)\n\tzlm.POST(\"/webrtc\", s.zlmWebrtcHandler)\n}\n\n// ---------- openRtpServer ----------\n\nfunc (s *LalMaxServer) zlmOpenRtpServerHandler(c *gin.Context) {\n\tvar req ZlmOpenRtpServerReq\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tc.JSON(http.StatusOK, ZlmOpenRtpServerResp{Code: -300, Msg: \"invalid params\"})\n\t\treturn\n\t}\n\n\tisTcpFlag := 0\n\tif req.TCPMode > 0 {\n\t\tisTcpFlag = 1\n\t}\n\n\tresp := s.rtpPubMgr.Start(base.ApiCtrlStartRtpPubReq{\n\t\tStreamName: req.StreamID,\n\t\tPort:       req.Port,\n\t\tIsTcpFlag:  isTcpFlag,\n\t})\n\n\tif resp.ErrorCode != base.ErrorCodeSucc {\n\t\tc.JSON(http.StatusOK, ZlmOpenRtpServerResp{Code: -1, Msg: resp.Desp})\n\t\treturn\n\t}\n\n\tLog.Infof(\"zlm compat openRtpServer. stream_id=%s, port=%d\", req.StreamID, resp.Data.Port)\n\tc.JSON(http.StatusOK, ZlmOpenRtpServerResp{Code: 0, Port: resp.Data.Port})\n}\n\n// ---------- closeRtpServer ----------\n\nfunc (s *LalMaxServer) zlmCloseRtpServerHandler(c *gin.Context) {\n\tvar req ZlmCloseRtpServerReq\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tc.JSON(http.StatusOK, ZlmCloseRtpServerResp{Code: -300})\n\t\treturn\n\t}\n\n\t_, err := s.rtpPubMgr.Stop(req.StreamID, \"\")\n\tif err != nil {\n\t\tLog.Infof(\"zlm compat closeRtpServer not found. stream_id=%s\", req.StreamID)\n\t\tc.JSON(http.StatusOK, ZlmCloseRtpServerResp{Code: 0, Hit: 0})\n\t\treturn\n\t}\n\n\tLog.Infof(\"zlm compat closeRtpServer. stream_id=%s\", req.StreamID)\n\tc.JSON(http.StatusOK, ZlmCloseRtpServerResp{Code: 0, Hit: 1})\n}\n\n// ---------- close_streams ----------\n\nfunc (s *LalMaxServer) zlmCloseStreamsHandler(c *gin.Context) {\n\tvar req ZlmCloseStreamsReq\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tc.JSON(http.StatusOK, ZlmCloseStreamsResp{Code: -300})\n\t\treturn\n\t}\n\n\tstreamName := req.Stream\n\tif streamName == \"\" {\n\t\tc.JSON(http.StatusOK, ZlmCloseStreamsResp{Code: 0, CountHit: 0, CountClosed: 0})\n\t\treturn\n\t}\n\n\t// 尝试通过 kick_session 关闭所有匹配的 session\n\tgroups := s.lalsvr.StatAllGroup()\n\thit := 0\n\tclosed := 0\n\tfor _, g := range groups {\n\t\tif g.StreamName != streamName {\n\t\t\tcontinue\n\t\t}\n\t\thit++\n\t\t// 关闭 pub session\n\t\tif g.StatPub.SessionId != \"\" {\n\t\t\tresp := s.lalsvr.CtrlKickSession(base.ApiCtrlKickSessionReq{\n\t\t\t\tStreamName: streamName,\n\t\t\t\tSessionId:  g.StatPub.SessionId,\n\t\t\t})\n\t\t\tif resp.ErrorCode == base.ErrorCodeSucc {\n\t\t\t\tclosed++\n\t\t\t}\n\t\t}\n\t}\n\n\t// 也尝试关闭 RTP pub session\n\tif _, err := s.rtpPubMgr.Stop(streamName, \"\"); err == nil {\n\t\tif hit == 0 {\n\t\t\thit++\n\t\t}\n\t\tclosed++\n\t}\n\n\tLog.Infof(\"zlm compat close_streams. stream=%s, hit=%d, closed=%d\", streamName, hit, closed)\n\tc.JSON(http.StatusOK, ZlmCloseStreamsResp{Code: 0, CountHit: hit, CountClosed: closed})\n}\n\n// ---------- getServerConfig ----------\n\nfunc (s *LalMaxServer) zlmGetServerConfigHandler(c *gin.Context) {\n\tcfg := buildZlmServerConfig(s.conf)\n\tc.JSON(http.StatusOK, ZlmGetServerConfigResp{Code: 0, Data: []map[string]any{cfg}})\n}\n\n// ---------- setServerConfig ----------\n\nfunc (s *LalMaxServer) zlmSetServerConfigHandler(c *gin.Context) {\n\tvar params map[string]*string\n\tif err := c.ShouldBindJSON(&params); err != nil {\n\t\tc.JSON(http.StatusOK, ZlmSetServerConfigResp{\n\t\t\tZlmFixedHeader: ZlmFixedHeader{Code: -300, Msg: \"invalid params\"},\n\t\t})\n\t\treturn\n\t}\n\n\tchanged := 0\n\tzlmCfg := s.conf.HttpNotifyConfig.ZlmCompatHookConfig\n\n\thookMap := map[string]*string{\n\t\t\"hook.on_stream_changed\":     &zlmCfg.ZlmOnStreamChanged,\n\t\t\"hook.on_server_keepalive\":   &zlmCfg.ZlmOnServerKeepalive,\n\t\t\"hook.on_stream_none_reader\": &zlmCfg.ZlmOnStreamNoneReader,\n\t\t\"hook.on_rtp_server_timeout\": &zlmCfg.ZlmOnRtpServerTimeout,\n\t\t\"hook.on_record_mp4\":         &zlmCfg.ZlmOnRecordMp4,\n\t\t\"hook.on_publish\":            &zlmCfg.ZlmOnPublish,\n\t\t\"hook.on_play\":               &zlmCfg.ZlmOnPlay,\n\t\t\"hook.on_stream_not_found\":   &zlmCfg.ZlmOnStreamNotFound,\n\t\t\"hook.on_server_started\":     &zlmCfg.ZlmOnServerStarted,\n\t}\n\n\tfor key, target := range hookMap {\n\t\tif v, ok := params[key]; ok && v != nil && *v != *target {\n\t\t\t*target = *v\n\t\t\tchanged++\n\t\t}\n\t}\n\n\t// 处理 keepalive 间隔\n\tif v, ok := params[\"hook.alive_interval\"]; ok && v != nil {\n\t\tif interval, err := strconv.Atoi(*v); err == nil && interval > 0 {\n\t\t\ts.conf.HttpNotifyConfig.KeepaliveIntervalSec = interval\n\t\t\tchanged++\n\t\t}\n\t}\n\n\t// 处理 hook 超时时间\n\tif v, ok := params[\"hook.timeoutSec\"]; ok && v != nil {\n\t\tif timeout, err := strconv.Atoi(*v); err == nil && timeout > 0 {\n\t\t\ts.conf.HttpNotifyConfig.HookTimeoutSec = timeout\n\t\t\tchanged++\n\t\t}\n\t}\n\n\t// 处理 rtp_proxy.port_range\n\tif v, ok := params[\"rtp_proxy.port_range\"]; ok && v != nil {\n\t\tif portMin, portMax, ok := parsePortRange(*v); ok {\n\t\t\ts.rtpPubMgr.UpdatePortRange(portMin, portMax)\n\t\t\tchanged++\n\t\t}\n\t}\n\n\tif changed > 0 {\n\t\ts.conf.HttpNotifyConfig.Enable = true\n\t\ts.notifyHub.UpdateZlmHookConfig(zlmCfg)\n\t\ts.conf.HttpNotifyConfig.ZlmCompatHookConfig = zlmCfg\n\n\t\t// 同步清零 conf 中的原有 hook URL\n\t\ts.conf.HttpNotifyConfig.OnServerStart = \"\"\n\t\ts.conf.HttpNotifyConfig.OnUpdate = \"\"\n\t\ts.conf.HttpNotifyConfig.OnGroupStart = \"\"\n\t\ts.conf.HttpNotifyConfig.OnGroupStop = \"\"\n\t\ts.conf.HttpNotifyConfig.OnStreamActive = \"\"\n\t\ts.conf.HttpNotifyConfig.OnPubStart = \"\"\n\t\ts.conf.HttpNotifyConfig.OnPubStop = \"\"\n\t\ts.conf.HttpNotifyConfig.OnSubStart = \"\"\n\t\ts.conf.HttpNotifyConfig.OnSubStop = \"\"\n\t\ts.conf.HttpNotifyConfig.OnRelayPullStart = \"\"\n\t\ts.conf.HttpNotifyConfig.OnRelayPullStop = \"\"\n\t\ts.conf.HttpNotifyConfig.OnRtmpConnect = \"\"\n\t\ts.conf.HttpNotifyConfig.OnHlsMakeTs = \"\"\n\n\t\tif err := s.conf.SaveToFile(); err != nil {\n\t\t\tLog.Errorf(\"zlm compat setServerConfig persist failed. err=%v\", err)\n\t\t}\n\t}\n\n\tLog.Infof(\"zlm compat setServerConfig. changed=%d\", changed)\n\tc.JSON(http.StatusOK, ZlmSetServerConfigResp{\n\t\tZlmFixedHeader: ZlmFixedHeader{Code: 0},\n\t\tChanged:        changed,\n\t})\n}\n\n// ---------- restartServer ----------\n\nfunc (s *LalMaxServer) zlmRestartServerHandler(c *gin.Context) {\n\t// 为什么不重启：lalmax 不需要像 ZLM 那样通过重启来重绑端口\n\tLog.Infof(\"zlm compat restartServer (noop)\")\n\tc.JSON(http.StatusOK, ZlmFixedHeader{Code: 0, Msg: \"ok\"})\n}\n\n// ---------- addStreamProxy ----------\n\nfunc (s *LalMaxServer) zlmAddStreamProxyHandler(c *gin.Context) {\n\tvar req ZlmAddStreamProxyReq\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tc.JSON(http.StatusOK, ZlmAddStreamProxyResp{ZlmFixedHeader: ZlmFixedHeader{Code: -300, Msg: \"invalid params\"}})\n\t\treturn\n\t}\n\n\tstreamName := req.Stream\n\tif streamName == \"\" {\n\t\tc.JSON(http.StatusOK, ZlmAddStreamProxyResp{ZlmFixedHeader: ZlmFixedHeader{Code: -300, Msg: \"stream is required\"}})\n\t\treturn\n\t}\n\n\tpullReq := base.ApiCtrlStartRelayPullReq{\n\t\tUrl:                      req.URL,\n\t\tStreamName:               streamName,\n\t\tPullTimeoutMs:            int(req.TimeoutSec * 1000),\n\t\tPullRetryNum:             req.RetryCount,\n\t\tAutoStopPullAfterNoOutMs: base.AutoStopPullAfterNoOutMsNever,\n\t\tRtspMode:                 req.RTPType,\n\t}\n\tif pullReq.PullRetryNum == 0 {\n\t\tpullReq.PullRetryNum = base.PullRetryNumNever\n\t}\n\tif pullReq.PullTimeoutMs == 0 {\n\t\tpullReq.PullTimeoutMs = logic.DefaultApiCtrlStartRelayPullReqPullTimeoutMs\n\t}\n\n\tresp := s.lalsvr.CtrlStartRelayPull(pullReq)\n\tif resp.ErrorCode != base.ErrorCodeSucc {\n\t\tc.JSON(http.StatusOK, ZlmAddStreamProxyResp{ZlmFixedHeader: ZlmFixedHeader{Code: -1, Msg: resp.Desp}})\n\t\treturn\n\t}\n\n\tLog.Infof(\"zlm compat addStreamProxy. stream=%s, session_id=%s\", streamName, resp.Data.SessionId)\n\tvar out ZlmAddStreamProxyResp\n\tout.Code = 0\n\tout.Data.Key = resp.Data.SessionId\n\tc.JSON(http.StatusOK, out)\n}\n\n// ---------- startRecord ----------\n\nfunc (s *LalMaxServer) zlmStartRecordHandler(c *gin.Context) {\n\tvar req ZlmStartRecordReq\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tc.JSON(http.StatusOK, ZlmStartRecordResp{ZlmFixedHeader: ZlmFixedHeader{Code: -300, Msg: \"invalid params\"}})\n\t\treturn\n\t}\n\n\trtmpAddr := extractHostPort(s.conf, \"rtmp\")\n\tif rtmpAddr == \"\" {\n\t\tc.JSON(http.StatusOK, ZlmStartRecordResp{ZlmFixedHeader: ZlmFixedHeader{Code: -1, Msg: \"rtmp not configured\"}})\n\t\treturn\n\t}\n\n\t_, err := s.recorder.startRecord(rtmpAddr, req.App, req.Stream, req.Type, req.MaxSecond)\n\tif err != nil {\n\t\tLog.Errorf(\"zlm compat startRecord failed. stream=%s, err=%v\", req.Stream, err)\n\t\tc.JSON(http.StatusOK, ZlmStartRecordResp{ZlmFixedHeader: ZlmFixedHeader{Code: -1, Msg: err.Error()}, Result: false})\n\t\treturn\n\t}\n\n\tLog.Infof(\"zlm compat startRecord. app=%s, stream=%s, type=%d\", req.App, req.Stream, req.Type)\n\tc.JSON(http.StatusOK, ZlmStartRecordResp{ZlmFixedHeader: ZlmFixedHeader{Code: 0}, Result: true})\n}\n\n// ---------- stopRecord ----------\n\nfunc (s *LalMaxServer) zlmStopRecordHandler(c *gin.Context) {\n\tvar req ZlmStopRecordReq\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tc.JSON(http.StatusOK, ZlmStopRecordResp{ZlmFixedHeader: ZlmFixedHeader{Code: -300, Msg: \"invalid params\"}})\n\t\treturn\n\t}\n\n\tfile, err := s.recorder.stopRecord(req.App, req.Stream, req.Type)\n\tif err != nil {\n\t\tLog.Infof(\"zlm compat stopRecord not recording. app=%s, stream=%s, err=%v\", req.App, req.Stream, err)\n\t\tc.JSON(http.StatusOK, ZlmStopRecordResp{ZlmFixedHeader: ZlmFixedHeader{Code: 0}, Result: false})\n\t\treturn\n\t}\n\n\tLog.Infof(\"zlm compat stopRecord. app=%s, stream=%s, file=%s\", req.App, req.Stream, file)\n\tc.JSON(http.StatusOK, ZlmStopRecordResp{ZlmFixedHeader: ZlmFixedHeader{Code: 0}, Result: true})\n}\n\n// ---------- getSnap ----------\n\nfunc (s *LalMaxServer) zlmGetSnapHandler(c *gin.Context) {\n\tvar req ZlmGetSnapReq\n\tif err := c.ShouldBindJSON(&req); err != nil {\n\t\tc.JSON(http.StatusOK, ZlmFixedHeader{Code: -300, Msg: \"invalid params\"})\n\t\treturn\n\t}\n\n\tif req.URL == \"\" {\n\t\tc.JSON(http.StatusOK, ZlmFixedHeader{Code: -300, Msg: \"url is required\"})\n\t\treturn\n\t}\n\n\tdata, err := getSnap(req.URL, req.TimeoutSec)\n\tif err != nil {\n\t\tLog.Errorf(\"zlm compat getSnap failed. url=%s, err=%v\", req.URL, err)\n\t\tc.JSON(http.StatusOK, ZlmFixedHeader{Code: -1, Msg: err.Error()})\n\t\treturn\n\t}\n\n\tLog.Infof(\"zlm compat getSnap. url=%s, size=%d\", req.URL, len(data))\n\tc.Data(http.StatusOK, \"image/jpeg\", data)\n}\n\n// ---------- webrtc ----------\n\n// zlmWebrtcHandler ZLM 兼容 WebRTC 信令接口\n// 为什么：gb28181 前端通过 /index/api/webrtc?app=xx&stream=xx&type=play 播放\nfunc (s *LalMaxServer) zlmWebrtcHandler(c *gin.Context) {\n\ttyp := c.Query(\"type\")\n\tapp := c.Query(\"app\")\n\tstream := c.Query(\"stream\")\n\n\tif stream == \"\" || typ != \"play\" {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": -1, \"msg\": \"only type=play supported\"})\n\t\treturn\n\t}\n\n\tif s.rtcsvr == nil {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": -1, \"msg\": \"webrtc not enabled\"})\n\t\treturn\n\t}\n\n\tbody, err := c.GetRawData()\n\tif err != nil || len(body) == 0 {\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": -1, \"msg\": \"invalid sdp offer\"})\n\t\treturn\n\t}\n\n\tLog.Infof(\"zlm compat webrtc play. app=%s, stream=%s\", app, stream)\n\n\tsdp, err := s.rtcsvr.HandleZlmWebrtcPlay(app, stream, string(body))\n\tif err != nil {\n\t\tLog.Errorf(\"zlm compat webrtc play failed. app=%s, stream=%s, err=%v\", app, stream, err)\n\t\tc.JSON(http.StatusOK, gin.H{\"code\": -1, \"msg\": err.Error()})\n\t\treturn\n\t}\n\n\tc.JSON(http.StatusOK, gin.H{\n\t\t\"code\": 0,\n\t\t\"id\":   s.conf.ServerId,\n\t\t\"sdp\":  sdp,\n\t\t\"type\": \"answer\",\n\t})\n}\n\n// extractHostPort 从 lal 原始配置中提取指定协议的 host:port\n// 为什么有默认值：ZLM 模式下 gb28181 假设 RTMP 总在标准端口可用\nfunc extractHostPort(conf *config.Config, protocol string) string {\n\tvar raw lalRawPorts\n\tif len(conf.LalRawContent) > 0 {\n\t\t_ = json.Unmarshal(conf.LalRawContent, &raw)\n\t}\n\tswitch protocol {\n\tcase \"rtmp\":\n\t\taddr := raw.Rtmp.Addr\n\t\tif addr == \"\" {\n\t\t\treturn \"127.0.0.1:1935\"\n\t\t}\n\t\tif addr[0] == ':' {\n\t\t\treturn \"127.0.0.1\" + addr\n\t\t}\n\t\treturn addr\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "server/server.go",
    "content": "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\"github.com/q191201771/lalmax/rtc\"\n\n\t\"github.com/q191201771/lalmax/gb28181/rtppub\"\n\n\tmaxlogic \"github.com/q191201771/lalmax/logic\"\n\n\thttpfmp4 \"github.com/q191201771/lalmax/fmp4/http-fmp4\"\n\n\t\"github.com/q191201771/lalmax/fmp4/hls\"\n\n\tconfig \"github.com/q191201771/lalmax/config\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/lal/pkg/logic\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\ntype LalMaxServer struct {\n\tlalsvr      logic.ILalServer\n\tconf        *config.Config\n\tstats       *maxlogic.StatAggregator\n\tnotifyHub   *HttpNotify\n\tsrtsvr      *srt.SrtServer\n\trtcsvr      *rtc.RtcServer\n\trouter      *gin.Engine\n\trouterTls   *gin.Engine\n\thttpfmp4svr *httpfmp4.HttpFmp4Server\n\thlssvr      *hls.HlsServer\n\trtpPubMgr   *rtppub.Manager\n\trecorder    *ffmpegRecorder\n}\n\nfunc NewLalMaxServer(conf *config.Config) (*LalMaxServer, error) {\n\tnotifyHub := NewHttpNotify(conf.HttpNotifyConfig, conf.ServerId)\n\tlalsvr := logic.NewLalServer(func(option *logic.Option) {\n\t\tif len(conf.LalRawContent) != 0 {\n\t\t\toption.ConfRawContent = conf.LalRawContent\n\t\t} else {\n\t\t\toption.ConfFilename = conf.LalSvrConfigPath\n\t\t}\n\t\toption.NotifyHandler = notifyHub\n\t})\n\n\tmaxsvr := &LalMaxServer{\n\t\tlalsvr:    lalsvr,\n\t\tconf:      conf,\n\t\tstats:     maxlogic.NewStatAggregator(maxlogic.GetGroupManagerInstance()),\n\t\tnotifyHub: notifyHub,\n\t\trtpPubMgr: rtppub.NewManager(lalsvr, conf.GB28181Config.MediaConfig),\n\t\trecorder:  newFfmpegRecorder(\"\"),\n\t}\n\n\t// 注入 sub 数量查询，用于 on_stream_none_reader 判断\n\tnotifyHub.SetSubCountFn(func(streamName string) int {\n\t\tfor _, g := range lalsvr.StatAllGroup() {\n\t\t\tif g.StreamName == streamName {\n\t\t\t\treturn len(g.StatSubs)\n\t\t\t}\n\t\t}\n\t\treturn 0\n\t})\n\n\tif conf.SrtConfig.Enable {\n\t\tmaxsvr.srtsvr = srt.NewSrtServer(conf.SrtConfig.Addr, lalsvr, func(option *srt.SrtOption) {\n\t\t\toption.Latency = 300\n\t\t\toption.PeerLatency = 300\n\t\t})\n\t}\n\n\tif conf.RtcConfig.Enable {\n\t\tvar err error\n\t\tmaxsvr.rtcsvr, err = rtc.NewRtcServer(conf.RtcConfig, lalsvr)\n\t\tif err != nil {\n\t\t\tnazalog.Error(\"create rtc svr failed, err:\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\tmaxsvr.rtcsvr.SetStreamNotFoundFn(func(app, stream, schema string) {\n\t\t\tnotifyHub.NotifyStreamNotFound(ZlmOnStreamNotFoundPayload{\n\t\t\t\tMediaServerID: conf.ServerId,\n\t\t\t\tApp:           app,\n\t\t\t\tStream:        stream,\n\t\t\t\tSchema:        schema,\n\t\t\t\tVhost:         \"__defaultVhost__\",\n\t\t\t})\n\t\t})\n\t}\n\n\tif conf.Fmp4Config.Http.Enable {\n\t\tmaxsvr.httpfmp4svr = httpfmp4.NewHttpFmp4Server()\n\t}\n\n\tif conf.Fmp4Config.Hls.Enable {\n\t\tmaxsvr.hlssvr = hls.NewHlsServer(conf.Fmp4Config.Hls)\n\t}\n\n\tmaxsvr.router = gin.Default()\n\tmaxsvr.InitRouter(maxsvr.router)\n\tif conf.HttpConfig.EnableHttps {\n\t\tmaxsvr.routerTls = gin.Default()\n\t\tmaxsvr.InitRouter(maxsvr.routerTls)\n\t}\n\n\treturn maxsvr, nil\n}\n\nfunc (s *LalMaxServer) Run() (err error) {\n\ts.lalsvr.WithOnHookSession(func(uniqueKey string, streamName string) logic.ICustomizeHookSessionContext {\n\t\tkey := maxlogic.StreamKeyFromStreamName(streamName)\n\t\tgroup, created := maxlogic.GetGroupManagerInstance().GetOrCreateGroupByStreamName(uniqueKey, streamName, s.hlssvr, s.conf.LogicConfig.GopCacheNum, s.conf.LogicConfig.SingleGopMaxFrameNum)\n\t\tgroup.BindActiveHook(key, func(activeKey maxlogic.StreamKey) {\n\t\t\tif s.notifyHub == nil || !activeKey.Valid() {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ts.notifyHub.NotifyStreamActive(HookGroupInfo{\n\t\t\t\tAppName:    activeKey.AppName,\n\t\t\t\tStreamName: activeKey.StreamName,\n\t\t\t})\n\t\t})\n\t\tgroup.BindStopHook(key, func(stopKey maxlogic.StreamKey) {\n\t\t\tif s.notifyHub == nil || !stopKey.Valid() {\n\t\t\t\treturn\n\t\t\t}\n\t\t\ts.notifyHub.NotifyGroupStop(HookGroupInfo{\n\t\t\t\tAppName:    stopKey.AppName,\n\t\t\t\tStreamName: stopKey.StreamName,\n\t\t\t})\n\t\t})\n\t\tif created && s.notifyHub != nil {\n\t\t\ts.notifyHub.NotifyGroupStart(HookGroupInfo{\n\t\t\t\tAppName:    key.AppName,\n\t\t\t\tStreamName: key.StreamName,\n\t\t\t})\n\t\t}\n\t\treturn group\n\t})\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tif s.srtsvr != nil {\n\t\tgo s.srtsvr.Run(ctx)\n\t}\n\n\tgo s.runPeriodicUpdate(ctx)\n\tgo s.runPeriodicKeepalive(ctx)\n\n\tgo func() {\n\t\tnazalog.Infof(\"lalmax http listen. addr=%s\", s.conf.HttpConfig.ListenAddr)\n\t\tif err = s.router.Run(s.conf.HttpConfig.ListenAddr); err != nil {\n\t\t\tnazalog.Infof(\"lalmax http stop. addr=%s\", s.conf.HttpConfig.ListenAddr)\n\t\t}\n\t}()\n\n\tif s.conf.HttpConfig.EnableHttps {\n\t\tserver := &http.Server{Addr: s.conf.HttpConfig.HttpsListenAddr, Handler: s.routerTls, TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){}}\n\t\tgo func() {\n\t\t\tnazalog.Infof(\"lalmax https listen. addr=%s\", s.conf.HttpConfig.HttpsListenAddr)\n\t\t\tif err = server.ListenAndServeTLS(s.conf.HttpConfig.HttpsCertFile, s.conf.HttpConfig.HttpsKeyFile); err != nil {\n\t\t\t\tnazalog.Infof(\"lalmax https stop. addr=%s\", s.conf.HttpConfig.ListenAddr)\n\t\t\t}\n\t\t}()\n\t}\n\n\treturn s.lalsvr.RunLoop()\n}\n\nfunc (s *LalMaxServer) runPeriodicUpdate(ctx context.Context) {\n\tif s == nil || s.notifyHub == nil || s.lalsvr == nil {\n\t\treturn\n\t}\n\n\tintervalSec := s.conf.HttpNotifyConfig.UpdateIntervalSec\n\tif intervalSec <= 0 {\n\t\treturn\n\t}\n\n\tticker := time.NewTicker(time.Duration(intervalSec) * time.Second)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-ticker.C:\n\t\t\ts.notifyHub.NotifyUpdate(base.UpdateInfo{\n\t\t\t\tGroups: s.lalsvr.StatAllGroup(),\n\t\t\t})\n\t\t}\n\t}\n}\n\n// runPeriodicKeepalive ZLM 兼容：定时发送 on_server_keepalive\nfunc (s *LalMaxServer) runPeriodicKeepalive(ctx context.Context) {\n\tif s == nil || s.notifyHub == nil {\n\t\treturn\n\t}\n\n\tintervalSec := s.conf.HttpNotifyConfig.KeepaliveIntervalSec\n\tif intervalSec <= 0 {\n\t\treturn\n\t}\n\n\tticker := time.NewTicker(time.Duration(intervalSec) * time.Second)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-ticker.C:\n\t\t\ts.notifyHub.NotifyServerKeepalive()\n\t\t}\n\t}\n}\n\nfunc (s *LalMaxServer) HookHub() *HttpNotify {\n\treturn s.notifyHub\n}\n\nfunc (s *LalMaxServer) RegisterHookPlugin(plugin HookPlugin, options HookPluginOptions) (func(), error) {\n\tif s == nil || s.notifyHub == nil {\n\t\treturn nil, fmt.Errorf(\"hook hub not initialized\")\n\t}\n\treturn s.notifyHub.RegisterPlugin(plugin, options)\n}\n"
  },
  {
    "path": "server/stat_view.go",
    "content": "package server\n\nimport (\n\t\"github.com/q191201771/lal/pkg/base\"\n\tmaxlogic \"github.com/q191201771/lalmax/logic\"\n)\n\ntype LalmaxGroupStat struct {\n\tExtSubs []base.StatSub `json:\"ext_subs\"`\n}\n\ntype LalmaxStatGroup struct {\n\tStreamName  string              `json:\"stream_name\"`\n\tAppName     string              `json:\"app_name\"`\n\tAudioCodec  string              `json:\"audio_codec\"`\n\tVideoCodec  string              `json:\"video_codec\"`\n\tVideoWidth  int                 `json:\"video_width\"`\n\tVideoHeight int                 `json:\"video_height\"`\n\tStatPub     base.StatPub        `json:\"pub\"`\n\tStatSubs    []base.StatSub      `json:\"subs\"`\n\tStatPull    base.StatPull       `json:\"pull\"`\n\tFps         []base.RecordPerSec `json:\"in_frame_per_sec\"`\n\tLalmax      LalmaxGroupStat     `json:\"lalmax\"`\n}\n\ntype ApiStatGroupResp struct {\n\tbase.ApiRespBasic\n\tData *LalmaxStatGroup `json:\"data\"`\n}\n\ntype ApiStatAllGroupResp struct {\n\tbase.ApiRespBasic\n\tData struct {\n\t\tGroups []LalmaxStatGroup `json:\"groups\"`\n\t} `json:\"data\"`\n}\n\nfunc newLalmaxStatGroup(view maxlogic.StatGroupView) LalmaxStatGroup {\n\tgroup := view.Group\n\treturn LalmaxStatGroup{\n\t\tStreamName:  group.StreamName,\n\t\tAppName:     group.AppName,\n\t\tAudioCodec:  group.AudioCodec,\n\t\tVideoCodec:  group.VideoCodec,\n\t\tVideoWidth:  group.VideoWidth,\n\t\tVideoHeight: group.VideoHeight,\n\t\tStatPub:     group.StatPub,\n\t\tStatSubs:    group.StatSubs,\n\t\tStatPull:    group.StatPull,\n\t\tFps:         group.Fps,\n\t\tLalmax: LalmaxGroupStat{\n\t\t\tExtSubs: view.ExtSubs,\n\t\t},\n\t}\n}\n\nfunc newLalmaxStatGroups(views []maxlogic.StatGroupView) []LalmaxStatGroup {\n\tif len(views) == 0 {\n\t\treturn nil\n\t}\n\n\tout := make([]LalmaxStatGroup, len(views))\n\tfor i, view := range views {\n\t\tout[i] = newLalmaxStatGroup(view)\n\t}\n\treturn out\n}\n"
  },
  {
    "path": "server/zlm_compat_config.go",
    "content": "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/config\"\n)\n\n// lalRawPorts 从 LalRawContent 中提取 lal 的端口配置\ntype lalRawPorts struct {\n\tRtmp struct {\n\t\tAddr     string `json:\"addr\"`\n\t\tSslAddr  string `json:\"rtmps_addr\"`\n\t} `json:\"rtmp\"`\n\tRtsp struct {\n\t\tAddr     string `json:\"addr\"`\n\t\tSslAddr  string `json:\"rtsps_addr\"`\n\t} `json:\"rtsp\"`\n}\n\n// buildZlmServerConfig 将 lalmax 配置转换为 ZLM getServerConfig 响应格式\n// 为什么：owl 的 ZLMDriver.Connect 依赖 data[0] 中的 http.port / rtmp.port 等字段来更新端口信息\nfunc buildZlmServerConfig(conf *config.Config) map[string]any {\n\tcfg := make(map[string]any)\n\n\tcfg[\"general.mediaServerId\"] = conf.ServerId\n\n\t// 从 lal raw config 中提取 rtmp/rtsp 端口\n\tvar lalPorts lalRawPorts\n\tif len(conf.LalRawContent) > 0 {\n\t\t_ = json.Unmarshal(conf.LalRawContent, &lalPorts)\n\t}\n\tcfg[\"rtmp.port\"] = extractPort(lalPorts.Rtmp.Addr)\n\tcfg[\"rtmp.sslport\"] = extractPort(lalPorts.Rtmp.SslAddr)\n\tcfg[\"rtsp.port\"] = extractPort(lalPorts.Rtsp.Addr)\n\tcfg[\"rtsp.sslport\"] = extractPort(lalPorts.Rtsp.SslAddr)\n\tcfg[\"http.port\"] = extractPort(conf.HttpConfig.ListenAddr)\n\tcfg[\"http.sslport\"] = extractPort(conf.HttpConfig.HttpsListenAddr)\n\n\t// rtp_proxy 端口从 gb28181 配置获取\n\tcfg[\"rtp_proxy.port\"] = strconv.Itoa(int(conf.GB28181Config.MediaConfig.ListenPort))\n\trtpBase := int(conf.GB28181Config.MediaConfig.ListenPort)\n\trtpMax := rtpBase + int(conf.GB28181Config.MediaConfig.MultiPortMaxIncrement)\n\tif rtpBase > 0 && rtpMax > rtpBase {\n\t\tcfg[\"rtp_proxy.port_range\"] = fmt.Sprintf(\"%d-%d\", rtpBase+1, rtpMax)\n\t} else {\n\t\tcfg[\"rtp_proxy.port_range\"] = \"30000-35000\"\n\t}\n\n\t// --- RTC 配置 ---\n\tif conf.RtcConfig.Enable {\n\t\tcfg[\"rtc.port\"] = strconv.Itoa(conf.RtcConfig.ICEUDPMuxPort)\n\t\tcfg[\"rtc.tcpPort\"] = strconv.Itoa(conf.RtcConfig.ICETCPMuxPort)\n\t} else {\n\t\tcfg[\"rtc.port\"] = \"0\"\n\t\tcfg[\"rtc.tcpPort\"] = \"0\"\n\t}\n\n\t// --- Hook 配置 ---\n\tcfg[\"hook.enable\"] = boolStr(conf.HttpNotifyConfig.Enable)\n\tcfg[\"hook.alive_interval\"] = strconv.Itoa(conf.HttpNotifyConfig.KeepaliveIntervalSec)\n\tcfg[\"hook.on_stream_changed\"] = conf.HttpNotifyConfig.ZlmOnStreamChanged\n\tcfg[\"hook.on_server_keepalive\"] = conf.HttpNotifyConfig.ZlmOnServerKeepalive\n\tcfg[\"hook.on_stream_none_reader\"] = conf.HttpNotifyConfig.ZlmOnStreamNoneReader\n\tcfg[\"hook.on_rtp_server_timeout\"] = conf.HttpNotifyConfig.ZlmOnRtpServerTimeout\n\tcfg[\"hook.on_record_mp4\"] = conf.HttpNotifyConfig.ZlmOnRecordMp4\n\tcfg[\"hook.on_server_started\"] = conf.HttpNotifyConfig.ZlmOnServerStarted\n\tcfg[\"hook.on_publish\"] = conf.HttpNotifyConfig.ZlmOnPublish\n\tcfg[\"hook.on_play\"] = conf.HttpNotifyConfig.ZlmOnPlay\n\tcfg[\"hook.on_flow_report\"] = \"\"\n\tcfg[\"hook.on_http_access\"] = \"\"\n\tcfg[\"hook.on_rtsp_auth\"] = \"\"\n\tcfg[\"hook.on_rtsp_realm\"] = \"\"\n\tcfg[\"hook.on_shell_login\"] = \"\"\n\tcfg[\"hook.on_send_rtp_stopped\"] = \"\"\n\tcfg[\"hook.on_server_exited\"] = \"\"\n\tcfg[\"hook.on_stream_not_found\"] = conf.HttpNotifyConfig.ZlmOnStreamNotFound\n\tcfg[\"hook.on_record_ts\"] = \"\"\n\thookTimeout := conf.HttpNotifyConfig.HookTimeoutSec\n\tif hookTimeout <= 0 {\n\t\thookTimeout = 10\n\t}\n\tcfg[\"hook.timeoutSec\"] = strconv.Itoa(hookTimeout)\n\tcfg[\"hook.retry\"] = \"1\"\n\tcfg[\"hook.retry_delay\"] = \"3\"\n\tcfg[\"hook.stream_changed_schemas\"] = \"\"\n\n\t// --- 默认值填充 ---\n\tcfg[\"api.secret\"] = \"\"\n\tcfg[\"api.apiDebug\"] = \"1\"\n\n\treturn cfg\n}\n\n// extractPort 从 \":1935\" 或 \"0.0.0.0:1935\" 格式中提取端口号字符串\nfunc extractPort(addr string) string {\n\tif addr == \"\" {\n\t\treturn \"0\"\n\t}\n\t_, portStr, err := net.SplitHostPort(addr)\n\tif err != nil {\n\t\treturn \"0\"\n\t}\n\treturn portStr\n}\n\n// parsePortRange 解析 \"30000-35000\" 格式的端口范围\n// 为什么：owl 通过 setServerConfig 下发端口范围字符串，需转换为 min/max int\nfunc parsePortRange(s string) (int, int, bool) {\n\tidx := strings.Index(s, \"-\")\n\tif idx <= 0 || idx == len(s)-1 {\n\t\treturn 0, 0, false\n\t}\n\tminPort, err1 := strconv.Atoi(strings.TrimSpace(s[:idx]))\n\tmaxPort, err2 := strconv.Atoi(strings.TrimSpace(s[idx+1:]))\n\tif err1 != nil || err2 != nil || minPort <= 0 || maxPort <= minPort {\n\t\treturn 0, 0, false\n\t}\n\treturn minPort, maxPort, true\n}\n\nfunc boolStr(v bool) string {\n\tif v {\n\t\treturn \"1\"\n\t}\n\treturn \"0\"\n}\n"
  },
  {
    "path": "server/zlm_compat_ffmpeg.go",
    "content": "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 管理 ffmpeg 录像进程\n// 为什么用 ffmpeg：lal 内核无按需录像 API，ffmpeg 可从 RTMP 拉流写 MP4，与 ZLM 行为一致\ntype ffmpegRecorder struct {\n\tmu        sync.Mutex\n\tsessions  map[string]*recordSession\n\toutputDir string\n}\n\ntype recordSession struct {\n\tcmd    *exec.Cmd\n\tcancel context.CancelFunc\n\tapp    string\n\tstream string\n\tfile   string\n\tstart  time.Time\n}\n\nfunc newFfmpegRecorder(outputDir string) *ffmpegRecorder {\n\tif outputDir == \"\" {\n\t\toutputDir = \"./record\"\n\t}\n\treturn &ffmpegRecorder{\n\t\tsessions:  make(map[string]*recordSession),\n\t\toutputDir: outputDir,\n\t}\n}\n\n// recordKey 生成录像会话唯一标识\nfunc recordKey(app, stream string, typ int) string {\n\treturn fmt.Sprintf(\"%d/%s/%s\", typ, app, stream)\n}\n\n// startRecord 启动 ffmpeg 从 RTMP 拉流并录制为 MP4\nfunc (r *ffmpegRecorder) startRecord(rtmpAddr, app, stream string, typ int, maxSecond int) (string, error) {\n\tkey := recordKey(app, stream, typ)\n\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\n\tif _, ok := r.sessions[key]; ok {\n\t\treturn \"\", fmt.Errorf(\"already recording: %s\", key)\n\t}\n\n\tdir := filepath.Join(r.outputDir, app, stream)\n\tif err := os.MkdirAll(dir, 0o755); err != nil {\n\t\treturn \"\", fmt.Errorf(\"create record dir: %w\", err)\n\t}\n\n\tfilename := fmt.Sprintf(\"%s_%s.mp4\", stream, time.Now().Format(\"20060102_150405\"))\n\toutPath := filepath.Join(dir, filename)\n\n\tsrcURL := fmt.Sprintf(\"rtmp://%s/%s/%s\", rtmpAddr, app, stream)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\n\targs := []string{\n\t\t\"-hide_banner\",\n\t\t\"-loglevel\", \"warning\",\n\t\t\"-i\", srcURL,\n\t\t\"-c\", \"copy\",\n\t\t\"-movflags\", \"+faststart\",\n\t}\n\tif maxSecond > 0 {\n\t\targs = append(args, \"-t\", fmt.Sprintf(\"%d\", maxSecond))\n\t}\n\targs = append(args, \"-y\", outPath)\n\n\tcmd := exec.CommandContext(ctx, \"ffmpeg\", args...)\n\tcmd.Stdout = nil\n\tcmd.Stderr = nil\n\n\tif err := cmd.Start(); err != nil {\n\t\tcancel()\n\t\treturn \"\", fmt.Errorf(\"ffmpeg start: %w\", err)\n\t}\n\n\tsess := &recordSession{\n\t\tcmd:    cmd,\n\t\tcancel: cancel,\n\t\tapp:    app,\n\t\tstream: stream,\n\t\tfile:   outPath,\n\t\tstart:  time.Now(),\n\t}\n\tr.sessions[key] = sess\n\n\tgo func() {\n\t\t_ = cmd.Wait()\n\t\tr.mu.Lock()\n\t\tdelete(r.sessions, key)\n\t\tr.mu.Unlock()\n\t\tLog.Infof(\"ffmpeg record finished. key=%s, file=%s\", key, outPath)\n\t}()\n\n\tLog.Infof(\"ffmpeg record started. key=%s, file=%s, src=%s\", key, outPath, srcURL)\n\treturn outPath, nil\n}\n\n// stopRecord 终止 ffmpeg 录像进程\nfunc (r *ffmpegRecorder) stopRecord(app, stream string, typ int) (string, error) {\n\tkey := recordKey(app, stream, typ)\n\n\tr.mu.Lock()\n\tsess, ok := r.sessions[key]\n\tif !ok {\n\t\tr.mu.Unlock()\n\t\treturn \"\", fmt.Errorf(\"not recording: %s\", key)\n\t}\n\tdelete(r.sessions, key)\n\tr.mu.Unlock()\n\n\tsess.cancel()\n\t_ = sess.cmd.Wait()\n\tLog.Infof(\"ffmpeg record stopped. key=%s, file=%s, duration=%s\", key, sess.file, time.Since(sess.start))\n\treturn sess.file, nil\n}\n\n// getSnap 用 ffmpeg 从指定 URL 截取一帧 JPEG 图片\nfunc getSnap(srcURL string, timeoutSec int) ([]byte, error) {\n\tif timeoutSec <= 0 {\n\t\ttimeoutSec = 10\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSec)*time.Second)\n\tdefer cancel()\n\n\targs := []string{\n\t\t\"-hide_banner\",\n\t\t\"-loglevel\", \"warning\",\n\t\t\"-i\", srcURL,\n\t\t\"-vframes\", \"1\",\n\t\t\"-f\", \"image2\",\n\t\t\"-vcodec\", \"mjpeg\",\n\t\t\"pipe:1\",\n\t}\n\n\tcmd := exec.CommandContext(ctx, \"ffmpeg\", args...)\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ffmpeg snap: %w\", err)\n\t}\n\n\tif len(out) == 0 {\n\t\treturn nil, fmt.Errorf(\"ffmpeg snap: empty output\")\n\t}\n\n\treturn out, nil\n}\n"
  },
  {
    "path": "server/zlm_compat_test.go",
    "content": "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\n\tconfig \"github.com/q191201771/lalmax/config\"\n\n\t\"github.com/q191201771/lal/pkg/base\"\n)\n\n// ===========================================================================\n// REST API 兼容测试\n// ===========================================================================\n\nfunc TestZlmCompatOpenRtpServer(t *testing.T) {\n\tbody := `{\"port\":0,\"tcp_mode\":0,\"stream_id\":\"zlm_compat_rtp_test\"}`\n\tr := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"POST\", \"/index/api/openRtpServer\", strings.NewReader(body))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tmax.router.ServeHTTP(r, req)\n\n\tif r.Code != http.StatusOK {\n\t\tt.Fatalf(\"unexpected status: %d, body: %s\", r.Code, r.Body.String())\n\t}\n\n\tvar resp ZlmOpenRtpServerResp\n\tif err := json.NewDecoder(r.Body).Decode(&resp); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp.Code != 0 {\n\t\tt.Fatalf(\"expected code=0, got %d msg=%s\", resp.Code, resp.Msg)\n\t}\n\tif resp.Port == 0 {\n\t\tt.Fatal(\"expected non-zero port\")\n\t}\n\n\t// 清理：关闭刚开启的 RTP 服务\n\tt.Cleanup(func() {\n\t\tcloseBody := `{\"stream_id\":\"zlm_compat_rtp_test\"}`\n\t\tcr := httptest.NewRecorder()\n\t\tcreq := httptest.NewRequest(\"POST\", \"/index/api/closeRtpServer\", strings.NewReader(closeBody))\n\t\tcreq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tmax.router.ServeHTTP(cr, creq)\n\t})\n}\n\nfunc TestZlmCompatCloseRtpServer(t *testing.T) {\n\t// 先开启\n\topenBody := `{\"port\":0,\"tcp_mode\":0,\"stream_id\":\"zlm_close_rtp_test\"}`\n\tr := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"POST\", \"/index/api/openRtpServer\", strings.NewReader(openBody))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tmax.router.ServeHTTP(r, req)\n\n\tif r.Code != http.StatusOK {\n\t\tt.Fatalf(\"open failed: %d %s\", r.Code, r.Body.String())\n\t}\n\n\t// 再关闭\n\tcloseBody := `{\"stream_id\":\"zlm_close_rtp_test\"}`\n\tr = httptest.NewRecorder()\n\treq = httptest.NewRequest(\"POST\", \"/index/api/closeRtpServer\", strings.NewReader(closeBody))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tmax.router.ServeHTTP(r, req)\n\n\tif r.Code != http.StatusOK {\n\t\tt.Fatalf(\"close failed: %d %s\", r.Code, r.Body.String())\n\t}\n\n\tvar resp ZlmCloseRtpServerResp\n\tif err := json.NewDecoder(r.Body).Decode(&resp); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp.Code != 0 {\n\t\tt.Fatalf(\"expected code=0, got %d\", resp.Code)\n\t}\n\tif resp.Hit != 1 {\n\t\tt.Fatalf(\"expected hit=1, got %d\", resp.Hit)\n\t}\n}\n\nfunc TestZlmCompatCloseRtpServerNotFound(t *testing.T) {\n\tbody := `{\"stream_id\":\"nonexistent_stream_id\"}`\n\tr := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"POST\", \"/index/api/closeRtpServer\", strings.NewReader(body))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tmax.router.ServeHTTP(r, req)\n\n\tif r.Code != http.StatusOK {\n\t\tt.Fatalf(\"unexpected status: %d\", r.Code)\n\t}\n\n\tvar resp ZlmCloseRtpServerResp\n\tif err := json.NewDecoder(r.Body).Decode(&resp); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp.Hit != 0 {\n\t\tt.Fatalf(\"expected hit=0 for nonexistent stream, got %d\", resp.Hit)\n\t}\n}\n\nfunc TestZlmCompatCloseStreams(t *testing.T) {\n\tstreamName := uniqueTestName(\"zlm_close_stream\")\n\t_, err := max.lalsvr.AddCustomizePubSession(streamName)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tbody := `{\"app\":\"\",\"stream\":\"` + streamName + `\"}`\n\tr := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"POST\", \"/index/api/close_streams\", strings.NewReader(body))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tmax.router.ServeHTTP(r, req)\n\n\tif r.Code != http.StatusOK {\n\t\tt.Fatalf(\"unexpected status: %d %s\", r.Code, r.Body.String())\n\t}\n\n\tvar resp ZlmCloseStreamsResp\n\tif err := json.NewDecoder(r.Body).Decode(&resp); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp.Code != 0 {\n\t\tt.Fatalf(\"expected code=0, got %d\", resp.Code)\n\t}\n\tif resp.CountHit == 0 {\n\t\tt.Fatal(\"expected count_hit > 0\")\n\t}\n}\n\nfunc TestZlmCompatGetServerConfig(t *testing.T) {\n\tbody := `{}`\n\tr := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"POST\", \"/index/api/getServerConfig\", strings.NewReader(body))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tmax.router.ServeHTTP(r, req)\n\n\tif r.Code != http.StatusOK {\n\t\tt.Fatalf(\"unexpected status: %d %s\", r.Code, r.Body.String())\n\t}\n\n\tvar resp ZlmGetServerConfigResp\n\tif err := json.NewDecoder(r.Body).Decode(&resp); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp.Code != 0 {\n\t\tt.Fatalf(\"expected code=0, got %d\", resp.Code)\n\t}\n\tif len(resp.Data) == 0 {\n\t\tt.Fatal(\"expected non-empty data array\")\n\t}\n\n\t// 验证返回的配置包含 ZLM 标准字段\n\tcfg := resp.Data[0]\n\trequiredKeys := []string{\n\t\t\"http.port\",\n\t\t\"rtmp.port\",\n\t\t\"rtsp.port\",\n\t\t\"rtp_proxy.port\",\n\t\t\"general.mediaServerId\",\n\t\t\"hook.on_stream_changed\",\n\t}\n\tfor _, key := range requiredKeys {\n\t\tif _, ok := cfg[key]; !ok {\n\t\t\tt.Errorf(\"missing required config key: %s\", key)\n\t\t}\n\t}\n}\n\nfunc TestZlmCompatSetServerConfig(t *testing.T) {\n\tbody := `{\n\t\t\"hook.on_stream_changed\":\"http://127.0.0.1:15123/webhook/on_stream_changed\",\n\t\t\"hook.on_server_keepalive\":\"http://127.0.0.1:15123/webhook/on_server_keepalive\",\n\t\t\"hook.on_publish\":\"http://127.0.0.1:15123/webhook/on_publish\",\n\t\t\"hook.on_play\":\"http://127.0.0.1:15123/webhook/on_play\",\n\t\t\"hook.on_stream_not_found\":\"http://127.0.0.1:15123/webhook/on_stream_not_found\",\n\t\t\"hook.on_stream_none_reader\":\"http://127.0.0.1:15123/webhook/on_stream_none_reader\",\n\t\t\"hook.on_record_mp4\":\"http://127.0.0.1:15123/webhook/on_record_mp4\",\n\t\t\"hook.on_server_started\":\"http://127.0.0.1:15123/webhook/on_server_started\",\n\t\t\"hook.alive_interval\":\"10\"\n\t}`\n\tr := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"POST\", \"/index/api/setServerConfig\", strings.NewReader(body))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tmax.router.ServeHTTP(r, req)\n\n\tif r.Code != http.StatusOK {\n\t\tt.Fatalf(\"unexpected status: %d %s\", r.Code, r.Body.String())\n\t}\n\n\tvar resp ZlmSetServerConfigResp\n\tif err := json.NewDecoder(r.Body).Decode(&resp); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif resp.Code != 0 {\n\t\tt.Fatalf(\"expected code=0, got %d\", resp.Code)\n\t}\n\tif resp.Changed < 8 {\n\t\tt.Fatalf(\"expected at least 8 changed, got %d\", resp.Changed)\n\t}\n\n\t// 验证 getServerConfig 返回更新后的值\n\tr2 := httptest.NewRecorder()\n\treq2 := httptest.NewRequest(\"POST\", \"/index/api/getServerConfig\", strings.NewReader(`{}`))\n\treq2.Header.Set(\"Content-Type\", \"application/json\")\n\tmax.router.ServeHTTP(r2, req2)\n\n\tvar getResp ZlmGetServerConfigResp\n\tjson.NewDecoder(r2.Body).Decode(&getResp)\n\tcfg := getResp.Data[0]\n\n\tif cfg[\"hook.on_stream_changed\"] != \"http://127.0.0.1:15123/webhook/on_stream_changed\" {\n\t\tt.Errorf(\"on_stream_changed not updated: %v\", cfg[\"hook.on_stream_changed\"])\n\t}\n\tif cfg[\"hook.on_publish\"] != \"http://127.0.0.1:15123/webhook/on_publish\" {\n\t\tt.Errorf(\"on_publish not updated: %v\", cfg[\"hook.on_publish\"])\n\t}\n}\n\nfunc TestZlmCompatAddStreamProxy(t *testing.T) {\n\tbody := `{\n\t\t\"vhost\":\"__defaultVhost__\",\n\t\t\"app\":\"live\",\n\t\t\"stream\":\"proxy_test\",\n\t\t\"url\":\"rtmp://127.0.0.1:19350/live/test\",\n\t\t\"retry_count\":0,\n\t\t\"rtp_type\":0,\n\t\t\"timeout_sec\":5\n\t}`\n\tr := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"POST\", \"/index/api/addStreamProxy\", strings.NewReader(body))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tmax.router.ServeHTTP(r, req)\n\n\tif r.Code != http.StatusOK {\n\t\tt.Fatalf(\"unexpected status: %d %s\", r.Code, r.Body.String())\n\t}\n\n\tvar resp ZlmAddStreamProxyResp\n\tif err := json.NewDecoder(r.Body).Decode(&resp); err != nil {\n\t\tt.Fatal(err)\n\t}\n\t// 拉流可能因目标不存在而失败，但响应格式必须正确\n\t// code=0 表示成功，其他值表示拉流失败但格式正确\n\tif resp.Code == 0 && resp.Data.Key == \"\" {\n\t\tt.Fatal(\"code=0 but key is empty\")\n\t}\n}\n\nfunc TestZlmCompatStartStopRecord(t *testing.T) {\n\tstreamName := uniqueTestName(\"zlm_record\")\n\t_, err := max.lalsvr.AddCustomizePubSession(streamName)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tdefer func() {\n\t\t// 清理：尝试停止录制\n\t\tstopBody := `{\"type\":1,\"vhost\":\"__defaultVhost__\",\"app\":\"live\",\"stream\":\"` + streamName + `\"}`\n\t\tsr := httptest.NewRecorder()\n\t\tsreq := httptest.NewRequest(\"POST\", \"/index/api/stopRecord\", strings.NewReader(stopBody))\n\t\tsreq.Header.Set(\"Content-Type\", \"application/json\")\n\t\tmax.router.ServeHTTP(sr, sreq)\n\t}()\n\n\t// 开始录制\n\tstartBody := `{\"type\":1,\"vhost\":\"__defaultVhost__\",\"app\":\"live\",\"stream\":\"` + streamName + `\"}`\n\tr := httptest.NewRecorder()\n\treq := httptest.NewRequest(\"POST\", \"/index/api/startRecord\", strings.NewReader(startBody))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tmax.router.ServeHTTP(r, req)\n\n\tif r.Code != http.StatusOK {\n\t\tt.Fatalf(\"start record unexpected status: %d %s\", r.Code, r.Body.String())\n\t}\n\n\tvar startResp ZlmStartRecordResp\n\tif err := json.NewDecoder(r.Body).Decode(&startResp); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif startResp.Code != 0 {\n\t\tt.Fatalf(\"start record expected code=0, got %d msg=%s\", startResp.Code, startResp.Msg)\n\t}\n\tif !startResp.Result {\n\t\tt.Fatal(\"start record expected result=true\")\n\t}\n\n\t// 停止录制\n\tstopBody := `{\"type\":1,\"vhost\":\"__defaultVhost__\",\"app\":\"live\",\"stream\":\"` + streamName + `\"}`\n\tr = httptest.NewRecorder()\n\treq = httptest.NewRequest(\"POST\", \"/index/api/stopRecord\", strings.NewReader(stopBody))\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\tmax.router.ServeHTTP(r, req)\n\n\tif r.Code != http.StatusOK {\n\t\tt.Fatalf(\"stop record unexpected status: %d %s\", r.Code, r.Body.String())\n\t}\n\n\tvar stopResp ZlmStopRecordResp\n\tif err := json.NewDecoder(r.Body).Decode(&stopResp); err != nil {\n\t\tt.Fatal(err)\n\t}\n\tif stopResp.Code != 0 {\n\t\tt.Fatalf(\"stop record expected code=0, got %d msg=%s\", stopResp.Code, stopResp.Msg)\n\t}\n}\n\n// ===========================================================================\n// Hook 兼容测试\n// ===========================================================================\n\n// TestZlmHookOnStreamChangedFormat 验证 on_stream_changed hook 的 payload 格式与 ZLM 兼容\nfunc TestZlmHookOnStreamChangedFormat(t *testing.T) {\n\treceived := make(chan ZlmOnStreamChangedPayload, 2)\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer r.Body.Close()\n\t\tvar payload ZlmOnStreamChangedPayload\n\t\tif err := json.NewDecoder(r.Body).Decode(&payload); err != nil {\n\t\t\tt.Errorf(\"decode on_stream_changed payload failed: %v\", err)\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\treceived <- payload\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer ts.Close()\n\n\thub := NewHttpNotify(config.HttpNotifyConfig{\n\t\tEnable:                true,\n\t\tZlmCompatHookConfig: config.ZlmCompatHookConfig{ZlmOnStreamChanged: ts.URL},\n\t}, \"zlm-hook-test\")\n\n\tstreamName := uniqueTestName(\"stream_changed_test\")\n\n\t// 模拟推流开始 -> 应触发 on_stream_changed(regist=true)\n\thub.NotifyPubStart(base.PubStartInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tSessionId:  \"pub-session-1\",\n\t\t\tAppName:    \"live\",\n\t\t\tStreamName: streamName,\n\t\t},\n\t})\n\n\tselect {\n\tcase payload := <-received:\n\t\tif !payload.Regist {\n\t\t\tt.Fatal(\"expected regist=true on pub_start\")\n\t\t}\n\t\t// gb28181 优先读 app_name/stream_name（lalmax 兼容字段）\n\t\tif payload.StreamName == \"\" && payload.Stream == \"\" {\n\t\t\tt.Fatal(\"expected stream or stream_name to be set\")\n\t\t}\n\t\tif payload.AppName == \"\" && payload.App == \"\" {\n\t\t\tt.Fatal(\"expected app or app_name to be set\")\n\t\t}\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatal(\"did not receive on_stream_changed for pub_start\")\n\t}\n\n\t// 模拟推流结束 -> 应触发 on_stream_changed(regist=false)\n\thub.NotifyPubStop(base.PubStopInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tSessionId:  \"pub-session-1\",\n\t\t\tAppName:    \"live\",\n\t\t\tStreamName: streamName,\n\t\t},\n\t})\n\n\tselect {\n\tcase payload := <-received:\n\t\tif payload.Regist {\n\t\t\tt.Fatal(\"expected regist=false on pub_stop\")\n\t\t}\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatal(\"did not receive on_stream_changed for pub_stop\")\n\t}\n}\n\n// TestZlmHookOnStreamChangedFieldCompleteness 验证 payload 包含 ZLM 必需字段\nfunc TestZlmHookOnStreamChangedFieldCompleteness(t *testing.T) {\n\treceived := make(chan json.RawMessage, 1)\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer r.Body.Close()\n\t\tvar raw json.RawMessage\n\t\tif err := json.NewDecoder(r.Body).Decode(&raw); err != nil {\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\treceived <- raw\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer ts.Close()\n\n\thub := NewHttpNotify(config.HttpNotifyConfig{\n\t\tEnable:              true,\n\t\tZlmCompatHookConfig: config.ZlmCompatHookConfig{ZlmOnStreamChanged: ts.URL},\n\t}, \"field-test\")\n\n\thub.NotifyPubStart(base.PubStartInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tSessionId:  \"completeness-sess\",\n\t\t\tAppName:    \"live\",\n\t\t\tStreamName: \"completeness-stream\",\n\t\t},\n\t})\n\n\tselect {\n\tcase raw := <-received:\n\t\tvar m map[string]any\n\t\tif err := json.Unmarshal(raw, &m); err != nil {\n\t\t\tt.Fatal(err)\n\t\t}\n\n\t\t// ZLM on_stream_changed 必须包含的字段\n\t\trequiredFields := []string{\n\t\t\t\"regist\",\n\t\t\t\"schema\",\n\t\t\t\"mediaServerId\",\n\t\t\t\"vhost\",\n\t\t}\n\t\tfor _, field := range requiredFields {\n\t\t\tif _, ok := m[field]; !ok {\n\t\t\t\tt.Errorf(\"missing required field in on_stream_changed: %s\", field)\n\t\t\t}\n\t\t}\n\n\t\t// 必须有 app+stream 或 app_name+stream_name\n\t\thasZlmStyle := m[\"app\"] != nil && m[\"stream\"] != nil\n\t\thasLalmaxStyle := m[\"app_name\"] != nil && m[\"stream_name\"] != nil\n\t\tif !hasZlmStyle && !hasLalmaxStyle {\n\t\t\tt.Error(\"payload must contain (app, stream) or (app_name, stream_name)\")\n\t\t}\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatal(\"did not receive on_stream_changed\")\n\t}\n}\n\n// TestZlmHookOnServerKeepalive 验证 keepalive hook 的触发和 payload 格式\nfunc TestZlmHookOnServerKeepalive(t *testing.T) {\n\treceived := make(chan ZlmOnServerKeepalivePayload, 1)\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer r.Body.Close()\n\t\tvar payload ZlmOnServerKeepalivePayload\n\t\tif err := json.NewDecoder(r.Body).Decode(&payload); err != nil {\n\t\t\tt.Errorf(\"decode keepalive payload failed: %v\", err)\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\treceived <- payload\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer ts.Close()\n\n\thub := NewHttpNotify(config.HttpNotifyConfig{\n\t\tEnable:               true,\n\t\tKeepaliveIntervalSec: 1,\n\t\tZlmCompatHookConfig:  config.ZlmCompatHookConfig{ZlmOnServerKeepalive: ts.URL},\n\t}, \"keepalive-test\")\n\n\t// 手动触发 keepalive\n\thub.NotifyServerKeepalive()\n\n\tselect {\n\tcase payload := <-received:\n\t\tif payload.MediaServerID == \"\" {\n\t\t\tt.Fatal(\"expected non-empty mediaServerId\")\n\t\t}\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatal(\"did not receive on_server_keepalive\")\n\t}\n}\n\n// TestZlmHookOnStreamNoneReader 验证无人观看 hook\nfunc TestZlmHookOnStreamNoneReader(t *testing.T) {\n\treceived := make(chan ZlmOnStreamNoneReaderPayload, 1)\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer r.Body.Close()\n\t\tvar payload ZlmOnStreamNoneReaderPayload\n\t\tif err := json.NewDecoder(r.Body).Decode(&payload); err != nil {\n\t\t\tt.Errorf(\"decode none_reader payload failed: %v\", err)\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\treceived <- payload\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer ts.Close()\n\n\thub := NewHttpNotify(config.HttpNotifyConfig{\n\t\tEnable:              true,\n\t\tZlmCompatHookConfig: config.ZlmCompatHookConfig{ZlmOnStreamNoneReader: ts.URL},\n\t}, \"none-reader-test\")\n\n\thub.NotifyStreamNoneReader(ZlmOnStreamNoneReaderPayload{\n\t\tMediaServerID: \"none-reader-test\",\n\t\tApp:           \"live\",\n\t\tSchema:        \"rtmp\",\n\t\tStream:        \"test-stream\",\n\t\tVhost:         \"__defaultVhost__\",\n\t})\n\n\tselect {\n\tcase payload := <-received:\n\t\tif payload.App != \"live\" {\n\t\t\tt.Fatalf(\"expected app=live, got %s\", payload.App)\n\t\t}\n\t\tif payload.Stream != \"test-stream\" {\n\t\t\tt.Fatalf(\"expected stream=test-stream, got %s\", payload.Stream)\n\t\t}\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatal(\"did not receive on_stream_none_reader\")\n\t}\n}\n\n// TestZlmHookOnRtpServerTimeout 验证 RTP 超时 hook\nfunc TestZlmHookOnRtpServerTimeout(t *testing.T) {\n\treceived := make(chan ZlmOnRtpServerTimeoutPayload, 1)\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer r.Body.Close()\n\t\tvar payload ZlmOnRtpServerTimeoutPayload\n\t\tif err := json.NewDecoder(r.Body).Decode(&payload); err != nil {\n\t\t\tt.Errorf(\"decode rtp_timeout payload failed: %v\", err)\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\treceived <- payload\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer ts.Close()\n\n\thub := NewHttpNotify(config.HttpNotifyConfig{\n\t\tEnable:              true,\n\t\tZlmCompatHookConfig: config.ZlmCompatHookConfig{ZlmOnRtpServerTimeout: ts.URL},\n\t}, \"rtp-timeout-test\")\n\n\thub.NotifyRtpServerTimeout(ZlmOnRtpServerTimeoutPayload{\n\t\tLocalPort:     30000,\n\t\tStreamID:      \"timeout_stream\",\n\t\tTCPMode:       0,\n\t\tMediaServerID: \"rtp-timeout-test\",\n\t})\n\n\tselect {\n\tcase payload := <-received:\n\t\tif payload.StreamID != \"timeout_stream\" {\n\t\t\tt.Fatalf(\"expected stream_id=timeout_stream, got %s\", payload.StreamID)\n\t\t}\n\t\tif payload.LocalPort != 30000 {\n\t\t\tt.Fatalf(\"expected local_port=30000, got %d\", payload.LocalPort)\n\t\t}\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatal(\"did not receive on_rtp_server_timeout\")\n\t}\n}\n\n// TestZlmHookOnStreamChangedOrderPerStream 验证同一流的 stream_changed 事件保序\nfunc TestZlmHookOnStreamChangedOrderPerStream(t *testing.T) {\n\tvar order atomic.Int32\n\tfirstDone := make(chan struct{})\n\tsecondDone := make(chan struct{})\n\tallowFirst := make(chan struct{})\n\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer r.Body.Close()\n\t\tvar payload ZlmOnStreamChangedPayload\n\t\tif err := json.NewDecoder(r.Body).Decode(&payload); err != nil {\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tseq := order.Add(1)\n\t\tif seq == 1 {\n\t\t\tclose(firstDone)\n\t\t\t<-allowFirst\n\t\t} else if seq == 2 {\n\t\t\tclose(secondDone)\n\t\t}\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer ts.Close()\n\n\thub := NewHttpNotify(config.HttpNotifyConfig{\n\t\tEnable:              true,\n\t\tZlmCompatHookConfig: config.ZlmCompatHookConfig{ZlmOnStreamChanged: ts.URL},\n\t}, \"order-test\")\n\n\tstreamName := uniqueTestName(\"order_stream\")\n\n\thub.NotifyPubStart(base.PubStartInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tSessionId:  \"order-1\",\n\t\t\tAppName:    \"live\",\n\t\t\tStreamName: streamName,\n\t\t},\n\t})\n\thub.NotifyPubStop(base.PubStopInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tSessionId:  \"order-2\",\n\t\t\tAppName:    \"live\",\n\t\t\tStreamName: streamName,\n\t\t},\n\t})\n\n\tselect {\n\tcase <-firstDone:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"first on_stream_changed not received\")\n\t}\n\n\t// 第二个应被阻塞（同流保序）\n\tselect {\n\tcase <-secondDone:\n\t\tt.Fatal(\"second on_stream_changed should be blocked\")\n\tcase <-time.After(200 * time.Millisecond):\n\t}\n\n\tclose(allowFirst)\n\n\tselect {\n\tcase <-secondDone:\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"second on_stream_changed not received after first finished\")\n\t}\n}\n\n// ---------- on_publish ----------\n\nfunc TestZlmHookOnPublish(t *testing.T) {\n\treceived := make(chan map[string]any, 1)\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tvar m map[string]any\n\t\tjson.NewDecoder(r.Body).Decode(&m)\n\t\treceived <- m\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer ts.Close()\n\n\thub := NewHttpNotify(config.HttpNotifyConfig{\n\t\tEnable:              true,\n\t\tZlmCompatHookConfig: config.ZlmCompatHookConfig{ZlmOnPublish: ts.URL},\n\t}, \"pub-hook-test\")\n\n\thub.NotifyPubStart(base.PubStartInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tAppName:    \"live\",\n\t\t\tStreamName: \"test_pub\",\n\t\t\tProtocol:   \"rtmp\",\n\t\t},\n\t})\n\n\tselect {\n\tcase m := <-received:\n\t\tif m[\"app\"] != \"live\" || m[\"stream\"] != \"test_pub\" || m[\"schema\"] != \"rtmp\" {\n\t\t\tt.Fatalf(\"unexpected on_publish payload: %+v\", m)\n\t\t}\n\t\tif m[\"mediaServerId\"] != \"pub-hook-test\" {\n\t\t\tt.Fatalf(\"unexpected mediaServerId: %v\", m[\"mediaServerId\"])\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"on_publish not received\")\n\t}\n}\n\n// ---------- on_play ----------\n\nfunc TestZlmHookOnPlay(t *testing.T) {\n\treceived := make(chan map[string]any, 1)\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tvar m map[string]any\n\t\tjson.NewDecoder(r.Body).Decode(&m)\n\t\treceived <- m\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer ts.Close()\n\n\thub := NewHttpNotify(config.HttpNotifyConfig{\n\t\tEnable:              true,\n\t\tZlmCompatHookConfig: config.ZlmCompatHookConfig{ZlmOnPlay: ts.URL},\n\t}, \"play-hook-test\")\n\n\thub.NotifySubStart(base.SubStartInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tAppName:    \"live\",\n\t\t\tStreamName: \"test_play\",\n\t\t\tProtocol:   \"rtsp\",\n\t\t},\n\t})\n\n\tselect {\n\tcase m := <-received:\n\t\tif m[\"app\"] != \"live\" || m[\"stream\"] != \"test_play\" || m[\"schema\"] != \"rtsp\" {\n\t\t\tt.Fatalf(\"unexpected on_play payload: %+v\", m)\n\t\t}\n\t\tif m[\"mediaServerId\"] != \"play-hook-test\" {\n\t\t\tt.Fatalf(\"unexpected mediaServerId: %v\", m[\"mediaServerId\"])\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"on_play not received\")\n\t}\n}\n\n// ---------- on_stream_not_found ----------\n\nfunc TestZlmHookOnStreamNotFound(t *testing.T) {\n\treceived := make(chan map[string]any, 1)\n\tts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tvar m map[string]any\n\t\tjson.NewDecoder(r.Body).Decode(&m)\n\t\treceived <- m\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer ts.Close()\n\n\thub := NewHttpNotify(config.HttpNotifyConfig{\n\t\tEnable:              true,\n\t\tZlmCompatHookConfig: config.ZlmCompatHookConfig{ZlmOnStreamNotFound: ts.URL},\n\t}, \"notfound-hook-test\")\n\n\thub.NotifyStreamNotFound(ZlmOnStreamNotFoundPayload{\n\t\tApp:    \"live\",\n\t\tStream: \"missing_stream\",\n\t\tSchema: \"rtmp\",\n\t\tVhost:  \"__defaultVhost__\",\n\t})\n\n\tselect {\n\tcase m := <-received:\n\t\tif m[\"app\"] != \"live\" || m[\"stream\"] != \"missing_stream\" {\n\t\t\tt.Fatalf(\"unexpected on_stream_not_found payload: %+v\", m)\n\t\t}\n\t\tif m[\"mediaServerId\"] != \"notfound-hook-test\" {\n\t\t\tt.Fatalf(\"unexpected mediaServerId: %v\", m[\"mediaServerId\"])\n\t\t}\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"on_stream_not_found not received\")\n\t}\n}\n\n// ---------- 融合兼容逻辑 ----------\n\nfunc TestZlmHookDispatchByConfig(t *testing.T) {\n\t// 验证：配置了 ZLM hook URL → ZLM 回调触发；\n\t//       未配置 ZLM hook URL → ZLM 回调不触发；\n\t//       lalmax 原有回调始终按 URL 配置分发\n\n\tzlmReceived := make(chan string, 8)\n\ttZlm := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tvar m map[string]any\n\t\tjson.NewDecoder(r.Body).Decode(&m)\n\t\tif _, ok := m[\"regist\"]; ok {\n\t\t\tzlmReceived <- \"on_stream_changed\"\n\t\t} else {\n\t\t\tzlmReceived <- \"on_publish\"\n\t\t}\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer tZlm.Close()\n\n\tlalReceived := make(chan string, 8)\n\ttLal := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tlalReceived <- \"on_pub_start\"\n\t\tw.WriteHeader(http.StatusOK)\n\t}))\n\tdefer tLal.Close()\n\n\t// 同时配置 ZLM + lalmax → 两者都应触发\n\thub := NewHttpNotify(config.HttpNotifyConfig{\n\t\tEnable:              true,\n\t\tOnPubStart:          tLal.URL,\n\t\tZlmCompatHookConfig: config.ZlmCompatHookConfig{ZlmOnStreamChanged: tZlm.URL, ZlmOnPublish: tZlm.URL},\n\t}, \"both-mode\")\n\n\thub.NotifyPubStart(base.PubStartInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tAppName:    \"live\",\n\t\t\tStreamName: \"both_test\",\n\t\t\tProtocol:   \"rtmp\",\n\t\t},\n\t})\n\n\t// ZLM 回调应触发（on_publish + on_stream_changed）\n\tfor i := 0; i < 2; i++ {\n\t\tselect {\n\t\tcase evt := <-zlmReceived:\n\t\t\tt.Logf(\"both mode zlm: %s\", evt)\n\t\tcase <-time.After(time.Second):\n\t\t\tt.Fatal(\"both mode: expected zlm callback\")\n\t\t}\n\t}\n\n\t// lalmax 原有回调也应触发\n\tselect {\n\tcase evt := <-lalReceived:\n\t\tt.Logf(\"both mode lal: %s\", evt)\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"both mode: expected lalmax callback\")\n\t}\n\n\t// 仅配置 lalmax，不配置 ZLM → ZLM 回调不应触发\n\thubLal := NewHttpNotify(config.HttpNotifyConfig{\n\t\tEnable:     true,\n\t\tOnPubStart: tLal.URL,\n\t}, \"lal-only\")\n\n\thubLal.NotifyPubStart(base.PubStartInfo{\n\t\tSessionEventCommonInfo: base.SessionEventCommonInfo{\n\t\t\tAppName:    \"live\",\n\t\t\tStreamName: \"lal_only_test\",\n\t\t\tProtocol:   \"rtmp\",\n\t\t},\n\t})\n\n\tselect {\n\tcase evt := <-lalReceived:\n\t\tt.Logf(\"lal-only mode: %s\", evt)\n\tcase <-time.After(time.Second):\n\t\tt.Fatal(\"lal-only mode: expected lalmax callback\")\n\t}\n\n\t// ZLM 回调不应触发\n\tselect {\n\tcase <-zlmReceived:\n\t\tt.Fatal(\"lal-only mode: should NOT receive zlm callback\")\n\tcase <-time.After(200 * time.Millisecond):\n\t}\n}\n"
  },
  {
    "path": "server/zlm_compat_types.go",
    "content": "package server\n\n// ZLM 兼容层请求/响应类型定义\n// 为什么放在 server 包：ZLM 兼容路由与现有 lalmax 路由同级，需访问 LalMaxServer 内部成员\n\n// ZlmFixedHeader ZLM 标准响应头\ntype ZlmFixedHeader struct {\n\tCode int    `json:\"code\"`\n\tMsg  string `json:\"msg,omitempty\"`\n}\n\n// --- /index/api/openRtpServer ---\n\ntype ZlmOpenRtpServerReq struct {\n\tPort     int    `json:\"port\"`\n\tTCPMode  int8   `json:\"tcp_mode\"`\n\tStreamID string `json:\"stream_id\"`\n}\n\ntype ZlmOpenRtpServerResp struct {\n\tCode int    `json:\"code\"`\n\tMsg  string `json:\"msg,omitempty\"`\n\tPort int    `json:\"port\"`\n}\n\n// --- /index/api/closeRtpServer ---\n\ntype ZlmCloseRtpServerReq struct {\n\tStreamID string `json:\"stream_id\"`\n}\n\ntype ZlmCloseRtpServerResp struct {\n\tCode int `json:\"code\"`\n\tHit  int `json:\"hit\"`\n}\n\n// --- /index/api/close_streams ---\n\ntype ZlmCloseStreamsReq struct {\n\tSchema string `json:\"schema,omitempty\"`\n\tVhost  string `json:\"vhost,omitempty\"`\n\tApp    string `json:\"app,omitempty\"`\n\tStream string `json:\"stream,omitempty\"`\n\tForce  bool   `json:\"force,omitempty\"`\n}\n\ntype ZlmCloseStreamsResp struct {\n\tCode        int `json:\"code\"`\n\tCountHit    int `json:\"count_hit\"`\n\tCountClosed int `json:\"count_closed\"`\n}\n\n// --- /index/api/getServerConfig ---\n\ntype ZlmGetServerConfigResp struct {\n\tCode int              `json:\"code\"`\n\tData []map[string]any `json:\"data\"`\n}\n\n// --- /index/api/setServerConfig ---\n\ntype ZlmSetServerConfigResp struct {\n\tZlmFixedHeader\n\tChanged int `json:\"changed\"`\n}\n\n// --- /index/api/startRecord ---\n\ntype ZlmStartRecordReq struct {\n\tType       int    `json:\"type\"`\n\tVhost      string `json:\"vhost\"`\n\tApp        string `json:\"app\"`\n\tStream     string `json:\"stream\"`\n\tCustomPath string `json:\"customized_path,omitempty\"`\n\tMaxSecond  int    `json:\"max_second,omitempty\"`\n}\n\ntype ZlmStartRecordResp struct {\n\tZlmFixedHeader\n\tResult bool `json:\"result\"`\n}\n\n// --- /index/api/stopRecord ---\n\ntype ZlmStopRecordReq struct {\n\tType   int    `json:\"type\"`\n\tVhost  string `json:\"vhost\"`\n\tApp    string `json:\"app\"`\n\tStream string `json:\"stream\"`\n}\n\ntype ZlmStopRecordResp struct {\n\tZlmFixedHeader\n\tResult bool `json:\"result\"`\n}\n\n// --- /index/api/addStreamProxy ---\n\ntype ZlmAddStreamProxyReq struct {\n\tVhost      string  `json:\"vhost\"`\n\tApp        string  `json:\"app\"`\n\tStream     string  `json:\"stream\"`\n\tURL        string  `json:\"url\"`\n\tRetryCount int     `json:\"retry_count\"`\n\tRTPType    int     `json:\"rtp_type\"`\n\tTimeoutSec float32 `json:\"timeout_sec\"`\n}\n\ntype ZlmAddStreamProxyResp struct {\n\tZlmFixedHeader\n\tData struct {\n\t\tKey string `json:\"key\"`\n\t} `json:\"data\"`\n}\n\n// --- /index/api/getSnap ---\n\ntype ZlmGetSnapReq struct {\n\tURL        string `json:\"url\"`\n\tTimeoutSec int    `json:\"timeout_sec\"`\n\tExpireSec  int    `json:\"expire_sec\"`\n}\n\n// --- on_stream_changed Hook Payload ---\n\ntype ZlmOnStreamChangedPayload struct {\n\tRegist           bool              `json:\"regist\"`\n\tAliveSecond      int               `json:\"aliveSecond\"`\n\tApp              string            `json:\"app\"`\n\tBytesSpeed       int               `json:\"bytesSpeed\"`\n\tCreateStamp      int64             `json:\"createStamp\"`\n\tMediaServerID    string            `json:\"mediaServerId\"`\n\tOriginSock       ZlmOriginSock     `json:\"originSock\"`\n\tOriginType       int               `json:\"originType\"`\n\tOriginTypeStr    string            `json:\"originTypeStr\"`\n\tOriginURL        string            `json:\"originUrl\"`\n\tReaderCount      int               `json:\"readerCount\"`\n\tSchema           string            `json:\"schema\"`\n\tStream           string            `json:\"stream\"`\n\tTotalReaderCount int               `json:\"totalReaderCount\"`\n\tTracks           []ZlmTrack        `json:\"tracks\"`\n\tVhost            string            `json:\"vhost\"`\n\tAppName          string            `json:\"app_name,omitempty\"`\n\tStreamName       string            `json:\"stream_name,omitempty\"`\n}\n\ntype ZlmOriginSock struct {\n\tIdentifier string `json:\"identifier\"`\n\tLocalIP    string `json:\"local_ip\"`\n\tLocalPort  int    `json:\"local_port\"`\n\tPeerIP     string `json:\"peer_ip\"`\n\tPeerPort   int    `json:\"peer_port\"`\n}\n\ntype ZlmTrack struct {\n\tChannels    int     `json:\"channels,omitempty\"`\n\tCodecID     int     `json:\"codec_id\"`\n\tCodecIDName string  `json:\"codec_id_name\"`\n\tCodecType   int     `json:\"codec_type\"`\n\tReady       bool    `json:\"ready\"`\n\tSampleBit   int     `json:\"sample_bit,omitempty\"`\n\tSampleRate  int     `json:\"sample_rate,omitempty\"`\n\tFps         float32 `json:\"fps,omitempty\"`\n\tHeight      int     `json:\"height,omitempty\"`\n\tWidth       int     `json:\"width,omitempty\"`\n}\n\n// --- on_server_keepalive Hook Payload ---\n\ntype ZlmOnServerKeepalivePayload struct {\n\tMediaServerID string `json:\"mediaServerId\"`\n}\n\n// --- on_stream_none_reader Hook Payload ---\n\ntype ZlmOnStreamNoneReaderPayload struct {\n\tMediaServerID string `json:\"mediaServerId\"`\n\tApp           string `json:\"app\"`\n\tSchema        string `json:\"schema\"`\n\tStream        string `json:\"stream\"`\n\tVhost         string `json:\"vhost\"`\n}\n\n// --- on_record_mp4 Hook Payload ---\n\ntype ZlmOnRecordMp4Payload struct {\n\tMediaServerID string  `json:\"mediaServerId\"`\n\tApp           string  `json:\"app\"`\n\tFileName      string  `json:\"file_name\"`\n\tFilePath      string  `json:\"file_path\"`\n\tFileSize      int64   `json:\"file_size\"`\n\tFolder        string  `json:\"folder\"`\n\tStartTime     int64   `json:\"start_time\"`\n\tStream        string  `json:\"stream\"`\n\tTimeLen       float64 `json:\"time_len\"`\n\tURL           string  `json:\"url\"`\n\tVhost         string  `json:\"vhost\"`\n}\n\n// --- on_publish Hook Payload ---\n\ntype ZlmOnPublishPayload struct {\n\tMediaServerID string `json:\"mediaServerId\"`\n\tApp           string `json:\"app\"`\n\tID            string `json:\"id\"`\n\tIP            string `json:\"ip\"`\n\tParams        string `json:\"params\"`\n\tPort          int    `json:\"port\"`\n\tSchema        string `json:\"schema\"`\n\tStream        string `json:\"stream\"`\n\tVhost         string `json:\"vhost\"`\n}\n\n// --- on_play Hook Payload ---\n\ntype ZlmOnPlayPayload struct {\n\tMediaServerID string `json:\"mediaServerId\"`\n\tApp           string `json:\"app\"`\n\tID            string `json:\"id\"`\n\tIP            string `json:\"ip\"`\n\tParams        string `json:\"params\"`\n\tPort          int    `json:\"port\"`\n\tSchema        string `json:\"schema\"`\n\tStream        string `json:\"stream\"`\n\tVhost         string `json:\"vhost\"`\n}\n\n// --- on_stream_not_found Hook Payload ---\n\ntype ZlmOnStreamNotFoundPayload struct {\n\tMediaServerID string `json:\"mediaServerId\"`\n\tApp           string `json:\"app\"`\n\tID            string `json:\"id\"`\n\tIP            string `json:\"ip\"`\n\tParams        string `json:\"params\"`\n\tPort          int    `json:\"port\"`\n\tSchema        string `json:\"schema\"`\n\tStream        string `json:\"stream\"`\n\tVhost         string `json:\"vhost\"`\n\tAppName       string `json:\"app_name,omitempty\"`\n\tStreamName    string `json:\"stream_name,omitempty\"`\n}\n\n// --- on_rtp_server_timeout Hook Payload ---\n\ntype ZlmOnRtpServerTimeoutPayload struct {\n\tLocalPort     int    `json:\"local_port\"`\n\tReUsePort     bool   `json:\"re_use_port\"`\n\tSSRC          uint32 `json:\"ssrc\"`\n\tStreamID      string `json:\"stream_id\"`\n\tTCPMode       int    `json:\"tcp_mode\"`\n\tMediaServerID string `json:\"mediaServerId\"`\n}\n"
  },
  {
    "path": "srt/pub.go",
    "content": "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.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/lal/pkg/logic\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n\tcodec \"github.com/yapingcat/gomedia/go-codec\"\n\tts \"github.com/yapingcat/gomedia/go-mpeg2\"\n)\n\ntype Publisher struct {\n\tctx         context.Context\n\tsrv         *SrtServer\n\tss          logic.ICustomizePubSessionContext\n\tstreamName  string\n\tdemuxer     *ts.TSDemuxer\n\tconn        srt.Conn\n\tsubscribers []*Subscriber\n}\n\nfunc NewPublisher(ctx context.Context, conn srt.Conn, streamName string, srv *SrtServer) *Publisher {\n\tpub := &Publisher{\n\t\tctx:        ctx,\n\t\tsrv:        srv,\n\t\tstreamName: streamName,\n\t\tconn:       conn,\n\t\tdemuxer:    ts.NewTSDemuxer(),\n\t}\n\n\tnazalog.Infof(\"create srt publisher, streamName:%s\", streamName)\n\treturn pub\n}\n\nfunc (p *Publisher) SetSession(session logic.ICustomizePubSessionContext) {\n\tp.ss = session\n}\n\nfunc (p *Publisher) Run() {\n\tdefer func() {\n\t\tp.conn.Close()\n\t\tp.srv.Remove(p.streamName, p.ss)\n\t}()\n\taudioSampleRate := uint32(0)\n\tvar foundAudio bool\n\tp.demuxer.OnFrame = func(cid ts.TS_STREAM_TYPE, frame []byte, pts uint64, dts uint64) {\n\t\tvar pkt base.AvPacket\n\t\tif cid == ts.TS_STREAM_AAC {\n\t\t\tif !foundAudio {\n\t\t\t\tif asc, err := codec.ConvertADTSToASC(frame); err != nil {\n\t\t\t\t\treturn\n\t\t\t\t} else {\n\t\t\t\t\tp.ss.FeedAudioSpecificConfig(asc.Encode())\n\t\t\t\t\taudioSampleRate = uint32(codec.AACSampleIdxToSample(int(asc.Sample_freq_index)))\n\t\t\t\t}\n\n\t\t\t\tfoundAudio = true\n\t\t\t}\n\n\t\t\tvar preAudioDts uint64\n\t\t\tctx := aac.AdtsHeaderContext{}\n\t\t\tfor len(frame) > aac.AdtsHeaderLength {\n\t\t\t\tctx.Unpack(frame[:])\n\t\t\t\tif preAudioDts == 0 {\n\t\t\t\t\tpreAudioDts = dts\n\t\t\t\t} else {\n\t\t\t\t\tpreAudioDts += uint64(1024 * 1000 / audioSampleRate)\n\t\t\t\t}\n\n\t\t\t\taacPacket := base.AvPacket{\n\t\t\t\t\tTimestamp:   int64(preAudioDts),\n\t\t\t\t\tPayloadType: base.AvPacketPtAac,\n\t\t\t\t\tPts:         int64(preAudioDts),\n\t\t\t\t}\n\t\t\t\tif len(frame) >= int(ctx.AdtsLength) {\n\t\t\t\t\tPayload := frame[aac.AdtsHeaderLength:ctx.AdtsLength]\n\t\t\t\t\tif len(frame) > int(ctx.AdtsLength) {\n\t\t\t\t\t\tframe = frame[ctx.AdtsLength:]\n\t\t\t\t\t} else {\n\t\t\t\t\t\tframe = frame[0:0]\n\t\t\t\t\t}\n\t\t\t\t\taacPacket.Payload = Payload\n\t\t\t\t\tp.ss.FeedAvPacket(aacPacket)\n\t\t\t\t}\n\n\t\t\t}\n\t\t} else if cid == ts.TS_STREAM_H264 {\n\t\t\tpkt.Payload = frame\n\t\t\tpkt.PayloadType = base.AvPacketPtAvc\n\t\t\tpkt.Pts = int64(pts)\n\t\t\tpkt.Timestamp = int64(dts)\n\t\t\tp.ss.FeedAvPacket(pkt)\n\t\t} else if cid == ts.TS_STREAM_H265 {\n\t\t\tpkt.Payload = frame\n\t\t\tpkt.PayloadType = base.AvPacketPtHevc\n\t\t\tpkt.Pts = int64(pts)\n\t\t\tpkt.Timestamp = int64(dts)\n\t\t\tp.ss.FeedAvPacket(pkt)\n\t\t}\n\t}\n\terr := p.demuxer.Input(bufio.NewReader(p.conn))\n\tif err != nil {\n\t\tnazalog.Infof(\"stream [%s] disconnected\", p.streamName)\n\t}\n\treturn\n}\n"
  },
  {
    "path": "srt/server.go",
    "content": "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/base\"\n\t\"github.com/q191201771/lal/pkg/logic\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n)\n\ntype SrtServer struct {\n\taddr      string\n\tlalServer logic.ILalServer\n\tsrtOpt    SrtOption\n}\ntype SrtOption struct {\n\tLatency           int\n\tRecvLatency       int\n\tPeerLatency       int\n\tTlpktDrop         bool\n\tTsbpdMode         bool\n\tRecvBuf           int\n\tSendBuf           int\n\tMaxSendPacketSize int\n}\n\nvar defaultSrtOption = SrtOption{\n\tLatency:           300,\n\tRecvLatency:       300,\n\tPeerLatency:       300,\n\tTlpktDrop:         true,\n\tTsbpdMode:         true,\n\tRecvBuf:           2 * 1024 * 1024,\n\tSendBuf:           2 * 1024 * 1024,\n\tMaxSendPacketSize: 4,\n}\n\ntype ModSrtOption func(option *SrtOption)\n\nfunc NewSrtServer(addr string, lal logic.ILalServer, modOptions ...ModSrtOption) *SrtServer {\n\topt := defaultSrtOption\n\tfor _, fn := range modOptions {\n\t\tfn(&opt)\n\t}\n\tsvr := &SrtServer{\n\t\taddr:      addr,\n\t\tlalServer: lal,\n\t\tsrtOpt:    opt,\n\t}\n\n\tnazalog.Info(\"create srt server\")\n\treturn svr\n}\n\nfunc (s *SrtServer) Run(ctx context.Context) {\n\tconf := srt.DefaultConfig()\n\tconf.Latency = time.Millisecond * time.Duration(s.srtOpt.Latency)\n\tconf.ReceiverLatency = time.Millisecond * time.Duration(s.srtOpt.RecvLatency)\n\tconf.PeerLatency = time.Millisecond * time.Duration(s.srtOpt.PeerLatency)\n\tconf.TooLatePacketDrop = s.srtOpt.TlpktDrop\n\tconf.TSBPDMode = s.srtOpt.TsbpdMode\n\tconf.SendBufferSize = uint32(s.srtOpt.SendBuf)\n\tconf.ReceiverBufferSize = uint32(s.srtOpt.RecvBuf)\n\n\tsrtlistener, err := srt.Listen(\"srt\", s.addr, conf)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tdefer srtlistener.Close()\n\n\tnazalog.Info(\"srt server listen addr:\", s.addr)\n\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tdefault:\n\n\t\t}\n\n\t\tvar info StreamInfo\n\t\tconn, mode, err := srtlistener.Accept(func(req srt.ConnRequest) srt.ConnType {\n\t\t\tinfo = getStreamInfo(req.StreamId())\n\t\t\treturn info.Mode\n\t\t})\n\n\t\tif err != nil {\n\t\t\t// rejected connection, ignore\n\t\t\tcontinue\n\t\t}\n\n\t\tif mode == srt.REJECT {\n\t\t\t// rejected connection, ignore\n\t\t\tcontinue\n\t\t}\n\n\t\tif info.Mode == srt.PUBLISH {\n\t\t\tgo s.handlePublish(ctx, conn, info.StreamName)\n\t\t} else {\n\t\t\tgo s.handleSubcribe(ctx, conn, info.StreamName)\n\t\t}\n\t}\n}\n\nfunc (s *SrtServer) handlePublish(ctx context.Context, conn srt.Conn, streamid string) {\n\tpublisher := NewPublisher(ctx, conn, streamid, s)\n\tsession, err := s.lalServer.AddCustomizePubSession(streamid)\n\tif err != nil {\n\t\tnazalog.Error(err)\n\t}\n\n\tif session != nil {\n\t\tsession.WithOption(func(option *base.AvPacketStreamOption) {\n\t\t\toption.VideoFormat = base.AvPacketStreamVideoFormatAnnexb\n\t\t})\n\t}\n\n\tpublisher.SetSession(session)\n\tpublisher.Run()\n}\n\nfunc (s *SrtServer) handleSubcribe(ctx context.Context, conn srt.Conn, streamid string) {\n\tsubscriber := NewSubscriber(ctx, conn, streamid, s.srtOpt.MaxSendPacketSize)\n\tsubscriber.Run()\n}\n\nfunc (s *SrtServer) Remove(host string, ss logic.ICustomizePubSessionContext) {\n\ts.lalServer.DelCustomizePubSession(ss)\n}\n\ntype StreamInfo struct {\n\tStreamName string\n\tMode    srt.ConnType\n}\n\nfunc getStreamInfo(streamid string) StreamInfo {\n\tinfo := StreamInfo{\n\t\tMode: srt.REJECT,\n\t}\n\n\ts := strings.TrimLeft(streamid, \"#!::\")\n\tvalues := strings.Split(s, \",\")\n\tfor _, v := range values {\n\t\tss := strings.Split(v, \"=\")\n\t\tname := ss[0]\n\t\tswitch name {\n\t\tcase \"h\":\n\t\t\tinfo.StreamName = ss[1]\n\t\tcase \"m\":\n\t\t\tswitch ss[1] {\n\t\t\tcase \"publish\":\n\t\t\t\tinfo.Mode = srt.PUBLISH\n\t\t\tcase \"request\":\n\t\t\t\tinfo.Mode = srt.SUBSCRIBE\n\t\t\t}\n\t\t}\n\t}\n\n\treturn info\n}\n"
  },
  {
    "path": "srt/stream_id.go",
    "content": "package srt\n\nimport (\n\t\"errors\"\n\t\"strings\"\n)\n\ntype StreamID struct {\n\tUser      string\n\tHost      string\n\tResource  string\n\tSessionID string\n\tType      string\n\tMode      string\n}\n\nfunc parseStreamID(streamID string) (*StreamID, error) {\n\tif !strings.Contains(streamID, \"#!::\") {\n\t\treturn nil, errors.New(\"invalid streamid\")\n\t}\n\tsplit := strings.Split(strings.TrimPrefix(streamID, \"#!::\"), \",\")\n\tid := &StreamID{}\n\n\tfor _, s := range split {\n\t\tif strings.Contains(s, \"=\") {\n\t\t\tkv := strings.Split(s, \"=\")\n\t\t\tif len(kv) != 2 {\n\t\t\t\treturn nil, errors.New(\"invalid streamid\")\n\t\t\t}\n\n\t\t\tif kv[0] == \"u\" {\n\t\t\t\tid.User = kv[1]\n\t\t\t}\n\t\t\tif kv[0] == \"h\" {\n\t\t\t\tid.Host = kv[1]\n\t\t\t}\n\t\t\tif kv[0] == \"r\" {\n\t\t\t\tid.Resource = kv[1]\n\t\t\t}\n\t\t\tif kv[0] == \"s\" {\n\t\t\t\tid.SessionID = kv[1]\n\t\t\t}\n\t\t\tif kv[0] == \"t\" {\n\t\t\t\tid.Type = kv[1]\n\t\t\t}\n\t\t\tif kv[0] == \"m\" {\n\t\t\t\tid.Mode = kv[1]\n\t\t\t}\n\t\t}\n\t}\n\treturn id, nil\n}\n"
  },
  {
    "path": "srt/sub.go",
    "content": "package srt\n\nimport (\n\t\"context\"\n\n\tmaxlogic \"github.com/q191201771/lalmax/logic\"\n\n\tsrt \"github.com/datarhei/gosrt\"\n\t\"github.com/gofrs/uuid\"\n\t\"github.com/q191201771/lal/pkg/base\"\n\t\"github.com/q191201771/naza/pkg/nazalog\"\n\tcodec \"github.com/yapingcat/gomedia/go-codec\"\n\tflv \"github.com/yapingcat/gomedia/go-flv\"\n\tts \"github.com/yapingcat/gomedia/go-mpeg2\"\n)\n\ntype Subscriber struct {\n\tctx               context.Context\n\tconn              srt.Conn\n\tstreamName        string\n\tmuxer             *ts.TSMuxer\n\thasInit           bool\n\tvideoPid          uint16\n\taudioPid          uint16\n\tflvVideoDemuxer   flv.VideoTagDemuxer\n\tflvAudioDemuxer   flv.AudioTagDemuxer\n\tvideodts          uint32\n\taudiodts          uint32\n\tsubscriberId      string\n\tmaxSendPacketSize int\n}\n\nfunc NewSubscriber(ctx context.Context, conn srt.Conn, streamName string, maxSendPacketSize int) *Subscriber {\n\tu, _ := uuid.NewV4()\n\tsub := &Subscriber{\n\t\tctx:               ctx,\n\t\tconn:              conn,\n\t\tstreamName:        streamName,\n\t\tmuxer:             ts.NewTSMuxer(),\n\t\tsubscriberId:      u.String(),\n\t\tmaxSendPacketSize: maxSendPacketSize,\n\t}\n\n\tnazalog.Infof(\"create srt subscriber, streamName:%s, subscriberId:%s\", streamName, sub.subscriberId)\n\n\treturn sub\n}\n\nfunc (s *Subscriber) Run() {\n\tok, group := maxlogic.GetGroupManagerInstance().GetGroupByStreamName(s.streamName)\n\tif ok {\n\t\tvar err error\n\t\tsendBuf := make([]byte, 0, s.maxSendPacketSize*ts.TS_PAKCET_SIZE)\n\t\ts.muxer.OnPacket = func(tsPacket []byte) {\n\t\t\tdefer func() {\n\t\t\t\tif err != nil {\n\t\t\t\t\tnazalog.Info(\"close srt socket\")\n\t\t\t\t\ts.conn.Close()\n\t\t\t\t}\n\n\t\t\t}()\n\n\t\t\tselect {\n\t\t\tcase <-s.ctx.Done():\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t}\n\t\t\tif len(sendBuf) > (s.maxSendPacketSize-1)*ts.TS_PAKCET_SIZE {\n\t\t\t\tif _, err = s.conn.Write(sendBuf); err != nil {\n\t\t\t\t\tgroup.RemoveSubscriber(s.subscriberId)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tsendBuf = sendBuf[0:0]\n\t\t\t}\n\t\t\tsendBuf = append(sendBuf, tsPacket...)\n\n\t\t}\n\t\tgroup.AddSubscriber(maxlogic.SubscriberInfo{\n\t\t\tSubscriberID: s.subscriberId,\n\t\t\tProtocol:     maxlogic.SubscriberProtocolSRT,\n\t\t}, s)\n\t} else {\n\t\tnazalog.Warnf(\"not found stream group, streamName:%s\", s.streamName)\n\t\ts.conn.Close()\n\t}\n}\n\nfunc (s *Subscriber) OnMsg(msg base.RtmpMsg) {\n\tvar err error\n\tif !s.hasInit {\n\t\tok, group := maxlogic.GetGroupManagerInstance().GetGroupByStreamName(s.streamName)\n\t\tif ok {\n\t\t\tvideoheader := group.GetVideoSeqHeaderMsg()\n\t\t\tif videoheader != nil {\n\t\t\t\tif videoheader.IsAvcKeySeqHeader() {\n\t\t\t\t\ts.videoPid = s.muxer.AddStream(ts.TS_STREAM_H264)\n\t\t\t\t\ts.flvVideoDemuxer = flv.CreateFlvVideoTagHandle(flv.FLV_AVC)\n\t\t\t\t} else {\n\t\t\t\t\ts.videoPid = s.muxer.AddStream(ts.TS_STREAM_H265)\n\t\t\t\t\ts.flvVideoDemuxer = flv.CreateFlvVideoTagHandle(flv.FLV_HEVC)\n\t\t\t\t}\n\n\t\t\t\ts.flvVideoDemuxer.OnFrame(func(codecid codec.CodecID, b []byte, cts int) {\n\t\t\t\t\ts.muxer.Write(s.videoPid, b, uint64(s.videodts)+uint64(cts), uint64(s.videodts))\n\t\t\t\t})\n\n\t\t\t\tif err = s.flvVideoDemuxer.Decode(videoheader.Payload); err != nil {\n\t\t\t\t\tnazalog.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\taudioheader := group.GetAudioSeqHeaderMsg()\n\t\t\tif audioheader != nil {\n\t\t\t\tif audioheader.IsAacSeqHeader() {\n\t\t\t\t\ts.audioPid = s.muxer.AddStream(ts.TS_STREAM_AAC)\n\t\t\t\t} else {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\ts.flvAudioDemuxer = flv.CreateAudioTagDemuxer(flv.FLV_AAC)\n\t\t\t\ts.flvAudioDemuxer.OnFrame(func(codecid codec.CodecID, b []byte) {\n\t\t\t\t\ts.muxer.Write(s.audioPid, b, uint64(s.audiodts), uint64(s.audiodts))\n\t\t\t\t})\n\n\t\t\t\tif err = s.flvAudioDemuxer.Decode(audioheader.Payload); err != nil {\n\t\t\t\t\tnazalog.Error(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\ts.hasInit = true\n\t}\n\n\tif msg.Header.MsgTypeId == base.RtmpTypeIdVideo {\n\t\ts.videodts = msg.Dts()\n\t\tif s.flvVideoDemuxer != nil {\n\t\t\tif err = s.flvVideoDemuxer.Decode(msg.Payload); err != nil {\n\t\t\t\tnazalog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t} else {\n\t\ts.audiodts = msg.Dts()\n\t\tif s.flvAudioDemuxer != nil {\n\t\t\tif err = s.flvAudioDemuxer.Decode(msg.Payload); err != nil {\n\t\t\t\tnazalog.Error(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (s *Subscriber) OnStop() {\n\tnazalog.Info(\"srt subscriber onStop\")\n\ts.conn.Close()\n}\n\nfunc (s *Subscriber) GetSubscriberStat() maxlogic.SubscriberStat {\n\tif s == nil || s.conn == nil {\n\t\treturn maxlogic.SubscriberStat{}\n\t}\n\n\tvar stats srt.Statistics\n\ts.conn.Stats(&stats)\n\n\tstat := maxlogic.SubscriberStat{\n\t\tReadBytesSum:  stats.Accumulated.ByteRecv,\n\t\tWroteBytesSum: stats.Accumulated.ByteSent,\n\t}\n\tif remoteAddr := s.conn.RemoteAddr(); remoteAddr != nil {\n\t\tstat.RemoteAddr = remoteAddr.String()\n\t}\n\treturn stat\n}\n"
  },
  {
    "path": "utils/adjustdts.go",
    "content": "package utils\n\nimport \"time\"\n\ntype DtsDecoder struct {\n\tstartDts  time.Duration\n\tclockRate time.Duration\n\toverall   time.Duration\n\tprev      uint32\n}\n\nfunc NewDtsDecoder(startDts, clockRate time.Duration, prevDts uint32) *DtsDecoder {\n\treturn &DtsDecoder{\n\t\tstartDts:  startDts,\n\t\tclockRate: clockRate,\n\t\tprev:      prevDts,\n\t}\n}\n\nfunc multiplyAndDivide(v, m, d time.Duration) time.Duration {\n\tsecs := v / d\n\tdec := v % d\n\treturn (secs*m + dec*m/d)\n}\n\nfunc (d *DtsDecoder) Decode(ts uint32) time.Duration {\n\t// 这样可以解决翻转问题\n\tdiff := int32(ts - d.prev)\n\tif diff >= 1000 || diff <= -1000 {\n\t\t// 以视频为主，音频计算以后看\n\t\tdiff = 40\n\t}\n\td.prev = ts\n\td.overall += time.Duration(diff * int32(d.clockRate/1000))\n\n\treturn d.startDts + multiplyAndDivide(d.overall, time.Second, d.clockRate)\n}\n"
  },
  {
    "path": "version/README.md",
    "content": "这个目录用于存放lalmax版本信息说明\n\n版本格式\n\nv0.x1.x2\n\n说明如下\n\nx1为大版本,例如一个大的功能发布或者常规迭代\n\nx2为小版本,例如小问题修复\n"
  },
  {
    "path": "version/v0.1.0.md",
    "content": "lalmax v0.1.0版本说明\n\n# 功能点\n\n(1) 支持SRT推拉流（暂不支持加密）\n\n[SRT相关说明](../document/srt.md)\n\nsrt支持以后可以使用srt推流到lalmax，然后使用rtsp/hls/rtmp/http-flv/srt等协议进行拉流，也可以使用rtmp/rtsp推流到lalmax中，使用srt进行拉流\n\n## SRT url格式\n\n推流url\nsrt://127.0.0.1:6001?streamid=#!::r=test110,m=publish\n\n拉流url\nsrt://127.0.0.1:6001?streamid=#!::r=test110,m=request\n\n"
  },
  {
    "path": "version/v0.2.0.md",
    "content": "lalmax v0.2.0版本说明\n\n[RTC相关说明](../document/rtc.md)\n\n# 功能点\n（1）支持WHIP推流和WHEP拉流，可以对接[OBS](https://github.com/obsproject/obs-studio/actions/runs/5227109208?pr=7926)、[vue-wish](https://github.com/zllovesuki/vue-wish)\n\n视频:h264\n\n音频:G711A/G711U\n\n# RTC url格式\n推流url\nhttp(s)://127.0.0.1:1290/whip?streamid=test110\n\n拉流url\nhttp(s)://127.0.0.1:1290/whep?streamid=test110\n"
  }
]