jaryhe 6 mēneši atpakaļ
revīzija
67001cd674
100 mainītis faili ar 18442 papildinājumiem un 0 dzēšanām
  1. 1 0
      m7s.live/engine/v4@v4.14.6/.gitignore
  2. 85 0
      m7s.live/engine/v4@v4.14.6/.golangci.yml
  3. 21 0
      m7s.live/engine/v4@v4.14.6/LICENSE
  4. 207 0
      m7s.live/engine/v4@v4.14.6/README.md
  5. 82 0
      m7s.live/engine/v4@v4.14.6/codec/av1.go
  6. 231 0
      m7s.live/engine/v4@v4.14.6/codec/codec.go
  7. 125 0
      m7s.live/engine/v4@v4.14.6/codec/flv.go
  8. 325 0
      m7s.live/engine/v4@v4.14.6/codec/h264.go
  9. 541 0
      m7s.live/engine/v4@v4.14.6/codec/h265.go
  10. 2417 0
      m7s.live/engine/v4@v4.14.6/codec/mp4.go
  11. 578 0
      m7s.live/engine/v4@v4.14.6/codec/mpegts/mpegts.go
  12. 520 0
      m7s.live/engine/v4@v4.14.6/codec/mpegts/mpegts.md
  13. 72 0
      m7s.live/engine/v4@v4.14.6/codec/mpegts/mpegts_crc32.go
  14. 230 0
      m7s.live/engine/v4@v4.14.6/codec/mpegts/mpegts_pat.go
  15. 455 0
      m7s.live/engine/v4@v4.14.6/codec/mpegts/mpegts_pes.go
  16. 368 0
      m7s.live/engine/v4@v4.14.6/codec/mpegts/mpegts_pmt.go
  17. 231 0
      m7s.live/engine/v4@v4.14.6/codec/mpegts/mpegts_psi.go
  18. 227 0
      m7s.live/engine/v4@v4.14.6/codec/sps.go
  19. 70 0
      m7s.live/engine/v4@v4.14.6/common/dtsestimator.go
  20. 17 0
      m7s.live/engine/v4@v4.14.6/common/dtsestimator_test.go
  21. 206 0
      m7s.live/engine/v4@v4.14.6/common/frame.go
  22. 139 0
      m7s.live/engine/v4@v4.14.6/common/index.go
  23. 118 0
      m7s.live/engine/v4@v4.14.6/common/ring-writer.go
  24. 23 0
      m7s.live/engine/v4@v4.14.6/common/stream.go
  25. 352 0
      m7s.live/engine/v4@v4.14.6/config/config.go
  26. 32 0
      m7s.live/engine/v4@v4.14.6/config/config_test.go
  27. 244 0
      m7s.live/engine/v4@v4.14.6/config/formily.go
  28. 138 0
      m7s.live/engine/v4@v4.14.6/config/http.go
  29. 27 0
      m7s.live/engine/v4@v4.14.6/config/local.monibuca.com.key
  30. 66 0
      m7s.live/engine/v4@v4.14.6/config/local.monibuca.com_bundle.pem
  31. 68 0
      m7s.live/engine/v4@v4.14.6/config/quic.go
  32. 49 0
      m7s.live/engine/v4@v4.14.6/config/regexp.go
  33. 165 0
      m7s.live/engine/v4@v4.14.6/config/remote.go
  34. 118 0
      m7s.live/engine/v4@v4.14.6/config/tcp.go
  35. 331 0
      m7s.live/engine/v4@v4.14.6/config/types.go
  36. BIN
      m7s.live/engine/v4@v4.14.6/demo.gif
  37. 121 0
      m7s.live/engine/v4@v4.14.6/events.go
  38. 47 0
      m7s.live/engine/v4@v4.14.6/go.mod
  39. 294 0
      m7s.live/engine/v4@v4.14.6/go.sum
  40. 379 0
      m7s.live/engine/v4@v4.14.6/http.go
  41. 292 0
      m7s.live/engine/v4@v4.14.6/io.go
  42. 66 0
      m7s.live/engine/v4@v4.14.6/lang/lang.go
  43. 68 0
      m7s.live/engine/v4@v4.14.6/lang/zh.yaml
  44. 129 0
      m7s.live/engine/v4@v4.14.6/log/log.go
  45. 46 0
      m7s.live/engine/v4@v4.14.6/log/sugar.go
  46. 36 0
      m7s.live/engine/v4@v4.14.6/log/writer.go
  47. 256 0
      m7s.live/engine/v4@v4.14.6/main.go
  48. 192 0
      m7s.live/engine/v4@v4.14.6/memory-ts.go
  49. 342 0
      m7s.live/engine/v4@v4.14.6/plugin.go
  50. 62 0
      m7s.live/engine/v4@v4.14.6/publisher-mp4.go
  51. 108 0
      m7s.live/engine/v4@v4.14.6/publisher-rtpdump.go
  52. 111 0
      m7s.live/engine/v4@v4.14.6/publisher-ts.go
  53. 136 0
      m7s.live/engine/v4@v4.14.6/publisher.go
  54. 109 0
      m7s.live/engine/v4@v4.14.6/puller.go
  55. 79 0
      m7s.live/engine/v4@v4.14.6/pusher.go
  56. 720 0
      m7s.live/engine/v4@v4.14.6/stream.go
  57. 422 0
      m7s.live/engine/v4@v4.14.6/subscriber.go
  58. 170 0
      m7s.live/engine/v4@v4.14.6/subscribers.go
  59. 112 0
      m7s.live/engine/v4@v4.14.6/summary.go
  60. 265 0
      m7s.live/engine/v4@v4.14.6/track/aac.go
  61. 98 0
      m7s.live/engine/v4@v4.14.6/track/audio.go
  62. 172 0
      m7s.live/engine/v4@v4.14.6/track/av1.go
  63. 318 0
      m7s.live/engine/v4@v4.14.6/track/base.go
  64. 113 0
      m7s.live/engine/v4@v4.14.6/track/data.go
  65. 82 0
      m7s.live/engine/v4@v4.14.6/track/g711.go
  66. 229 0
      m7s.live/engine/v4@v4.14.6/track/h264.go
  67. 225 0
      m7s.live/engine/v4@v4.14.6/track/h265.go
  68. 55 0
      m7s.live/engine/v4@v4.14.6/track/opus.go
  69. 144 0
      m7s.live/engine/v4@v4.14.6/track/reader-av.go
  70. 71 0
      m7s.live/engine/v4@v4.14.6/track/reader-data.go
  71. 104 0
      m7s.live/engine/v4@v4.14.6/track/rtp.go
  72. 306 0
      m7s.live/engine/v4@v4.14.6/track/video.go
  73. 283 0
      m7s.live/engine/v4@v4.14.6/util/amf.go
  74. 28 0
      m7s.live/engine/v4@v4.14.6/util/big_endian.go
  75. 269 0
      m7s.live/engine/v4@v4.14.6/util/big_little_endian.go
  76. 118 0
      m7s.live/engine/v4@v4.14.6/util/bits/bits.go
  77. 61 0
      m7s.live/engine/v4@v4.14.6/util/bits/bits_test.go
  78. 22 0
      m7s.live/engine/v4@v4.14.6/util/bits/bufio/bufio.go
  79. 65 0
      m7s.live/engine/v4@v4.14.6/util/bits/golomb_reader.go
  80. 3 0
      m7s.live/engine/v4@v4.14.6/util/bits/pio/pio.go
  81. 121 0
      m7s.live/engine/v4@v4.14.6/util/bits/pio/reader.go
  82. 68 0
      m7s.live/engine/v4@v4.14.6/util/bits/pio/vec.go
  83. 22 0
      m7s.live/engine/v4@v4.14.6/util/bits/pio/vec_test.go
  84. 87 0
      m7s.live/engine/v4@v4.14.6/util/bits/pio/writer.go
  85. 253 0
      m7s.live/engine/v4@v4.14.6/util/buffer.go
  86. 18 0
      m7s.live/engine/v4@v4.14.6/util/buffer_test.go
  87. 407 0
      m7s.live/engine/v4@v4.14.6/util/convert.go
  88. 126 0
      m7s.live/engine/v4@v4.14.6/util/crc32.go
  89. 86 0
      m7s.live/engine/v4@v4.14.6/util/index.go
  90. 31 0
      m7s.live/engine/v4@v4.14.6/util/ip.go
  91. 13 0
      m7s.live/engine/v4@v4.14.6/util/linux.go
  92. 179 0
      m7s.live/engine/v4@v4.14.6/util/list.go
  93. 68 0
      m7s.live/engine/v4@v4.14.6/util/map.go
  94. 342 0
      m7s.live/engine/v4@v4.14.6/util/pool.go
  95. 77 0
      m7s.live/engine/v4@v4.14.6/util/reorder.go
  96. 38 0
      m7s.live/engine/v4@v4.14.6/util/reorder_test.go
  97. 39 0
      m7s.live/engine/v4@v4.14.6/util/retry.go
  98. 136 0
      m7s.live/engine/v4@v4.14.6/util/ring.go
  99. 88 0
      m7s.live/engine/v4@v4.14.6/util/safe_chan.go
  100. 36 0
      m7s.live/engine/v4@v4.14.6/util/slice.go

+ 1 - 0
m7s.live/engine/v4@v4.14.6/.gitignore

@@ -0,0 +1 @@
+.idea

+ 85 - 0
m7s.live/engine/v4@v4.14.6/.golangci.yml

@@ -0,0 +1,85 @@
+run:
+    timeout: 5m
+
+linters:
+    disable-all: true
+    enable:
+        - bodyclose
+        - deadcode
+        - dogsled
+        - dupl
+        - durationcheck
+        - exhaustive
+        - exportloopref
+        - gci
+        - gofmt
+        - gofumpt
+        - goimports
+        - gomoddirectives
+        - goprintffuncname
+        - govet
+        - importas
+        - ineffassign
+        - makezero
+        - misspell
+        - nakedret
+        - nilerr
+        - noctx
+        - nolintlint
+        - prealloc
+        - predeclared
+        - revive
+        - rowserrcheck
+        - sqlclosecheck
+        - staticcheck
+        - structcheck
+        - stylecheck
+        - tparallel
+        # - typecheck
+        - unconvert
+        - unparam
+        - unused
+        - varcheck
+        - wastedassign
+        - whitespace
+
+        # fixme
+        # - cyclop
+        # - errcheck
+        # - errorlint
+        # - exhaustivestruct
+        # - forbidigo
+        # - forcetypeassert
+        # - gochecknoglobals
+        # - gochecknoinits
+        # - gocognit
+        # - goconst
+        # - gocritic
+        # - gocyclo
+        # - godot
+        # - gosec
+        # - gosimple
+        # - ifshort
+        # - lll
+        # - nlreturn
+        # - paralleltest
+        # - scopelint
+        # - thelper
+        # - wrapcheck
+
+        # unused
+        # - depguard
+        # - goheader
+        # - gomodguard
+
+        # don't enable:
+        # - asciicheck
+        # - funlen
+        # - godox
+        # - goerr113
+        # - gomnd
+        # - interfacer
+        # - maligned
+        # - nestif
+        # - testpackage
+        # - wsl

+ 21 - 0
m7s.live/engine/v4@v4.14.6/LICENSE

@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2019-present, dexter
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 207 - 0
m7s.live/engine/v4@v4.14.6/README.md

@@ -0,0 +1,207 @@
+# m7s v4核心引擎
+
+该项目为m7s的引擎部分,该部分逻辑是流媒体服务器的核心转发逻辑。包含了一个插件的引入机制,其他功能均由插件实现
+
+# 引擎的基本功能
+- 提供插件机制,对插件的启动,配置解析,事件派发等进行统一管理
+- 提供H264、H265、AAC、G711格式的转发
+- 提供可复用的AVCC格式、RTP格式、AnnexB格式、ADTS格式等预封装机制
+- 提供多Track机制,支持大小流,加密流扩展
+- 提供DataTrack机制,可用于实现房间文字聊天等功能
+- 提供时间戳同步机制,限速机制
+- 提供RTP包乱序重排机制
+- 提供订阅者追帧跳帧机制
+- 提供发布订阅对外推拉的基础架构
+- 提供鉴权机制的底层架构支持
+- 提供内存复用机制
+- 提供发布者断线重连机制
+- 提供按需拉流机制
+- 提供HTTP服务端口公用机制
+- 提供HTTP API接口自动注册机制
+- 提供HTTP接口中间件机制
+- 提供结构化日志
+- 提供流信息统计和输出
+- 提供事件总线机制,可以对所有插件广播事件
+- 提供配置热更新机制
+## 引擎自带HTTP接口
+- 获取某一个流的详情 `/api/stream?streamPath=xxx`
+- 终止某一个流 `/api/closestream?streamPath=xxx`
+- 获取engine信息 `/api/sysInfo` 返回值{Version:xxx,StartTime:xxx,IP:[xxx.xxx.xxx.xxx]}
+- 获取系统基本情况 `/api/summary` 返回值Summary数据
+- 获取所有插件信息 `/api/plugins` 返回值Plugin数据
+- 读取mp4文件再次发布为视频流 `/api/replay/mp4?streamPath=xxx&dump=filepath`  filepath是文件路径
+- 读取ts文件再次发布为视频流 `/api/replay/ts?streamPath=xxx&dump=filepath`  filepath是文件路径
+- 获取指定的配置信息 `/api/getconfig?name=xxx` 返回xxx插件的配置信息,如果不带参数或参数为空则返回全局配置
+- 修改并保存配置信息 `/api/modifyconfig?name=xxx&yaml=1` 修改xxx插件的配置信息,在请求的body中传入修改后的配置yaml字符串
+- 热更新配置信息 `/api/updateconfig?name=xxx` 热更新xxx插件的配置信息,如果不带参数或参数为空则热更新全局配置
+- 获取所有远端拉流信息 `/api/list/pull` 返回{RemoteURL:"",StreamPath:"",Type:"",StartTime:""}
+- 获取所有向远端推流信息 `/api/list/push` 返回{RemoteURL:"",StreamPath:"",Type:"",StartTime:""}
+- 停止推流 `/api/stop/push?url=xxx` 停止向xxx推流 ,成功返回ok
+- 停止某个订阅者 `/api/stop/subscribe?streamPath=xxx&id=xxx` 停止xxx流的xxx订阅者 ,成功返回ok
+- 插入SEI帧 `/api/insertsei?streamPath=xxx&type=5` 向xxx流内插入SEI帧 ,成功返回ok。type为SEI类型,可选,默认是5
+# 引擎默认配置
+```yaml
+global:
+  disableall: false # 是否禁用所有插件
+  loglang: zh # 日志语言,可选值:zh,en
+  loglevel: info # 日志级别,可选值:debug,info,warn,error,panic,fatal
+  http:
+    listenaddr: :8080 # 网关地址,用于访问API
+    listenaddrtls: :8443  # 用于HTTPS方式访问API的端口配置
+    certfile: ""
+    keyfile: ""
+    cors: true  # 是否自动添加cors头
+    username: ""  # 用户名和密码,用于API访问时的基本身份认证
+    password: ""
+    readtimeout: 0 # 读取超时时间,0为不限制
+    writetimeout: 0 # 写入超时时间,0为不限制
+    idletimeout: 0 # 空闲超时时间,0为不限制
+  publish:
+      pubaudio: true # 是否发布音频流
+      pubvideo: true # 是否发布视频流
+      kickexist: false # 剔出已经存在的发布者,用于顶替原有发布者
+      insertsei: false # 是否开启插入SEI信息功能
+      publishtimeout: 10s # 发布流默认过期时间,超过该时间发布者没有恢复流将被删除
+      delayclosetimeout: 0 # 自动关闭触发后延迟的时间(期间内如果有新的订阅则取消触发关闭),0为关闭该功能,保持连接。
+      waitclosetimeout: 0 # 发布者断开后等待时间,超过该时间发布者没有恢复流将被删除,0为关闭该功能,由订阅者决定是否删除
+      buffertime: 0 # 缓存时间,用于时光回溯,0为关闭缓存
+      idletimeout: 0 # 空闲超时时间,0为不限制
+      speedlimit: 500ms # 限速超时时间 0为不限速,对于读取文件这类流需要限速,否则读取过快(如果流的时间戳不正确,则只能关闭该功能:设置为0)
+      key:                      # 发布鉴权key
+	    secretargname: secret     # 发布鉴权参数名
+	    expireargname:   expire   # 发布鉴权失效时间参数名
+  subscribe:
+      subaudio: true # 是否订阅音频流
+      subvideo: true # 是否订阅视频流
+      subaudioargname: ats # 订阅音频轨道参数名
+      subvideoargname: vts # 订阅视频轨道参数名
+      subdataargname: dts # 订阅数据轨道参数名
+      subaudiotracks: [] # 订阅音频轨道名称列表
+      subvideotracks: [] # 订阅视频轨道名称列表
+      submode: 0 # 订阅模式,0为跳帧追赶模式,1为不追赶(多用于录制),2为时光回溯模式
+      syncmode: 0 # 音视频同步模式,0按照时间戳同步,1按照写入时间同步(在时间戳不正确的时候)
+      iframeonly: false # 只订阅关键帧
+      waittimeout: 10s # 等待发布者的超时时间,用于订阅尚未发布的流
+      writebuffersize: 0 # 订阅者写缓存大小,用于减少io次数,但可能影响实时性
+      key:                      # 订阅鉴权key
+	    secretargname: secret     # 订阅鉴权参数名
+	    expireargname:   expire   # 订阅鉴权失效时间参数名
+      internal: false # 是否内部订阅,内部订阅不会触发发布者自动断开功能
+  enableavcc : true  # 启用AVCC格式缓存,用于rtmp协议
+  enablertp : true # 启用rtp格式缓存,用于rtsp、websocket、gb28181协议
+  enableauth: true # 启用鉴权,详细查看鉴权机制
+  enablesubevent: true # 启用订阅事件,用于订阅者上下线事件,关闭可以提高性能
+  rtpreoderbufferlen: 50 # rtp乱序重排缓存长度
+  eventbussize: 10 # 事件总线缓存大小,事件较多时容易堵阻塞线程,需要增大缓存
+  poolsize: 0 # 内存池大小,0为不使用内存池
+  pulseinterval: 5s # 心跳事件间隔时间
+  console: 
+    server : console.monibuca.com:44944 # 连接远程控制台的地址
+    secret: "" # 远程控制台的秘钥
+    publicaddr: "" # 实例公网地址,提供远程控制台访问的地址,不配置的话使用自动识别的地址
+    publicaddrtls: "" # 实例公网地址,提供远程控制台访问的地址,不配置的话使用自动识别的地址(https)
+```
+
+# 配置覆盖机制
+- 如果不存在配置文件,将使用默认配置,该配置值为代码中写死的配置值
+- 如果存在配置文件,则使用配置文件中的值覆盖默认值
+- http、publish、subscribe三个配置遵循优先级顺序
+1. 如果发布流或者订阅流中包含对应的参数,则优先使用
+2. 其次,查找对应插件的配置项中是否包含配置项
+3. 最后,使用全局配置中的配置
+
+# 流的状态图
+```mermaid
+stateDiagram-v2
+    [*] --> ⌛等待发布者 : 创建
+    ⌛等待发布者 --> 🟢正在发布 :发布
+    ⌛等待发布者 --> 🔴已关闭 :关闭
+    ⌛等待发布者 --> 🔴已关闭  :超时
+    ⌛等待发布者 --> 🔴已关闭  :最后订阅者离开
+    🟢正在发布 --> ⌛等待发布者: 发布者断开
+    🟢正在发布 --> 🟡等待关闭: 最后订阅者离开
+    🟢正在发布 --> 🔴已关闭  :关闭
+    🟡等待关闭 --> 🟢正在发布 :第一个订阅者进入
+    🟡等待关闭 --> 🔴已关闭  :关闭
+    🟡等待关闭 --> 🔴已关闭  :超时
+    🟡等待关闭 --> 🔴已关闭  :发布者断开
+```
+
+# 鉴权机制
+## 默认鉴权
+
+在publish 和 subscribe 中配置 key 引擎会自动进行鉴权,
+推流或者拉流时需要在url中添加参数 secret=xxx&expire=xxx。
+
+- secret为鉴权前面,MD5(key+StreamPath+expire)
+- expire为鉴权失效时间,格式是十六进制 UNIX 时间戳
+
+### 时间戳计算
+```
+设置时间:2018.12.01 08:30:00
+十进制 UNIX 时间戳:1543624200
+十六进制 UNIX 时间戳:5C01D608(云直播鉴权配置使用十六进制 UNIX 时间戳,十六进制不区分字母大小写)
+```
+### 鉴权签名计算
+```
+secret = MD5(key+StreamPath+expire) 
+secret = MD5(ngoeiq03+test/01+5C01D608)
+secret = MD5(ngoeiq03test/015C01D608)
+secret = ce797dc6238156d548ef945e6ad1ea20
+```
+
+## 单独鉴权
+
+如果需要自定义鉴权,可以在插件中实现鉴权接口,
+引擎中定义如下两个接口,插件中的发布者或者订阅者可以实现这两个接口,引擎会在发布或者订阅时调用这两个接口进行鉴权
+```go
+type AuthSub interface {
+	OnAuth(*util.Promise[ISubscriber]) error
+}
+
+type AuthPub interface {
+	OnAuth(*util.Promise[IPublisher]) error
+}
+```
+- OnAuth返回错误即鉴权失败
+- Promise方便异步鉴权,可以后续调用其Resolve或Reject方法进行鉴权结果的返回
+
+## 全局鉴权
+
+自定义鉴权也可以全局生效,
+引擎中定义如下两个全局函数的变量,插件中可以对这两个变量进行赋值,引擎会在发布或者订阅时调用这两个接口进行鉴权
+```go
+var OnAuthSub func(p *util.Promise[ISubscriber]) error
+var OnAuthPub func(p *util.Promise[IPublisher]) error
+```
+** 注意:如果单独鉴权和全局鉴权同时存在,优先使用单独鉴权 **
+** 全局鉴权函数可以被多次覆盖,所以需要自己实现鉴权逻辑的合并 **
+
+# Http中间件
+在HTTPConfig接口中增加了AddMiddleware方法,可以通过该方法添加中间件,中间件的定义如下
+```go
+type Middleware func(string, http.Handler) http.Handler
+type HTTPConfig interface {
+	GetHTTPConfig() *HTTP
+	Listen(ctx context.Context) error
+	Handle(string, http.Handler)
+	AddMiddleware(Middleware)
+}
+
+```
+中间件的添加必须在FirstConfig之前,也就是在Listen之前
+例如:
+```go
+type MyMiddlewareConfig struct {
+  	config.HTTP
+}
+var myMiddlewareConfig = &MyMiddlewareConfig{}
+func init(){
+  myMiddlewareConfig.AddMiddleware(func(pattern string, handler http.Handler) http.Handler {
+    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+      // do something
+      handler.ServeHTTP(w, r)
+    })
+  })
+}
+```

+ 82 - 0
m7s.live/engine/v4@v4.14.6/codec/av1.go

@@ -0,0 +1,82 @@
+package codec
+
+import (
+	"errors"
+	"io"
+)
+
+var (
+	ErrInvalidMarker       = errors.New("invalid marker value found in AV1CodecConfigurationRecord")
+	ErrInvalidVersion      = errors.New("unsupported AV1CodecConfigurationRecord version")
+	ErrNonZeroReservedBits = errors.New("non-zero reserved bits found in AV1CodecConfigurationRecord")
+)
+
+const (
+	AV1_OBU_SEQUENCE_HEADER        = 1
+	AV1_OBU_TEMPORAL_DELIMITER     = 2
+	AV1_OBU_FRAME_HEADER           = 3
+	AV1_OBU_TILE_GROUP             = 4
+	AV1_OBU_METADATA               = 5
+	AV1_OBU_FRAME                  = 6
+	AV1_OBU_REDUNDANT_FRAME_HEADER = 7
+	AV1_OBU_TILE_LIST              = 8
+	AV1_OBU_PADDING                = 15
+)
+
+type AV1CodecConfigurationRecord struct {
+	Version                          byte
+	SeqProfile                       byte
+	SeqLevelIdx0                     byte
+	SeqTier0                         byte
+	HighBitdepth                     byte
+	TwelveBit                        byte
+	MonoChrome                       byte
+	ChromaSubsamplingX               byte
+	ChromaSubsamplingY               byte
+	ChromaSamplePosition             byte
+	InitialPresentationDelayPresent  byte
+	InitialPresentationDelayMinusOne byte
+	ConfigOBUs                       []byte
+}
+
+func (p *AV1CodecConfigurationRecord) Unmarshal(data []byte) (n int, err error) {
+	l := len(data)
+	if l < 4 {
+		err = io.ErrShortWrite
+		return
+	}
+	Marker := data[0] >> 7
+	if Marker != 1 {
+		return 0, ErrInvalidMarker
+	}
+	p.Version = data[0] & 0x7F
+	if p.Version != 1 {
+		return 1, ErrInvalidVersion
+	}
+	p.SeqProfile = data[1] >> 5
+	p.SeqLevelIdx0 = data[1] & 0x1F
+	p.SeqTier0 = data[2] >> 7
+	p.HighBitdepth = (data[2] >> 6) & 0x01
+	p.TwelveBit = (data[2] >> 5) & 0x01
+	p.MonoChrome = (data[2] >> 4) & 0x01
+	p.ChromaSubsamplingX = (data[2] >> 3) & 0x01
+	p.ChromaSubsamplingY = (data[2] >> 2) & 0x01
+	p.ChromaSamplePosition = data[2] & 0x03
+	if data[3]>>5 != 0 {
+		return 3, ErrNonZeroReservedBits
+	}
+	p.InitialPresentationDelayPresent = (data[3] >> 4) & 0x01
+	if p.InitialPresentationDelayPresent == 1 {
+		p.InitialPresentationDelayMinusOne = data[3] & 0x0F
+	} else {
+		if data[3]&0x0F != 0 {
+			return 3, ErrNonZeroReservedBits
+		}
+		p.InitialPresentationDelayMinusOne = 0
+	}
+	if l > 4 {
+		p.ConfigOBUs = data[4:]
+	}
+
+	return l, nil
+}

+ 231 - 0
m7s.live/engine/v4@v4.14.6/codec/codec.go

@@ -0,0 +1,231 @@
+package codec
+
+import (
+	"errors"
+)
+
+type AudioCodecID byte
+type VideoCodecID byte
+
+const (
+	ADTS_HEADER_SIZE              = 7
+	CodecID_AAC      AudioCodecID = 0xA
+	CodecID_PCMA     AudioCodecID = 7
+	CodecID_PCMU     AudioCodecID = 8
+	CodecID_OPUS     AudioCodecID = 0xC
+	CodecID_H264     VideoCodecID = 7
+	CodecID_H265     VideoCodecID = 0xC
+	CodecID_AV1      VideoCodecID = 0xD
+)
+
+func (codecId AudioCodecID) String() string {
+	switch codecId {
+	case CodecID_AAC:
+		return "aac"
+	case CodecID_PCMA:
+		return "pcma"
+	case CodecID_PCMU:
+		return "pcmu"
+	case CodecID_OPUS:
+		return "opus"
+	}
+	return "unknow"
+}
+
+func (codecId VideoCodecID) String() string {
+	switch codecId {
+	case CodecID_H264:
+		return "h264"
+	case CodecID_H265:
+		return "h265"
+	case CodecID_AV1:
+		return "av1"
+	}
+	return "unknow"
+}
+
+// ISO/IEC 14496-3 38(52)/page
+//
+// Audio
+//
+
+type AudioSpecificConfig struct {
+	AudioObjectType        byte // 5 bits
+	SamplingFrequencyIndex byte // 4 bits
+	ChannelConfiguration   byte // 4 bits
+	GASpecificConfig
+}
+
+func (asc *AudioSpecificConfig) Parse(data []byte) {
+	asc.AudioObjectType = data[0] >> 3
+	asc.SamplingFrequencyIndex = (data[0] & 0x07 << 1) | (data[1] >> 7)
+	asc.ChannelConfiguration = (data[1] >> 3) & 0x0F
+	asc.FrameLengthFlag = (data[1] >> 2) & 0x01
+	asc.DependsOnCoreCoder = (data[1] >> 1) & 0x01
+	asc.ExtensionFlag = data[1] & 0x01
+}
+
+func (asc *AudioSpecificConfig) ToADTS(rawDataLength int, adtsByte []byte) (adts ADTS, err error) {
+	return AudioSpecificConfigToADTS(asc, rawDataLength, adtsByte)
+}
+
+type GASpecificConfig struct {
+	FrameLengthFlag    byte // 1 bit
+	DependsOnCoreCoder byte // 1 bit
+	ExtensionFlag      byte // 1 bit
+}
+
+//
+// AudioObjectTypes -> ISO/IEC 14496-3 43(57)/page
+//
+// 1 AAC MAIN 	ISO/IEC 14496-3 subpart 4
+// 2 AAC LC 	ISO/IEC 14496-3 subpart 4
+// 3 AAC SSR 	ISO/IEC 14496-3 subpart 4
+// 4 AAC LTP 	ISO/IEC 14496-3 subpart 4
+//
+//
+
+// ISO/IEC 13838-7 20(25)/page
+//
+// # Advanced Audio Coding
+//
+// AudioDataTransportStream
+type ADTS struct {
+	ADTSFixedHeader
+	ADTSVariableHeader
+}
+
+// 28 bits
+type ADTSFixedHeader struct {
+	SyncWord               uint16 // 12 bits The bit string ‘1111 1111 1111’. See ISO/IEC 11172-3,subclause 2.4.2.3 (Table 8)
+	ID                     byte   // 1 bit MPEG identifier, set to ‘1’. See ISO/IEC 11172-3,subclause 2.4.2.3 (Table 8)
+	Layer                  byte   // 2 bits Indicates which layer is used. Set to ‘00’. See ISO/IEC 11172-3,subclause 2.4.2.3 (Table 8)
+	ProtectionAbsent       byte   // 1 bit Indicates whether error_check() data is present or not. Same assyntax element ‘protection_bit’ in ISO/IEC 11172-3,subclause 2.4.1 and 2.4.2 (Table 8)
+	Profile                byte   // 2 bits profile used. See clause 2 (Table 8)
+	SamplingFrequencyIndex byte   // 4 bits indicates the sampling frequency used according to the followingtable (Table 8)
+	PrivateBit             byte   // 1 bit see ISO/IEC 11172-3, subclause 2.4.2.3 (Table 8)
+	ChannelConfiguration   byte   // 3 bits indicates the channel configuration used. Ifchannel_configuration is greater than 0, the channelconfiguration is given in Table 42, see subclause 8.5.3.1. Ifchannel_configuration equals 0, the channel configuration is notspecified in the header and must be given by aprogram_config_element() following as first syntactic element inthe first raw_data_block() after the header (seesubclause 8.5.3.2), or by the implicit configuration (seesubclause 8.5.3.3) or must be known in the application (Table 8)
+	OriginalCopy           byte   // 1 bit see ISO/IEC 11172-3, definition of data element copyright
+	Home                   byte   // 1 bit see ISO/IEC 11172-3, definition of data element original/copy
+}
+
+// SyncWord, 同步头 总是0xFFF, all bits must be 1,代表着一个ADTS帧的开始
+// ID, MPEG Version: 0 for MPEG-4, 1 for MPEG-2
+// Layer, always: '00'
+// ProtectionAbsent, 表示是否误码校验
+// Profile, 表示使用哪个级别的AAC,有些芯片只支持AAC LC 。在MPEG-2 AAC中定义了3种.
+// SamplingFrequencyIndex, 表示使用的采样率下标,通过这个下标在 Sampling Frequencies[ ]数组中查找得知采样率的值
+// PrivateBit,
+// ChannelConfiguration, 表示声道数
+// OriginalCopy,
+// Home,
+
+// Profile:
+//
+// 0: Main profile
+// 1: Low Complexity profile(LC)
+// 2: Scalable Sampling Rate profile(SSR)
+// 3: Reserved
+var SamplingFrequencies = [...]int{96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350, 0, 0, 0}
+
+// Sampling Frequencies[]:
+//
+// 0: 96000 Hz
+// 1: 88200 Hz
+// 2: 64000 Hz
+// 3: 48000 Hz
+// 4: 44100 Hz
+// 5: 32000 Hz
+// 6: 24000 Hz
+// 7: 22050 Hz
+// 8: 16000 Hz
+// 9: 12000 Hz
+// 10: 11025 Hz
+// 11: 8000 Hz
+// 12: 7350 Hz
+// 13: Reserved
+// 14: Reserved
+// 15: frequency is written explictly
+//
+
+// ChannelConfiguration:
+//
+// 0: Defined in AOT Specifc Config
+// 1: 1 channel: front-center
+// 2: 2 channels: front-left, front-right
+// 3: 3 channels: front-center, front-left, front-right
+// 4: 4 channels: front-center, front-left, front-right, back-center
+// 5: 5 channels: front-center, front-left, front-right, back-left, back-right
+// 6: 6 channels: front-center, front-left, front-right, back-left, back-right, LFE-channel
+// 7: 8 channels: front-center, front-left, front-right, side-left, side-right, back-left, back-right, LFE-channel
+// 8-15: Reserved
+//
+
+// 28 bits
+type ADTSVariableHeader struct {
+	CopyrightIdentificationBit   byte   // 1 bit One bit of the 72-bit copyright identification field (seecopyright_id above). The bits of this field are transmitted frame by frame; the first bit is indicated by the copyright_identification_start bit set to ‘1’. The field consists of an 8-bit copyright_identifier, followed by a 64-bit copyright_number.The copyright identifier is given by a Registration Authority as designated by SC29. The copyright_number is a value which identifies uniquely the copyrighted material. See ISO/IEC 13818-3, subclause 2.5.2.13 (Table 9)
+	CopyrightIdentificationStart byte   // 1 bit One bit to indicate that the copyright_identification_bit in this audio frame is the first bit of the 72-bit copyright identification. If no copyright identification is transmitted, this bit should be kept '0'.'0' no start of copyright identification in this audio frame '1' start of copyright identification in this audio frame See ISO/IEC 13818-3, subclause 2.5.2.13 (Table 9)
+	AACFrameLength               uint16 // 13 bits Length of the frame including headers and error_check in bytes(Table 9)
+	ADTSBufferFullness           uint16 // 11 bits state of the bit reservoir in the course of encoding the ADTS frame, up to and including the first raw_data_block() and the optionally following adts_raw_data_block_error_check(). It is transmitted as the number of available bits in the bit reservoir divided by NCC divided by 32 and truncated to an integer value (Table 9). A value of hexadecimal 7FF signals that the bitstream is a variable rate bitstream. In this case, buffer fullness is not applicable
+	NumberOfRawDataBlockInFrame  byte   // 2 bits Number of raw_data_block()’s that are multiplexed in the adts_frame() is equal to number_of_raw_data_blocks_in_frame + 1. The minimum value is 0 indicating 1 raw_data_block()(Table 9)
+}
+
+// CopyrightIdentificationBit,
+// CopyrightIdentificationStart,
+// AACFrameLength, 一个ADTS帧的长度包括ADTS头和raw data block.
+// ADTSBufferFullness, 0x7FF 说明是码率可变的码流.
+// NumberOfRawDataBlockInFrame, 表示ADTS帧中有number_of_raw_data_blocks_in_frame + 1个AAC原始帧
+
+// 所以说number_of_raw_data_blocks_in_frame == 0 表示说ADTS帧中有一个AAC数据块并不是说没有。(一个AAC原始帧包含一段时间内1024个采样及相关数据)
+
+func AudioSpecificConfigToADTS(asc *AudioSpecificConfig, rawDataLength int, adtsByte []byte) (adts ADTS, err error) {
+	if asc.ChannelConfiguration > 8 || asc.FrameLengthFlag > 13 {
+		err = errors.New("Reserved field.")
+		return
+	}
+
+	// ADTSFixedHeader
+	adts.SyncWord = 0xfff
+	adts.ID = 0
+	adts.Layer = 0
+	adts.ProtectionAbsent = 1
+
+	// SyncWord(12) + ID(1) + Layer(2) + ProtectionAbsent(1)
+	adtsByte[0] = 0xFF
+	adtsByte[1] = 0xF1
+	if asc.AudioObjectType >= 3 || asc.AudioObjectType == 0 {
+		adts.Profile = 1
+	} else {
+		adts.Profile = asc.AudioObjectType - 1
+	}
+
+	adts.SamplingFrequencyIndex = asc.SamplingFrequencyIndex
+	adts.PrivateBit = 0
+	adts.ChannelConfiguration = asc.ChannelConfiguration
+	adts.OriginalCopy = 0
+	adts.Home = 0
+
+	// Profile(2) + SamplingFrequencyIndex(4) + PrivateBit(1) + ChannelConfiguration(3)(取高1位)
+	adtsByte[2] = uint8(adts.Profile<<6) + uint8(adts.SamplingFrequencyIndex<<2) + uint8(adts.PrivateBit<<1) + uint8((adts.ChannelConfiguration&0x7)>>2)
+	// ADTSVariableHeader
+	adts.CopyrightIdentificationBit = 0
+	adts.CopyrightIdentificationStart = 0
+	adts.AACFrameLength = 7 + uint16(rawDataLength)
+	adts.ADTSBufferFullness = 0x7ff
+	adts.NumberOfRawDataBlockInFrame = 0
+
+	// ChannelConfiguration(3)(取低2位) + OriginalCopy(1) + Home(1) + CopyrightIdentificationBit(1) + CopyrightIdentificationStart(1) +  AACFrameLength(13)(取高2位)
+	adtsByte[3] = uint8((adts.ChannelConfiguration&0x3)<<6) + uint8((adts.AACFrameLength&0x1fff)>>11)
+
+	// AACFrameLength(13)
+	// xx xxxxxxxx xxx
+	// 取中间的部分
+	adtsByte[4] = uint8(((adts.AACFrameLength & 0x1fff) >> 3) & 0x0ff)
+
+	// AACFrameLength(13)(取低3位) + ADTSBufferFullness(11)(取高5位)
+	adtsByte[5] = uint8((adts.AACFrameLength&0x0007)<<5) + 0x1f
+
+	// ADTSBufferFullness(11)(取低6位) + NumberOfRawDataBlockInFrame(2)
+	adtsByte[6] = 0xfc
+	return
+}

+ 125 - 0
m7s.live/engine/v4@v4.14.6/codec/flv.go

@@ -0,0 +1,125 @@
+package codec
+
+import (
+	"errors"
+	"io"
+	"net"
+
+	"m7s.live/engine/v4/util"
+)
+
+const (
+	// FLV Tag Type
+	FLV_TAG_TYPE_AUDIO  = 0x08
+	FLV_TAG_TYPE_VIDEO  = 0x09
+	FLV_TAG_TYPE_SCRIPT = 0x12
+)
+const (
+	PacketTypeSequenceStart = iota
+	PacketTypeCodedFrames
+	PacketTypeSequenceEnd
+	PacketTypeCodedFramesX
+	PacketTypeMetadata
+	PacketTypeMPEG2TSSequenceStart
+)
+var (
+	Codec2SoundFormat = map[string]byte{
+		"aac":  10,
+		"pcma": 7,
+		"pcmu": 8,
+	}
+	// 音频格式. 4 bit
+	SoundFormat = map[byte]string{
+		0:  "Linear PCM, platform endian",
+		1:  "ADPCM",
+		2:  "MP3",
+		3:  "Linear PCM, little endian",
+		4:  "Nellymoser 16kHz mono",
+		5:  "Nellymoser 8kHz mono",
+		6:  "Nellymoser",
+		7:  "PCMA",
+		8:  "PCMU",
+		9:  "reserved",
+		10: "AAC",
+		11: "Speex",
+		14: "MP3 8Khz",
+		15: "Device-specific sound"}
+
+	// 采样频率. 2 bit
+	SoundRate = map[byte]int{
+		0: 5500,
+		1: 11000,
+		2: 22000,
+		3: 44000}
+
+	// 量化精度. 1 bit
+	SoundSize = map[byte]string{
+		0: "8Bit",
+		1: "16Bit"}
+
+	// 音频类型. 1bit
+	SoundType = map[byte]string{
+		0: "Mono",
+		1: "Stereo"}
+
+	// 视频帧类型. 4bit
+	FrameType = map[byte]string{
+		1: "keyframe (for AVC, a seekable frame)",
+		2: "inter frame (for AVC, a non-seekable frame)",
+		3: "disposable inter frame (H.263 only)",
+		4: "generated keyframe (reserved for server use only)",
+		5: "video info/command frame"}
+
+	// 视频编码类型. 4bit
+	CodecID = map[byte]string{
+		1:  "JPEG (currently unused)",
+		2:  "Sorenson H.263",
+		3:  "Screen video",
+		4:  "On2 VP6",
+		5:  "On2 VP6 with alpha channel",
+		6:  "Screen video version 2",
+		7:  "H264",
+		12: "H265"}
+)
+var ErrInvalidFLV = errors.New("invalid flv")
+var FLVHeader = []byte{'F', 'L', 'V', 0x01, 0x05, 0, 0, 0, 9, 0, 0, 0, 0}
+
+func WriteFLVTag(w io.Writer, t byte, timestamp uint32, payload []byte) (err error) {
+	buffers := AVCC2FLV(t, timestamp, payload)
+	_, err = buffers.WriteTo(w)
+	return
+}
+
+func ReadFLVTag(r io.Reader) (t byte, timestamp uint32, payload []byte, err error) {
+	head := make([]byte, 11)
+	if _, err = io.ReadFull(r, head); err != nil {
+		return
+	}
+	t = head[0]
+	dataSize := util.ReadBE[int](head[1:4])
+	timestamp = (uint32(head[7]) << 24) | (uint32(head[4]) << 16) | (uint32(head[5]) << 8) | uint32(head[6])
+	payload = make([]byte, dataSize)
+	if _, err = io.ReadFull(r, payload); err == nil {
+		_, err = io.ReadFull(r, head[:4])
+	}
+	return
+}
+
+func AudioAVCC2FLV(ts uint32, avcc ...[]byte) net.Buffers {
+	return AVCC2FLV(FLV_TAG_TYPE_AUDIO, ts, avcc...)
+}
+
+func VideoAVCC2FLV(ts uint32, avcc ...[]byte) net.Buffers {
+	return AVCC2FLV(FLV_TAG_TYPE_VIDEO, ts, avcc...)
+}
+
+func AVCC2FLV(t byte, ts uint32, avcc ...[]byte) (flv net.Buffers) {
+	b := util.Buffer(make([]byte, 0, 15))
+	b.WriteByte(t)
+	dataSize := util.SizeOfBuffers(avcc)
+	b.WriteUint24(uint32(dataSize))
+	b.WriteUint24(ts)
+	b.WriteByte(byte(ts >> 24))
+	b.WriteUint24(0)
+	return append(append(append(flv, b), avcc...), util.PutBE(b.Malloc(4), dataSize+11))
+}

+ 325 - 0
m7s.live/engine/v4@v4.14.6/codec/h264.go

@@ -0,0 +1,325 @@
+package codec
+
+import (
+	"bytes"
+	"errors"
+	"io"
+
+	"m7s.live/engine/v4/util"
+	"m7s.live/engine/v4/util/bits/pio"
+)
+
+// Start Code + NAL Unit -> NALU Header + NALU Body
+// RTP Packet -> NALU Header + NALU Body
+
+// NALU Body -> Slice Header + Slice data
+// Slice data -> flags + Macroblock layer1 + Macroblock layer2 + ...
+// Macroblock layer1 -> mb_type + PCM Data
+// Macroblock layer2 -> mb_type + Sub_mb_pred or mb_pred + Residual Data
+// Residual Data ->
+type H264NALUType byte
+
+func (b H264NALUType) Or(b2 byte) byte {
+	return byte(b) | b2
+}
+
+func (b H264NALUType) Offset() int {
+	switch b {
+	case NALU_STAPA:
+		return 1
+	case NALU_STAPB:
+		return 3
+	case NALU_FUA:
+		return 2
+	case NALU_FUB:
+		return 4
+	}
+	return 0
+}
+
+func (b H264NALUType) Byte() byte {
+	return byte(b)
+}
+func ParseH264NALUType(b byte) H264NALUType {
+	return H264NALUType(b & 0x1F)
+}
+func (H264NALUType) Parse(b byte) H264NALUType {
+	return H264NALUType(b & 0x1F)
+}
+
+func (H264NALUType) ParseBytes(bs []byte) H264NALUType {
+	return H264NALUType(bs[0] & 0x1F)
+}
+
+const (
+	// NALU Type
+	NALU_Unspecified           H264NALUType = iota
+	NALU_Non_IDR_Picture                    // 1
+	NALU_Data_Partition_A                   // 2
+	NALU_Data_Partition_B                   // 3
+	NALU_Data_Partition_C                   // 4
+	NALU_IDR_Picture                        // 5
+	NALU_SEI                                // 6
+	NALU_SPS                                // 7
+	NALU_PPS                                // 8
+	NALU_Access_Unit_Delimiter              // 9
+	NALU_Sequence_End                       // 10
+	NALU_Stream_End                         // 11
+	NALU_Filler_Data                        // 12
+	NALU_SPS_Extension                      // 13
+	NALU_Prefix                             // 14
+	NALU_SPS_Subset                         // 15
+	NALU_DPS                                // 16
+	NALU_Reserved1                          // 17
+	NALU_Reserved2                          // 18
+	NALU_Not_Auxiliary_Coded                // 19
+	NALU_Coded_Slice_Extension              // 20
+	NALU_Reserved3                          // 21
+	NALU_Reserved4                          // 22
+	NALU_Reserved5                          // 23
+	NALU_STAPA                              // 24
+	NALU_STAPB
+	NALU_MTAP16
+	NALU_MTAP24
+	NALU_FUA // 28
+	NALU_FUB
+	// 24 - 31 NALU_NotReserved
+
+)
+
+var (
+	NALU_AUD_BYTE   = []byte{0x00, 0x00, 0x00, 0x01, 0x09, 0xF0}
+	NALU_Delimiter1 = []byte{0x00, 0x00, 0x01}
+	NALU_Delimiter2 = []byte{0x00, 0x00, 0x00, 0x01}
+	// 0x17 keyframe  7:AVC
+	// 0x00 AVC sequence header
+	// 0x00 0x00 0x00
+	// 0x01 configurationVersion
+	// 0x42 AVCProfileIndication
+	// 0x00 profile_compatibility
+	// 0x1E AVCLevelIndication
+	// 0xFF lengthSizeMinusOne
+	RTMP_AVC_HEAD         = []byte{0x17, 0x00, 0x00, 0x00, 0x00, 0x01, 0x42, 0x00, 0x1E, 0xFF}
+	RTMP_KEYFRAME_HEAD    = []byte{0x17, 0x01, 0x00, 0x00, 0x00}
+	RTMP_NORMALFRAME_HEAD = []byte{0x27, 0x01, 0x00, 0x00, 0x00}
+)
+
+// H.264/AVC视频编码标准中,整个系统框架被分为了两个层面:视频编码层面(VCL)和网络抽象层面(NAL)
+// NAL - Network Abstract Layer
+// raw byte sequence payload (RBSP) 原始字节序列载荷
+
+// SplitH264 以0x00000001分割H264裸数据
+func SplitH264(payload []byte) (nalus [][]byte) {
+	for _, v := range bytes.SplitN(payload, NALU_Delimiter2, -1) {
+		if len(v) == 0 {
+			continue
+		}
+		nalus = append(nalus, bytes.SplitN(v, NALU_Delimiter1, -1)...)
+	}
+	return
+}
+
+func BuildH264SeqHeaderFromSpsPps(sps, pps []byte) (seqHeader []byte) {
+	lenSPS, lenPPS := len(sps), len(pps)
+	seqHeader = append([]byte{}, RTMP_AVC_HEAD...)
+	if lenSPS > 3 {
+		copy(seqHeader[6:], sps[1:4])
+	}
+	seqHeader = append(seqHeader, 0xE1, byte(lenSPS>>8), byte(lenSPS))
+	seqHeader = append(seqHeader, sps...)
+	seqHeader = append(append(seqHeader, 0x01, byte(lenPPS>>8), byte(lenPPS)), pps...)
+	return
+}
+
+// ISO/IEC 14496-15 11(16)/page
+//
+// Advanced Video Coding
+//
+
+// AVCC
+type AVCDecoderConfigurationRecord struct {
+	ConfigurationVersion       byte // 8 bits Version
+	AVCProfileIndication       byte // 8 bits
+	ProfileCompatibility       byte // 8 bits
+	AVCLevelIndication         byte // 8 bits
+	Reserved1                  byte // 6 bits
+	LengthSizeMinusOne         byte // 2 bits 非常重要,每个NALU包前面都(lengthSizeMinusOne & 3)+1个字节的NAL包长度描述
+	Reserved2                  byte // 3 bits
+	NumOfSequenceParameterSets byte // 5 bits SPS 的个数,计算方法是 numOfSequenceParameterSets & 0x1F
+	NumOfPictureParameterSets  byte // 8 bits PPS 的个数
+
+	SequenceParameterSetLength  uint16 // 16 byte SPS Length
+	SequenceParameterSetNALUnit []byte // n byte  SPS
+	PictureParameterSetLength   uint16 // 16 byte PPS Length
+	PictureParameterSetNALUnit  []byte // n byte  PPS
+}
+
+func (p *AVCDecoderConfigurationRecord) Marshal(b []byte) (n int) {
+	b[0] = 1
+	b[1] = p.AVCProfileIndication
+	b[2] = p.ProfileCompatibility
+	b[3] = p.AVCLevelIndication
+	b[4] = p.LengthSizeMinusOne | 0xfc
+	b[5] = uint8(1) | 0xe0
+	n += 6
+
+	pio.PutU16BE(b[n:], p.SequenceParameterSetLength)
+	n += 2
+	copy(b[n:], p.SequenceParameterSetNALUnit)
+	n += len(p.SequenceParameterSetNALUnit)
+	b[n] = uint8(1)
+	n++
+
+	pio.PutU16BE(b[n:], p.PictureParameterSetLength)
+	n += 2
+	copy(b[n:], p.PictureParameterSetNALUnit)
+	n += len(p.PictureParameterSetNALUnit)
+
+	return
+}
+
+var ErrDecconfInvalid = errors.New("decode error")
+
+func (p *AVCDecoderConfigurationRecord) Unmarshal(b []byte) (n int, err error) {
+	if len(b) < 7 {
+		err = errors.New("not enough len")
+		return
+	}
+
+	p.AVCProfileIndication = b[1]
+	p.ProfileCompatibility = b[2]
+	p.AVCLevelIndication = b[3]
+	p.LengthSizeMinusOne = b[4] & 0x03
+	spscount := int(b[5] & 0x1f)
+	n += 6
+	var sps, pps [][]byte
+	for i := 0; i < spscount; i++ {
+		if len(b) < n+2 {
+			err = ErrDecconfInvalid
+			return
+		}
+		spslen := util.ReadBE[int](b[n : n+2])
+		n += 2
+
+		if len(b) < n+spslen {
+			err = ErrDecconfInvalid
+			return
+		}
+		sps = append(sps, b[n:n+spslen])
+		n += spslen
+	}
+	p.SequenceParameterSetLength = uint16(len(sps[0]))
+	p.SequenceParameterSetNALUnit = sps[0]
+	if len(b) < n+1 {
+		err = ErrDecconfInvalid
+		return
+	}
+	ppscount := int(b[n])
+	n++
+
+	for i := 0; i < ppscount; i++ {
+		if len(b) < n+2 {
+			err = ErrDecconfInvalid
+			return
+		}
+		ppslen := util.ReadBE[int](b[n : n+2])
+		n += 2
+
+		if len(b) < n+ppslen {
+			err = ErrDecconfInvalid
+			return
+		}
+		pps = append(pps, b[n:n+ppslen])
+		n += ppslen
+	}
+	if ppscount >= 1 {
+		p.PictureParameterSetLength = uint16(len(pps[0]))
+		p.PictureParameterSetNALUnit = pps[0]
+	} else {
+		err = ErrDecconfInvalid
+	}
+	return
+}
+
+type NALUnit struct {
+	NALUHeader
+	RBSP
+}
+
+type NALUHeader struct {
+	forbidden_zero_bit byte // 1 bit  0
+	nal_ref_idc        byte // 2 bits nal_unit_type等于6,9,10,11或12的NAL单元其nal_ref_idc都应等于 0
+	nal_uint_type      byte // 5 bits 包含在 NAL 单元中的 RBSP 数据结构的类型
+}
+
+type RBSP interface {
+}
+
+/*
+0      Unspecified                                                    non-VCL
+1      Coded slice of a non-IDR picture                               VCL
+2      Coded slice data partition A                                   VCL
+3      Coded slice data partition B                                   VCL
+4      Coded slice data partition C                                   VCL
+5      Coded slice of an IDR picture                                  VCL
+6      Supplemental enhancement information (SEI)                     non-VCL
+7      Sequence parameter set                                         non-VCL
+8      Picture parameter set                                          non-VCL
+9      Access unit delimiter                                          non-VCL
+10     End of sequence                                                non-VCL
+11     End of stream                                                  non-VCL
+12     Filler data                                                    non-VCL
+13     Sequence parameter set extension                               non-VCL
+14     Prefix NAL unit                                                non-VCL
+15     Subset sequence parameter set                                  non-VCL
+16     Depth parameter set                                            non-VCL
+17..18 Reserved                                                       non-VCL
+19     Coded slice of an auxiliary coded picture without partitioning non-VCL
+20     Coded slice extension                                          non-VCL
+21     Coded slice extension for depth view components                non-VCL
+22..23 Reserved                                                       non-VCL
+24..31 Unspecified                                                    non-VCL
+
+0:未规定
+1:非IDR图像中不采用数据划分的片段
+2:非IDR图像中A类数据划分片段
+3:非IDR图像中B类数据划分片段
+4:非IDR图像中C类数据划分片段
+5:IDR图像的片段
+6:补充增强信息(SEI)
+7:序列参数集(SPS)
+8:图像参数集(PPS)
+9:分割符
+10:序列结束符
+11:流结束符
+12:填充数据
+13:序列参数集扩展
+14:带前缀的NAL单元
+15:子序列参数集
+16 – 18:保留
+19:不采用数据划分的辅助编码图像片段
+20:编码片段扩展
+21 – 23:保留
+24 – 31:未规定
+
+nal_unit_type		NAL类型						nal_reference_bit
+0					未使用						0
+1					非IDR的片					此片属于参考帧,则不等于0,不属于参考帧,则等与0
+2					片数据A分区					同上
+3					片数据B分区					同上
+4					片数据C分区					同上
+5					IDR图像的片					5
+6					补充增强信息单元(SEI)		0
+7					序列参数集					非0
+8					图像参数集					非0
+9					分界符						0
+10					序列结束					0
+11					码流结束					0
+12					填充						0
+13..23				保留						0
+24..31				不保留						0
+*/
+
+func ReadPPS(w io.Writer) {
+
+}

+ 541 - 0
m7s.live/engine/v4@v4.14.6/codec/h265.go

@@ -0,0 +1,541 @@
+package codec
+
+import (
+	"bytes"
+	"errors"
+
+	"github.com/q191201771/naza/pkg/nazabits"
+	"m7s.live/engine/v4/util"
+)
+
+type H265NALUType byte
+
+func (H265NALUType) Parse(b byte) H265NALUType {
+	return H265NALUType(b & 0x7E >> 1)
+}
+
+func ParseH265NALUType(b byte) H265NALUType {
+	return H265NALUType(b & 0x7E >> 1)
+}
+
+const (
+	// HEVC_VPS    = 0x40
+	// HEVC_SPS    = 0x42
+	// HEVC_PPS    = 0x44
+	// HEVC_SEI    = 0x4E
+	// HEVC_IDR    = 0x26
+	// HEVC_PSLICE = 0x02
+
+	NAL_UNIT_CODED_SLICE_TRAIL_N H265NALUType = iota // 0
+	NAL_UNIT_CODED_SLICE_TRAIL_R                     // 1
+	NAL_UNIT_CODED_SLICE_TSA_N                       // 2
+	NAL_UNIT_CODED_SLICE_TLA                         // 3 // Current name in the spec: TSA_R
+	NAL_UNIT_CODED_SLICE_STSA_N                      // 4
+	NAL_UNIT_CODED_SLICE_STSA_R                      // 5
+	NAL_UNIT_CODED_SLICE_RADL_N                      // 6
+	NAL_UNIT_CODED_SLICE_DLP                         // 7 // Current name in the spec: RADL_R
+	NAL_UNIT_CODED_SLICE_RASL_N                      // 8
+	NAL_UNIT_CODED_SLICE_TFD                         // 9 // Current name in the spec: RASL_R
+	NAL_UNIT_RESERVED_10
+	NAL_UNIT_RESERVED_11
+	NAL_UNIT_RESERVED_12
+	NAL_UNIT_RESERVED_13
+	NAL_UNIT_RESERVED_14
+	NAL_UNIT_RESERVED_15
+	NAL_UNIT_CODED_SLICE_BLA      // 16 // Current name in the spec: BLA_W_LP
+	NAL_UNIT_CODED_SLICE_BLANT    // 17 // Current name in the spec: BLA_W_DLP
+	NAL_UNIT_CODED_SLICE_BLA_N_LP // 18
+	NAL_UNIT_CODED_SLICE_IDR      // 19// Current name in the spec: IDR_W_DLP
+	NAL_UNIT_CODED_SLICE_IDR_N_LP // 20
+	NAL_UNIT_CODED_SLICE_CRA      // 21
+	NAL_UNIT_RESERVED_22
+	NAL_UNIT_RESERVED_23
+	NAL_UNIT_RESERVED_24
+	NAL_UNIT_RESERVED_25
+	NAL_UNIT_RESERVED_26
+	NAL_UNIT_RESERVED_27
+	NAL_UNIT_RESERVED_28
+	NAL_UNIT_RESERVED_29
+	NAL_UNIT_RESERVED_30
+	NAL_UNIT_RESERVED_31
+	NAL_UNIT_VPS                   // 32
+	NAL_UNIT_SPS                   // 33
+	NAL_UNIT_PPS                   // 34
+	NAL_UNIT_ACCESS_UNIT_DELIMITER // 35
+	NAL_UNIT_EOS                   // 36
+	NAL_UNIT_EOB                   // 37
+	NAL_UNIT_FILLER_DATA           // 38
+	NAL_UNIT_SEI                   // 39 Prefix SEI
+	NAL_UNIT_SEI_SUFFIX            // 40 Suffix SEI
+	NAL_UNIT_RESERVED_41
+	NAL_UNIT_RESERVED_42
+	NAL_UNIT_RESERVED_43
+	NAL_UNIT_RESERVED_44
+	NAL_UNIT_RESERVED_45
+	NAL_UNIT_RESERVED_46
+	NAL_UNIT_RESERVED_47
+	NAL_UNIT_RTP_AP
+	NAL_UNIT_RTP_FU
+	NAL_UNIT_UNSPECIFIED_50
+	NAL_UNIT_UNSPECIFIED_51
+	NAL_UNIT_UNSPECIFIED_52
+	NAL_UNIT_UNSPECIFIED_53
+	NAL_UNIT_UNSPECIFIED_54
+	NAL_UNIT_UNSPECIFIED_55
+	NAL_UNIT_UNSPECIFIED_56
+	NAL_UNIT_UNSPECIFIED_57
+	NAL_UNIT_UNSPECIFIED_58
+	NAL_UNIT_UNSPECIFIED_59
+	NAL_UNIT_UNSPECIFIED_60
+	NAL_UNIT_UNSPECIFIED_61
+	NAL_UNIT_UNSPECIFIED_62
+	NAL_UNIT_UNSPECIFIED_63
+	NAL_UNIT_INVALID
+)
+
+var AudNalu = []byte{0x00, 0x00, 0x00, 0x01, 0x46, 0x01, 0x10}
+var ErrHevc = errors.New("hevc parse config error")
+var FourCC_H265_32 = util.BigEndian.Uint32([]byte{'h', 'v', 'c', '1'})
+var FourCC_AV1_32 = util.BigEndian.Uint32([]byte{'a', 'v', '0', '1'})
+// HVCC
+type HVCDecoderConfigurationRecord struct {
+	PicWidthInLumaSamples  uint32 // sps
+	PicHeightInLumaSamples uint32 // sps
+
+	configurationVersion uint8
+
+	generalProfileSpace              uint8
+	generalTierFlag                  uint8
+	generalProfileIdc                uint8
+	generalProfileCompatibilityFlags uint32
+	generalConstraintIndicatorFlags  uint64
+	generalLevelIdc                  uint8
+
+	lengthSizeMinusOne uint8
+
+	numTemporalLayers    uint8
+	temporalIdNested     uint8
+	parallelismType      uint8
+	chromaFormat         uint8
+	bitDepthLumaMinus8   uint8
+	bitDepthChromaMinus8 uint8
+	avgFrameRate         uint16
+}
+
+func ParseVpsSpsPpsFromSeqHeaderWithoutMalloc(payload []byte) (vps, sps, pps []byte, err error) {
+	if len(payload) < 5 {
+		return nil, nil, nil, ErrHevc
+	}
+
+	// if payload[0] != 0x1c || payload[1] != 0x00 {
+	// 	return nil, nil, nil, ErrHevc
+	// }
+
+	if len(payload) < 33 {
+		return nil, nil, nil, ErrHevc
+	}
+
+	index := 27
+	if numOfArrays := payload[index]; numOfArrays != 3 && numOfArrays != 4 {
+		return nil, nil, nil, ErrHevc
+	}
+	index++
+
+	if payload[index]&0x7f != byte(NAL_UNIT_VPS) {
+		return nil, nil, nil, ErrHevc
+	}
+	if numNalus := util.ReadBE[int](payload[index+1 : index+3]); numNalus != 1 {
+		return nil, nil, nil, ErrHevc
+	}
+	vpsLen := util.ReadBE[int](payload[index+3 : index+5])
+
+	if len(payload) < 33+vpsLen {
+		return nil, nil, nil, ErrHevc
+	}
+
+	vps = payload[index+5 : index+5+vpsLen]
+	index += 5 + vpsLen
+
+	if len(payload) < 38+vpsLen {
+		return nil, nil, nil, ErrHevc
+	}
+	if payload[index]&0x7f != byte(NAL_UNIT_SPS) {
+		return nil, nil, nil, ErrHevc
+	}
+	if numNalus := util.ReadBE[int](payload[index+1 : index+3]); numNalus != 1 {
+		return nil, nil, nil, ErrHevc
+	}
+	spsLen := util.ReadBE[int](payload[index+3 : index+5])
+	if len(payload) < 38+vpsLen+spsLen {
+		return nil, nil, nil, ErrHevc
+	}
+	sps = payload[index+5 : index+5+spsLen]
+	index += 5 + spsLen
+
+	if len(payload) < 43+vpsLen+spsLen {
+		return nil, nil, nil, ErrHevc
+	}
+	if payload[index]&0x7f != byte(NAL_UNIT_PPS) {
+		return nil, nil, nil, ErrHevc
+	}
+	// if numNalus := util.ReadBE[int](payload[index+1 : index+3]); numNalus != 1 {
+	// 	return nil, nil, nil, ErrHevc
+	// }
+	ppsLen := util.ReadBE[int](payload[index+3 : index+5])
+	if len(payload) < 43+vpsLen+spsLen+ppsLen {
+		return nil, nil, nil, ErrHevc
+	}
+	pps = payload[index+5 : index+5+ppsLen]
+
+	return
+}
+
+func BuildH265SeqHeaderFromVpsSpsPps(vps, sps, pps []byte) ([]byte, error) {
+	sh := make([]byte, 43+len(vps)+len(sps)+len(pps))
+
+	sh[0] =  0b1001_0000 | byte(PacketTypeSequenceStart)
+	util.BigEndian.PutUint32(sh[1:], FourCC_H265_32)
+	// unsigned int(8) configurationVersion = 1;
+	sh[5] = 0x1
+
+	ctx := HVCDecoderConfigurationRecord{
+		configurationVersion:             1,
+		lengthSizeMinusOne:               3, // 4 bytes
+		generalProfileCompatibilityFlags: 0xffffffff,
+		generalConstraintIndicatorFlags:  0xffffffffffff,
+	}
+	if err := ctx.ParseVps(vps); err != nil {
+		return nil, err
+	}
+	if err := ctx.ParseSps(sps); err != nil {
+		return nil, err
+	}
+
+	// unsigned int(2) general_profile_space;
+	// unsigned int(1) general_tier_flag;
+	// unsigned int(5) general_profile_idc;
+	sh[6] = ctx.generalProfileSpace<<6 | ctx.generalTierFlag<<5 | ctx.generalProfileIdc
+	// unsigned int(32) general_profile_compatibility_flags
+	util.PutBE(sh[7:7+4], ctx.generalProfileCompatibilityFlags)
+	// unsigned int(48) general_constraint_indicator_flags
+	util.PutBE(sh[11:11+4], uint32(ctx.generalConstraintIndicatorFlags>>16))
+	util.PutBE(sh[15:15+2], uint16(ctx.generalConstraintIndicatorFlags))
+	// unsigned int(8) general_level_idc;
+	sh[17] = ctx.generalLevelIdc
+
+	// bit(4) reserved = ‘1111’b;
+	// unsigned int(12) min_spatial_segmentation_idc;
+	// bit(6) reserved = ‘111111’b;
+	// unsigned int(2) parallelismType;
+	// TODO chef: 这两个字段没有解析
+	util.PutBE(sh[18:20], 0xf000)
+	sh[20] = ctx.parallelismType | 0xfc
+
+	// bit(6) reserved = ‘111111’b;
+	// unsigned int(2) chromaFormat;
+	sh[21] = ctx.chromaFormat | 0xfc
+
+	// bit(5) reserved = ‘11111’b;
+	// unsigned int(3) bitDepthLumaMinus8;
+	sh[22] = ctx.bitDepthLumaMinus8 | 0xf8
+
+	// bit(5) reserved = ‘11111’b;
+	// unsigned int(3) bitDepthChromaMinus8;
+	sh[23] = ctx.bitDepthChromaMinus8 | 0xf8
+
+	// bit(16) avgFrameRate;
+	util.PutBE(sh[24:26], ctx.avgFrameRate)
+
+	// bit(2) constantFrameRate;
+	// bit(3) numTemporalLayers;
+	// bit(1) temporalIdNested;
+	// unsigned int(2) lengthSizeMinusOne;
+	sh[26] = 0<<6 | ctx.numTemporalLayers<<3 | ctx.temporalIdNested<<2 | ctx.lengthSizeMinusOne
+
+	// num of vps sps pps
+	sh[27] = 0x03
+	i := 28
+	sh[i] = byte(NAL_UNIT_VPS)
+	// num of vps
+	util.PutBE(sh[i+1:i+3], 1)
+	// length
+	util.PutBE(sh[i+3:i+5], len(vps))
+	copy(sh[i+5:], vps)
+	i = i + 5 + len(vps)
+	sh[i] = byte(NAL_UNIT_SPS)
+	util.PutBE(sh[i+1:i+3], 1)
+	util.PutBE(sh[i+3:i+5], len(sps))
+	copy(sh[i+5:], sps)
+	i = i + 5 + len(sps)
+	sh[i] = byte(NAL_UNIT_PPS)
+	util.PutBE(sh[i+1:i+3], 1)
+	util.PutBE(sh[i+3:i+5], len(pps))
+	copy(sh[i+5:], pps)
+
+	return sh, nil
+}
+func (ctx *HVCDecoderConfigurationRecord) ParseVps(vps []byte) error {
+	if len(vps) < 2 {
+		return ErrHevc
+	}
+
+	rbsp := nal2rbsp(vps[2:])
+	br := nazabits.NewBitReader(rbsp)
+
+	// skip
+	// vps_video_parameter_set_id u(4)
+	// vps_reserved_three_2bits   u(2)
+	// vps_max_layers_minus1      u(6)
+	if _, err := br.ReadBits16(12); err != nil {
+		return ErrHevc
+	}
+
+	vpsMaxSubLayersMinus1, err := br.ReadBits8(3)
+	if err != nil {
+		return ErrHevc
+	}
+	if vpsMaxSubLayersMinus1+1 > ctx.numTemporalLayers {
+		ctx.numTemporalLayers = vpsMaxSubLayersMinus1 + 1
+	}
+
+	// skip
+	// vps_temporal_id_nesting_flag u(1)
+	// vps_reserved_0xffff_16bits   u(16)
+	if _, err := br.ReadBits32(17); err != nil {
+		return ErrHevc
+	}
+
+	return ctx.parsePtl(&br, vpsMaxSubLayersMinus1)
+}
+
+func (ctx *HVCDecoderConfigurationRecord) ParseSps(sps []byte) error {
+	var err error
+
+	if len(sps) < 2 {
+		return ErrHevc
+	}
+
+	rbsp := nal2rbsp(sps[2:])
+	br := nazabits.NewBitReader(rbsp)
+
+	// sps_video_parameter_set_id
+	if _, err = br.ReadBits8(4); err != nil {
+		return err
+	}
+
+	spsMaxSubLayersMinus1, err := br.ReadBits8(3)
+	if err != nil {
+		return err
+	}
+
+	if spsMaxSubLayersMinus1+1 > ctx.numTemporalLayers {
+		ctx.numTemporalLayers = spsMaxSubLayersMinus1 + 1
+	}
+
+	// sps_temporal_id_nesting_flag
+	if ctx.temporalIdNested, err = br.ReadBit(); err != nil {
+		return err
+	}
+
+	if err = ctx.parsePtl(&br, spsMaxSubLayersMinus1); err != nil {
+		return err
+	}
+
+	// sps_seq_parameter_set_id
+	if _, err = br.ReadGolomb(); err != nil {
+		return err
+	}
+
+	var cf uint32
+	if cf, err = br.ReadGolomb(); err != nil {
+		return err
+	}
+	ctx.chromaFormat = uint8(cf)
+	if ctx.chromaFormat == 3 {
+		if _, err = br.ReadBit(); err != nil {
+			return err
+		}
+	}
+
+	if ctx.PicWidthInLumaSamples, err = br.ReadGolomb(); err != nil {
+		return err
+	}
+	if ctx.PicHeightInLumaSamples, err = br.ReadGolomb(); err != nil {
+		return err
+	}
+
+	conformanceWindowFlag, err := br.ReadBit()
+	if err != nil {
+		return err
+	}
+	if conformanceWindowFlag != 0 {
+		if _, err = br.ReadGolomb(); err != nil {
+			return err
+		}
+		if _, err = br.ReadGolomb(); err != nil {
+			return err
+		}
+		if _, err = br.ReadGolomb(); err != nil {
+			return err
+		}
+		if _, err = br.ReadGolomb(); err != nil {
+			return err
+		}
+	}
+
+	var bdlm8 uint32
+	if bdlm8, err = br.ReadGolomb(); err != nil {
+		return err
+	}
+	ctx.bitDepthLumaMinus8 = uint8(bdlm8)
+	var bdcm8 uint32
+	if bdcm8, err = br.ReadGolomb(); err != nil {
+		return err
+	}
+	ctx.bitDepthChromaMinus8 = uint8(bdcm8)
+
+	_, err = br.ReadGolomb()
+	if err != nil {
+		return err
+	}
+	spsSubLayerOrderingInfoPresentFlag, err := br.ReadBit()
+	if err != nil {
+		return err
+	}
+	var i uint8
+	if spsSubLayerOrderingInfoPresentFlag != 0 {
+		i = 0
+	} else {
+		i = spsMaxSubLayersMinus1
+	}
+	for ; i <= spsMaxSubLayersMinus1; i++ {
+		if _, err = br.ReadGolomb(); err != nil {
+			return err
+		}
+		if _, err = br.ReadGolomb(); err != nil {
+			return err
+		}
+		if _, err = br.ReadGolomb(); err != nil {
+			return err
+		}
+	}
+
+	if _, err = br.ReadGolomb(); err != nil {
+		return err
+	}
+	if _, err = br.ReadGolomb(); err != nil {
+		return err
+	}
+	if _, err = br.ReadGolomb(); err != nil {
+		return err
+	}
+	if _, err = br.ReadGolomb(); err != nil {
+		return err
+	}
+	if _, err = br.ReadGolomb(); err != nil {
+		return err
+	}
+	if _, err = br.ReadGolomb(); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func (ctx *HVCDecoderConfigurationRecord) parsePtl(br *nazabits.BitReader, maxSubLayersMinus1 uint8) error {
+	var err error
+	var ptl HVCDecoderConfigurationRecord
+	if ptl.generalProfileSpace, err = br.ReadBits8(2); err != nil {
+		return err
+	}
+	if ptl.generalTierFlag, err = br.ReadBit(); err != nil {
+		return err
+	}
+	if ptl.generalProfileIdc, err = br.ReadBits8(5); err != nil {
+		return err
+	}
+	if ptl.generalProfileCompatibilityFlags, err = br.ReadBits32(32); err != nil {
+		return err
+	}
+	if ptl.generalConstraintIndicatorFlags, err = br.ReadBits64(48); err != nil {
+		return err
+	}
+	if ptl.generalLevelIdc, err = br.ReadBits8(8); err != nil {
+		return err
+	}
+	ctx.updatePtl(&ptl)
+
+	if maxSubLayersMinus1 == 0 {
+		return nil
+	}
+
+	subLayerProfilePresentFlag := make([]uint8, maxSubLayersMinus1)
+	subLayerLevelPresentFlag := make([]uint8, maxSubLayersMinus1)
+	for i := uint8(0); i < maxSubLayersMinus1; i++ {
+		if subLayerProfilePresentFlag[i], err = br.ReadBit(); err != nil {
+			return err
+		}
+		if subLayerLevelPresentFlag[i], err = br.ReadBit(); err != nil {
+			return err
+		}
+	}
+	if maxSubLayersMinus1 > 0 {
+		for i := maxSubLayersMinus1; i < 8; i++ {
+			if _, err = br.ReadBits8(2); err != nil {
+				return err
+			}
+		}
+	}
+
+	for i := uint8(0); i < maxSubLayersMinus1; i++ {
+		if subLayerProfilePresentFlag[i] != 0 {
+			if _, err = br.ReadBits32(32); err != nil {
+				return err
+			}
+			if _, err = br.ReadBits32(32); err != nil {
+				return err
+			}
+			if _, err = br.ReadBits32(24); err != nil {
+				return err
+			}
+		}
+
+		if subLayerLevelPresentFlag[i] != 0 {
+			if _, err = br.ReadBits8(8); err != nil {
+				return err
+			}
+		}
+	}
+
+	return nil
+}
+
+func (ctx *HVCDecoderConfigurationRecord) updatePtl(ptl *HVCDecoderConfigurationRecord) {
+	ctx.generalProfileSpace = ptl.generalProfileSpace
+
+	if ptl.generalTierFlag > ctx.generalTierFlag {
+		ctx.generalLevelIdc = ptl.generalLevelIdc
+
+		ctx.generalTierFlag = ptl.generalTierFlag
+	} else {
+		if ptl.generalLevelIdc > ctx.generalLevelIdc {
+			ctx.generalLevelIdc = ptl.generalLevelIdc
+		}
+	}
+
+	if ptl.generalProfileIdc > ctx.generalProfileIdc {
+		ctx.generalProfileIdc = ptl.generalProfileIdc
+	}
+
+	ctx.generalProfileCompatibilityFlags &= ptl.generalProfileCompatibilityFlags
+
+	ctx.generalConstraintIndicatorFlags &= ptl.generalConstraintIndicatorFlags
+}
+
+func nal2rbsp(nal []byte) []byte {
+	// TODO chef:
+	// 1. 输出应该可由外部申请
+	// 2. 替换性能
+	// 3. 该函数应该放入avc中
+	return bytes.Replace(nal, []byte{0x0, 0x0, 0x3}, []byte{0x0, 0x0}, -1)
+}

+ 2417 - 0
m7s.live/engine/v4@v4.14.6/codec/mp4.go

@@ -0,0 +1,2417 @@
+package codec
+
+import "m7s.live/engine/v4/util"
+
+type MP4 interface {
+}
+
+type MP4Box interface {
+	Header() *MP4Header
+	Body() *MP4Body
+}
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/17
+//
+// The standard boxes all use compact types (32-bit) and most boxes will use the compact (32-bit) size
+// standard header
+type MP4BoxHeader struct {
+	BoxSize uint32 // 32 bits, is an integer that specifies the number of bytes in this box, including all its fields and contained boxes; if size is 1 then the actual size is in the field largesize; if size is 0, then this box is the last one in the file, and its contents extend to the end of the file (normally only used for a Media Data Box)
+	BoxType uint32 // 32 bits, identifies the box type; standard boxes use a compact type, which is normally four printable characters, to permit ease of identification, and is shown so in the boxes below. User extensions use an extended type; in this case, the type field is set to ‘uuid’.
+}
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/17
+//
+// Many objects also contain a version number and flags field
+// full box header
+type MP4FullBoxHeader struct {
+	Version uint8   // 8 bits, is an integer that specifies the version of this format of the box.
+	Flags   [3]byte // 24 bits, is a map of flags
+}
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/17
+//
+// Typically only the Media Data Box(es) need the 64-bit size.
+// lagesize box header
+type MP4BoxLargeHeader struct {
+	LargeSize uint64    // 64 bits
+	UUIDs     [16]uint8 // 128 bits
+}
+
+// if(size == 1)
+// {
+// 	unsigned int(64) largesize;
+// }
+// else if(size == 0)
+// {
+// 	// box extends to end of file
+// }
+// if(boxtype == ‘uuid’)
+// {
+// 	unsigned int(8)[16] usertype = extended_type;
+// }
+
+type MP4Header struct {
+	MP4BoxHeader
+}
+
+type MP4Body struct{}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/18
+//
+// Box Type: ftyp
+// Container: File
+// Mandatory: Yes
+// Quantity: Exactly one (but see below)
+//
+// Each brand is a printable four-character code, registered with ISO, that identifies a precise specification
+type FileTypeBox struct {
+	MP4BoxHeader // standard header
+
+	MajorBrand       uint32   // 32 bits, is a brand identifier
+	MinorVersion     uint32   // 32 bits, is an informative integer for the minor version of the major brand
+	CompatibleBrands []uint32 // 32 bits array, is a list, to the end of the box, of brands
+}
+
+func NewFileTypeBox() (box *FileTypeBox) {
+	box = new(FileTypeBox)
+	util.GetBE([]byte("ftyp"), &box.MP4BoxHeader.BoxType)
+	return
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/30
+//
+// Box Types: pdin
+// Container: File
+// Mandatory: No
+// Quantity: Zero or One
+type ProgressiveDownloadInformationBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	Rate         uint32 // 32 bits, is a download rate expressed in bytes/second
+	InitialDelay uint32 // 32 bits, is the suggested delay to use when playing the file, such that if download continues at the given rate, all data within the file will arrive in time for its use and playback should not need to stall.
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/30
+//
+// Box Type: moov
+// Container: File
+// Mandatory: Yes
+// Quantity: Exactly one
+//
+// The metadata for a presentation is stored in the single Movie Box which occurs at the top-level of a file.
+// Normally this box is close to the beginning or end of the file, though this is not required
+type MovieBox struct {
+	MP4BoxHeader // standard header
+
+	//Mhb MovieHeaderBox // the first child box(header box)
+}
+
+func NewMovieBox() (box *MovieBox) {
+	box = new(MovieBox)
+	util.GetBE([]byte("moov"), &box.MP4BoxHeader.BoxType)
+	return
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/31
+//
+// Box Type: mvhd
+// Container: Movie Box ('moov')
+// Mandatory: Yes
+// Quantity: Exactly one
+//
+// This box defines overall information which is media-independent, and relevant to the entire presentation
+// considered as a whole
+type MovieHeaderBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	CreationTime     interface{} // uint64 or uint32, is an integer that declares the creation time of the presentation (in seconds since midnight, Jan. 1, 1904, in UTC time)
+	ModificationTime interface{} // uint64 or uint32, is an integer that declares the most recent time the presentation was modified (in seconds since midnight, Jan. 1, 1904, in UTC time)
+	TimeScale        uint32      // 32 bits, is an integer that specifies the time-scale for the entire presentation; this is the number of time units that pass in one second. For example, a time coordinate system that measures time in sixtieths of a second has a time scale of 60.
+	Duration         interface{} // uint64 or uint32, is an integer that declares length of the presentation (in the indicated timescale). This property is derived from the presentation's tracks: the value of this field corresponds to the duration of the longest track in the presentation. If the duration cannot be determined then duration is set to all 1s.
+	Rate             int32       // 32 bits, is a fixed point 16.16 number that indicates the preferred rate to play the presentation; 1.0 (0x00010000) is normal forward playback
+	Volume           int16       // 16 bits, is a fixed point 8.8 number that indicates the preferred playback volume. 1.0 (0x0100) is full volume.
+	Reserved1        int16       // 16 bits, bit[16]
+	Reserved2        [2]uint32   // 32 bits array, const unsigned int(32)[2]
+	Matrix           [9]int32    // 32 bits array, provides a transformation matrix for the video; (u,v,w) are restricted here to (0,0,1), hex values(0,0,0x40000000).
+	PreDefined       [6]int32    // 32 bits array, bit(32)[6]
+	NextTrackID      uint32      // 32 bits, is a non-zero integer that indicates a value to use for the track ID of the next track to be added to this presentation. Zero is not a valid track ID value. The value of next_track_ID shall be larger than the largest track-ID in use. If this value is equal to all 1s (32-bit maxint), and a new media track is to be added, then a search must be made in the file for an unused track identifier.
+}
+
+// CreationTime 	: 创建时间(相对于UTC时间1904-01-01零点的秒数)
+// ModificationTime : 修改时间
+// TimeScale 		: 文件媒体在1秒时间内的刻度值,可以理解为1秒长度的时间单元数
+// Duration 		: 该track的时间长度,用duration和time scale值可以计算track时长,比如audio track的time scale = 8000, duration = 560128,时长为70.016,video track的time scale = 600, duration = 42000,时长为70
+// Rate 			: 推荐播放速率,高16位和低16位分别为小数点整数部分和小数部分,即[16.16] 格式,该值为1.0(0x00010000)表示正常前向播放
+// Volume 			: 与rate类似,[8.8] 格式,1.0(0x0100)表示最大音量
+// Matrix 			: 视频变换矩阵 { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 }
+// NextTrackID 		: 下一个track使用的id号
+
+// PreDefined:
+// Preview Time 		: 开始预览此movie的时间
+// Preview Duration 	: 以movie的time scale为单位,预览的duration
+// Poster Time 			: The time value of the time of the movie poster.
+// Selection Time 		: The time value for the start time of the current selection.
+// Selection Duration 	: The duration of the current selection in movie time scale units.
+// Current Time 		: 当前时间
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/32
+//
+// Box Type: trak
+// Container: Movie Box ('moov')
+// Mandatory: Yes
+// Quantity: One or more
+type TrackBox struct {
+	MP4BoxHeader // standard header
+
+	Thb TrackHeaderBox // the first child box(header box)
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/32
+//
+// Box Type: tkhd
+// Container: Track Box ('trak')
+// Mandatory: Yes
+// Quantity: Exactly one
+type TrackHeaderBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	CreationTime     interface{} // uint64 or uint32,
+	ModificationTime interface{} // uint64 or uint32,
+	TrackID          uint32      // 32 bits, is an integer that uniquely identifies this track over the entire life-time of this presentation. Track IDs are never re-used and cannot be zero
+	Reserved1        uint32      // 32 bits,
+	Duration         interface{} // uint64 or uint32,
+	Reserved2        [2]uint32   // 32 bits array,
+	Layer            int16       // 16 bits, specifies the front-to-back ordering of video tracks; tracks with lower numbers are closer to the viewer. 0 is the normal value, and -1 would be in front of track 0, and so on
+	AlternateGroup   int16       // 16 bits,
+	Volume           int16       // 16 bits, if track_is_audio 0x0100 else 0
+	Reserved3        uint16      // 16 bits,
+	Matrix           [9]int32    // 32 bits array, provides a transformation matrix for the video; (u,v,w) are restricted here to (0,0,1), hex (0,0,0x40000000). { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 }
+	Width            uint32      // 32 bits,
+	Height           uint32      // 32 bits,
+}
+
+// CreationTime     : 创建时间
+// ModificationTime : 修改时间
+// TrackID          : id号,不能重复且不能为0
+// Reserved1        : 保留位
+// Duration         : track的时间长度
+// Reserved2        : 保留位
+// Layer            : 视频层,默认为0,值小的在上层
+// AlternateGroup   : track分组信息,默认为0表示该track未与其他track有群组关系
+// Volume           : [8.8] 格式,如果为音频track,1.0(0x0100)表示最大音量;否则为0
+// Reserved3        : 保留位
+// Matrix           : 视频变换矩阵 { 0x00010000,0,0,0,0x00010000,0,0,0,0x40000000 }
+// Width            : 宽
+// Height           : 高,均为 [16.16] 格式值,与sample描述中的实际画面大小比值,用于播放时的展示宽高
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/34
+//
+// Box Type: tref
+// Container: Track Box (‘trak’)
+// Mandatory: No
+// Quantity: Zero or one
+type TrackReferenceBox struct {
+	MP4BoxHeader // standard header
+}
+
+type TrackReferenceTypeBox struct {
+	MP4BoxHeader // standard header
+
+	TrackIDs []uint32 // 32 bits, is an integer that provides a reference from the containing track to another track in the presentation. track_IDs are never re-used and cannot be equal to zero
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/35
+//
+// Box Type: trgr
+// Container: Track Box (‘trak’)
+// Mandatory: No
+// Quantity: Zero or one
+type TrackGroupBox struct {
+	MP4BoxHeader // standard header
+}
+
+type TrackGroupTypeBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	TrackGroupID uint32 // 32 bits, indicates the grouping type and shall be set to one of the following values, or a value registered, or a value from a derived specification or registration
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/54
+//
+// Box Type: edts
+// Container: Track Box (‘trak’)
+// Mandatory: No
+// Quantity: Zero or one
+type EditBox struct {
+	MP4BoxHeader // standard header
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/54
+//
+// Box Type: elst
+// Container: Edit Box (‘edts’)
+// Mandatory: No
+// Quantity: Zero or one
+type EditListBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	EntryCount uint32          // 32 bits, is an integer that gives the number of entries in the following table
+	Tables     []EditListTable // Edit List Table
+}
+
+type EditListTable struct {
+	SegmentDuration   interface{} // uint64 or uint32, is an integer that specifies the duration of this edit segment in units of the timescale in the Movie Header Box
+	MediaTime         interface{} // uint64 or uint32, is an integer containing the starting time within the media of this edit segment (in media time scale units, in composition time). If this field is set to –1, it is an empty edit. The last edit in a track shall never be an empty edit. Any difference between the duration in the Movie Header Box, and the track’s duration is expressed as an implicit empty edit at the end.
+	MediaRateInteger  int16       // 16 bits,
+	MediaRateFraction int16       // 16 bits,
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/36
+//
+// Box Type: mdia
+// Container: Track Box ('trak')
+// Mandatory: Yes
+// Quantity: Exactly one
+//
+// The media declaration container contains all the objects that declare information about the media data within a track.
+type MediaBox struct {
+	MP4BoxHeader // standard header
+
+	Mhb MediaHeaderBox // the first child box(header box)
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/36
+//
+// Box Type: mdhd
+// Container: Media Box ('mdia')
+// Mandatory: Yes
+// Quantity: Exactly one
+//
+// The media header declares overall information that is media-independent, and relevant to characteristics of the media in a track.
+type MediaHeaderBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	CreationTime     interface{} // int64 or int32, is an integer that declares the creation time of the presentation (in seconds since midnight, Jan. 1, 1904, in UTC time)
+	ModificationTime interface{} // int64 or int32, is an integer that declares the most recent time the presentation was modified (in seconds since midnight, Jan. 1, 1904, in UTC time)
+	TimeScale        uint32      // 32 bits, is an integer that specifies the time-scale for the entire presentation; this is the number of time units that pass in one second. For example, a time coordinate system that measures time in sixtieths of a second has a time scale of 60.
+	Duration         interface{} // int64 or int32, is an integer that declares length of the presentation (in the indicated timescale). This property is derived from the presentation's tracks: the value of this field corresponds to the duration of the longest track in the presentation. If the duration cannot be determined then duration is set to all 1s.
+	Pad              byte        // 1 bit,
+	Language         [2]byte     // 15 bits, unsigned int(5)[3], declares the language code for this media. See ISO 639-2/T for the set of three charactercodes. Each character is packed as the difference between its ASCII value and 0x60. Since the code is confined to being three lower-case letters, these values are strictly positive
+	PreDefined       uint16      // 16 bits,
+}
+
+// Language		: 媒体的语言码
+// PreDefined	: 媒体的回放质量???怎样生成此质量,什么是参照点
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/37
+//
+// Box Type: hdlr
+// Container: Media Box ('mdia') or Meta Box ('meta')
+// Mandatory: Yes
+// Quantity: Exactly one
+type HandlerBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	PreDefined  uint32    // 32 bits,
+	HandlerType uint32    // 32 bits, when present in a meta box, contains an appropriate value to indicate the format of the meta box contents. The value 'null' can be used in the primary meta box to indicate that it is merely being used to hold resources
+	Reserved    [3]uint32 // 32 bits,
+	Name        string    // string, is a null-terminated string in UTF-8 characters which gives a human-readable name for the track type (for debugging and inspection purposes).
+}
+
+// handler_type when present in a media box, is an integer containing one of the following values, or a value from a derived specification:
+// 'vide' Video track
+// 'soun' Audio track
+// 'hint' Hint track
+// 'meta' Timed Metadata track
+// 'auxv' Auxiliary Video track
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/38
+//
+// Box Type: minf
+// Container: Media Box ('mdia')
+// Mandatory: Yes
+// Quantity: Exactly one
+//
+// This box contains all the objects that declare characteristic information of the media in the track.
+type MediaInformationBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/38
+//
+// Box Types: vmhd, smhd, hmhd, nmhd
+// Container: Media Information Box (‘minf’)
+// Mandatory: Yes
+// Quantity: Exactly one specific media header shall be present
+//
+// There is a different media information header for each track type (corresponding to the media handler-type);
+// the matching header shall be present, which may be one of those defined here, or one defined in a derived specification
+type MediaInformationHeaderBoxes struct {
+	// VideoMediaHeaderBox
+	//
+}
+
+// Box Types: vmhd
+// The video media header contains general presentation information, independent of the coding, for video media.
+// Note that the flags field has the value 1.
+type VideoMediaHeaderBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	GraphicsMode uint16    // 16 bits, specifies a composition mode for this video track, from the following enumerated set, which may be extended by derived specifications: copy = 0 copy over the existing image
+	Opcolor      [3]uint16 // 16 bits array, is a set of 3 colour values (red, green, blue) available for use by graphics modes
+}
+
+// Box Types: smhd
+// The sound media header contains general presentation information, independent of the coding, for audio media.
+// This header is used for all tracks containing audio.
+type SoundMediaHeaderBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	Balance  int16  // 16 bits, is a fixed-point 8.8 number that places mono audio tracks in a stereo space; 0 is centre (the normal value); full left is -1.0 and full right is 1.0
+	Reserved uint16 // 16 bits,
+}
+
+// Box Types: hmhd
+// The hint media header contains general information, independent of the protocol, for hint tracks.
+// (A PDU is a Protocol Data Unit.)
+type HintMediaHeaderBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	MaxPDUSize uint16 // 16 bits, gives the size in bytes of the largest PDU in this (hint) stream
+	AvgPDUSize uint16 // 16 bits, gives the average size of a PDU over the entire presentation
+	MaxBitrate uint32 // 32 bits, gives the maximum rate in bits/second over any window of one second
+	AvgBitrate uint32 // 32 bits, gives the average rate in bits/second over the entire presentation
+	Reserved   uint32 // 32 bits,
+}
+
+// Box Types: nmhd
+// Streams other than visual and audio (e.g., timed metadata streams) may use a null Media Header Box, as defined here.
+type NullMediaHeaderBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/56
+//
+// Box Type: dinf
+// Container: Media Information Box ('minf') or Meta Box ('meta')
+// Mandatory: Yes (required within 'minf' box) and No (optional within 'meta' box)
+// Quantity: Exactly one
+//
+// The data information box contains objects that declare the location of the media information in a track
+type DataInformationBox struct {
+	MP4BoxHeader // standard header
+}
+
+// -------------------------------------------------------------------------------------------------------
+//
+// ISO_IEC_14496-12_2012.pdf Page/56
+//
+// Box Types: url, urn, dref
+// Container: Data Information Box ('dinf')
+// Mandatory: Yes
+// Quantity: Exactly one
+type DataReferenceBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	EntryCount uint32      // 32 bits, is an integer that gives the number of entries in the following table
+	DataEntry  interface{} // DataEntryUrlBox or DataEntryUrnBox.
+}
+
+// aligned(8) class DataReferenceBox
+// 	extends FullBox('dref', version = 0, 0) {
+// 	unsigned int(32) entry_count;
+// 	for (i=1; i <= entry_count; i++) {
+// 		DataEntryBox(entry_version, entry_flags) data_entry;
+// 	}
+// }
+
+type DataEntryUrlBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	Location string // string,
+}
+
+type DataEntryUrnBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	Name     string // string,
+	Location string // string,
+}
+
+// -------------------------------------------------------------------------------------------------------
+//
+// ISO_IEC_14496-12_2012.pdf Page/40
+//
+// Box Type: stbl
+// Container: Media Information Box ('minf')
+// Mandatory: Yes
+// Quantity: Exactly one
+type SampleTableBox struct {
+	MP4BoxHeader // standard header
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/40
+//
+// Box Types: stsd
+// Container: Sample Table Box ('stbl')
+// Mandatory: Yes
+// Quantity: Exactly one
+type SampleDescriptionBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	EntryCount uint32 // 32 bits, is an integer that gives the number of entries in the following table
+}
+
+// for (i = 1 ; i <= entry_count ; i++) {
+// 	switch (handler_type){
+// 		case ‘soun’: // for audio tracks
+// 			AudioSampleEntry();
+// 			break;
+// 		case ‘vide’: // for video tracks
+// 			VisualSampleEntry();
+// 			break;
+// 		case ‘hint’: // Hint track
+// 			HintSampleEntry();
+// 			break;
+// 		case ‘meta’: // Metadata track
+// 			MetadataSampleEntry();
+// 			break;
+// 	}
+// }
+
+// box header和version字段后会有一个entry count字段,根据entry的个数,每个entry会有type信息,如“vide”、“sund”等,
+// 根据type不同sample description会提供不同的信息,例如对于video track,会有“VisualSampleEntry”类型信息,
+// 对于audio track会有“AudioSampleEntry”类型信息.
+// 视频的编码类型、宽高、长度,音频的声道、采样等信息都会出现在这个box中
+
+// is the appropriate sample entry
+type SampleEntry struct {
+	Reserved           [6]uint8 // 48 bits,
+	DataReferenceIndex uint16   // 16 bits, is an integer that contains the index of the data reference to use to retrieve data associated with samples that use this sample description. Data references are stored in Data Reference Boxes. The index ranges from 1 to the number of data references.
+}
+
+type HintSampleEntry struct {
+	Data []uint8 // 8 bits array,
+}
+
+// Box Types: btrt
+type BitRateBox struct {
+	MP4BoxHeader // standard header
+
+	BufferSizeDB uint32 // 32 bits, gives the size of the decoding buffer for the elementary stream in bytes.
+	MaxBitrate   uint32 // 32 bits, gives the maximum rate in bits/second over any window of one second.
+	AvgBitrate   uint32 // 32 bits, gives the average rate in bits/second over the entire presentation.
+}
+
+type MetaDataSampleEntry struct{}
+
+type XMLMetaDataSampleEntry struct {
+	ContentEncoding string     // optional, is a null-terminated string in UTF-8 characters, and provides a MIME type which identifies the content encoding of the timed metadata
+	NameSpace       string     // string, gives the namespace of the schema for the timed XML metadata
+	SchemaLocation  string     // optional, optionally provides an URL to find the schema corresponding to the namespace. This is needed for decoding of the timed metadata by XML aware encoding mechanisms such as BiM.
+	Brb             BitRateBox // optional
+}
+
+type TextMetaDataSampleEntry struct {
+	ContentEncoding string     // optional, is a null-terminated string in UTF-8 characters, and provides a MIME type which identifies the content encoding of the timed metadata
+	MimeFormat      string     // string, provides a MIME type which identifies the content format of the timed metadata. Examples for this field are ‘text/html’ and ‘text/plain’.
+	Brb             BitRateBox // optional
+}
+
+type URIBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	TheURI string // string, is a URI formatted according to the rules in 6.2.4
+}
+
+type URIInitBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	UriInitializationData []uint8 // 8 bits array,  is opaque data whose form is defined in the documentation of the URI form.
+}
+
+type URIMetaSampleEntry struct {
+	TheLabel URIBox
+	Init     URIInitBox // optional
+	//Mpeg4    MPEG4BitRateBox // optional
+}
+
+// Box Types: pasp
+type PixelAspectRatioBox struct {
+	MP4BoxHeader // standard header
+
+	HSpacing uint32 // 32 bits, define the relative width and height of a pixel;
+	VSpacing uint32 // 32 bits, define the relative width and height of a pixel;
+}
+
+// Box Types: clap
+// Visual Sequences
+type CleanApertureBox struct {
+	MP4BoxHeader // standard header
+
+	CleanApertureWidthN  uint32 // 32 bits, a fractional number which defines the exact clean aperture width, in counted pixels, of the video image
+	CleanApertureWidthD  uint32 // 32 bits, a fractional number which defines the exact clean aperture width, in counted pixels, of the video image
+	CleanApertureHeightN uint32 // 32 bits, a fractional number which defines the exact clean aperture height, in counted pixels, of the video image
+	CleanApertureHeightD uint32 // 32 bits, a fractional number which defines the exact clean aperture height, in counted pixels, of the video image
+	HorizOffN            uint32 // 32 bits, a fractional number which defines the horizontal offset of clean aperture centre minus (width-1)/2. Typically 0
+	HorizOffD            uint32 // 32 bits, a fractional number which defines the horizontal offset of clean aperture centre minus (width-1)/2. Typically 0
+	VertOffN             uint32 // 32 bits, a fractional number which defines the vertical offset of clean aperture centre minus (height-1)/2. Typically 0
+	VertOffD             uint32 // 32 bits, a fractional number which defines the vertical offset of clean aperture centre minus (height-1)/2. Typically 0
+}
+
+// Box Types: colr
+type ColourInformationBox struct {
+	MP4BoxHeader // standard header
+
+	ColourType uint32 // 32 bits, an indication of the type of colour information supplied. For colour_type ‘nclx’: these fields are exactly the four bytes defined for PTM_COLOR_INFO( ) in A.7.2 of ISO/IEC 29199-2 but note that the full range flag is here in a different bit position
+}
+
+// if (colour_type == ‘nclx’) /* on-screen colours */
+// {
+// 	unsigned int(16) colour_primaries;
+// 	unsigned int(16) transfer_characteristics;
+// 	unsigned int(16) matrix_coefficients;
+// 	unsigned int(1) full_range_flag;
+// 	unsigned int(7) reserved = 0;
+// }
+// else if (colour_type == ‘rICC’)
+// {
+// 	ICC_profile; // restricted ICC profile
+// }
+// else if (colour_type == ‘prof’)
+// {
+// 	ICC_profile; // unrestricted ICC profile
+// }
+
+// ICC_profile : an ICC profile as defined in ISO 15076-1 or ICC.1:2010 is supplied.
+
+type VisualSampleEntry struct {
+	PreDefined1     uint16              // 16 bits,
+	Reserved1       uint16              // 16 bits,
+	PreDefined2     [3]uint32           // 96 bits,
+	Width           uint16              // 16 bits, are the maximum visual width and height of the stream described by this sample description, in pixels
+	Height          uint16              // 16 bits, are the maximum visual width and height of the stream described by this sample description, in pixels
+	HorizreSolution uint32              // 32 bits, fields give the resolution of the image in pixels-per-inch, as a fixed 16.16 number
+	VertreSolution  uint32              // 32 bits, fields give the resolution of the image in pixels-per-inch, as a fixed 16.16 number
+	Reserved3       uint32              // 32 bits,
+	FrameCount      uint16              // 16 bits, indicates how many frames of compressed video are stored in each sample. The default is 1, for one frame per sample; it may be more than 1 for multiple frames per sample
+	CompressorName  [32]string          // 32 string, is a name, for informative purposes. It is formatted in a fixed 32-byte field, with the first byte set to the number of bytes to be displayed, followed by that number of bytes of displayable data, and then padding to complete 32 bytes total (including the size byte). The field may be set to 0.
+	Depth           uint16              // 16 bits, takes one of the following values 0x0018 – images are in colour with no alpha
+	PreDefined3     int16               // 16 bits,
+	Cab             CleanApertureBox    // optional, other boxes from derived specifications
+	Parb            PixelAspectRatioBox // optional, other boxes from derived specifications
+}
+
+// Audio Sequences
+type AudioSampleEntry struct {
+	Reserved1    [2]uint32 // 32 bits array,
+	ChannelCount uint16    // 16 bits, is the number of channels such as 1 (mono) or 2 (stereo)
+	SampleSize   uint16    // 16 bits, is in bits, and takes the default value of 16
+	PreDefined   uint16    // 16 bits,
+	Reserved2    uint16    // 16 bits,
+	SampleRate   uint32    // 32 bits, is the sampling rate expressed as a 16.16 fixed-point number (hi.lo)
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/48
+//
+// Box Type: stts
+// Container: Sample Table Box ('stbl')
+// Mandatory: Yes
+// Quantity: Exactly one
+type TimeToSampleBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	EntryCount uint32              // 32 bits, is an integer that gives the number of entries in the following table
+	Table      []TimeToSampleTable // Time To Sample Table , EntryCount elements
+}
+
+type TimeToSampleTable struct {
+	SampleCount []uint32 // 32 bits, is an integer that counts the number of consecutive samples that have the given duration
+	SampleDelta []uint32 // 32 bits, is an integer that gives the delta of these samples in the time-scale of the media.
+}
+
+// “stts”存储了sample的duration,描述了sample时序的映射方法,我们通过它可以找到任何时间的sample.
+// “stts”可以包含一个压缩的表来映射时间和sample序号,用其他的表来提供每个sample的长度和指针.
+// 表中每个条目提供了在同一个时间偏移量里面连续的sample序号,以及samples的偏移量.
+// 递增这些偏移量,就可以建立一个完整的time to sample表
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/49
+//
+// Box Type: ctts
+// Container: Sample Table Box (‘stbl’)
+// Mandatory: No
+// Quantity: Zero or one
+type CompositionOffsetBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	EntryCount uint32                   // 32 bits, is an integer that gives the number of entries in the following table
+	Table      []CompositionOffsetTable // Composition Offset Table, EntryCount elements.
+}
+
+type CompositionOffsetTable struct {
+	SampleCount  uint32      // 32 bits, is an integer that counts the number of consecutive samples that have the given offset.
+	SampleOffset interface{} // int32 or uint32, is an integer that gives the offset between CT and DT, such that CT(n) = DT(n) + CTTS(n).
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/50
+//
+// Box Type: cslg
+// Container: Sample Table Box (‘stbl’)
+// Mandatory: No
+// Quantity: Zero or one
+type CompositionToDecodeBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	CompositionToDTSShift        int32 // 32 bits, signed, if this value is added to the composition times (as calculated by the CTS offsets from the DTS), then for all samples, their CTS is guaranteed to be greater than or equal to their DTS, and the buffer model implied by the indicated profile/level will be honoured; if leastDecodeToDisplayDelta is positive or zero, this field can be 0; otherwise it should be at least (- leastDecodeToDisplayDelta)
+	LeastDecodeToDisplayDelta    int32 // 32 bits, signed, the smallest composition offset in the CompositionTimeToSample box in this track
+	GreatestDecodeToDisplayDelta int32 // 32 bits, signed, the largest composition offset in the CompositionTimeToSample box in this track
+	CompositionStartTime         int32 // 32 bits, signed, the smallest computed composition time (CTS) for any sample in the media of this track
+	CompositionEndTime           int32 // 32 bits, signed, the composition time plus the composition duration, of the sample with the largest computed composition time (CTS) in the media of this track; if this field takes the value 0, the composition end time is unknown.
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/58
+//
+// Box Type: stsc
+// Container: Sample Table Box ('stbl')
+// Mandatory: Yes
+// Quantity: Exactly one
+type SampleToChunkBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	EntryCount uint32               // 32 bits, is an integer that gives the number of entries in the following table
+	Table      []SampleToChunkTable // Sample To Chunk Table, entry count elements.
+}
+
+type SampleToChunkTable struct {
+	FirstChunk             []uint32 // 32 bits, is an integer that gives the index of the first chunk in this run of chunks that share the same samples-per-chunk and sample-description-index; the index of the first chunk in a track has the value 1 (the first_chunk field in the first record of this box has the value 1, identifying that the first sample maps to the first chunk).
+	SamplesPerChunk        []uint32 // 32 bits, is an integer that gives the number of samples in each of these chunks
+	SampleDescriptionIndex []uint32 // 32 bits, is an integer that gives the index of the sample entry that describes the samples in this chunk. The index ranges from 1 to the number of sample entries in the Sample Description Box
+}
+
+// 用chunk组织sample可以方便优化数据获取,一个thunk包含一个或多个sample.
+// “stsc”中用一个表描述了sample与chunk的映射关系,查看这张表就可以找到包含指定sample的thunk,从而找到这个sample
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/57
+//
+// Box Type: stsz, stz2
+// Container: Sample Table Box (‘stbl’)
+// Mandatory: Yes
+// Quantity: Exactly one variant must be present
+type SampleSizeBoxes struct{}
+
+// Box Type: stsz
+type SampleSizeBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	SampleSize  uint32      // 32 bits, is integer specifying the default sample size. If all the samples are the same size, this field contains that size value. If this field is set to 0, then the samples have different sizes, and those sizes are stored in the sample size table. If this field is not 0, it specifies the constant sample size, and no array follows.
+	SampleCount uint32      // 32 bits, is an integer that gives the number of samples in the track; if sample-size is 0, then it is also the number of entries in the following table.
+	EntrySize   interface{} // 32 bits array, SampleCount elements, is an integer specifying the size of a sample, indexed by its number.
+}
+
+// if (sample_size	==	0) {
+// 	for (i = 1; i <= sample_count; i++) {
+// 		unsigned int(32) entry_size;
+// 	}
+// }
+
+// Box Type: stz2
+type CompactSampleSizeBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	Reserved    [3]uint8    // 24 bits,
+	FieldSize   uint8       // 8 bits, is an integer specifying the size in bits of the entries in the following table; it shall take the value 4, 8 or 16. If the value 4 is used, then each byte contains two values: entry[i]<<4 + entry[i+1]; if the sizes do not fill an integral number of bytes, the last byte is padded with zeros.
+	SampleCount uint32      // 32 bits,  is an integer that gives the number of entries in the following table
+	EntrySize   interface{} //
+}
+
+// for (i = 1; i <= sample_count; i++) {
+// 	unsigned int(field_size) entry_size;
+// }
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/59
+//
+// Box Type: stco, co64
+// Container: Sample Table Box (‘stbl’)
+// Mandatory: Yes
+// Quantity: Exactly one variant must be present
+type ChunkOffsetBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	EntryCount  uint32   // 32 bits, is an integer that gives the number of entries in the following table
+	ChunkOffset []uint32 // 32 bits array, entry count elements.
+}
+
+// “stco”定义了每个thunk在媒体流中的位置。位置有两种可能,32位的和64位的,后者对非常大的电影很有用。
+// 在一个表中只会有一种可能,这个位置是在整个文件中的,而不是在任何box中的,这样做就可以直接在文件中找到媒体数据,
+// 而不用解释box。需要注意的是一旦前面的box有了任何改变,这张表都要重新建立,因为位置信息已经改变了
+
+// Box Type: co64
+type ChunkLargeOffsetBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	EntryCount  uint32   // 32 bits, is an integer that gives the number of entries in the following table
+	ChunkOffset []uint64 // 64 bits array, entry count elements.
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/51
+//
+// Box Type: stss
+// Container: Sample Table Box (‘stbl’)
+// Mandatory: No
+// Quantity: Zero or one
+//
+// This box provides a compact marking of the sync samples within the stream. The table is arranged in strictly increasing order of sample number.
+// If the sync sample box is not present, every sample is a sync sample.
+type SyncSampleBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	EntryCount   uint32   // 32 bits, is an integer that gives the number of entries in the following table. If entry_count is zero, there are no sync samples within the stream and the following table is empty
+	SampleNumber []uint32 // 32 bits array, entry count elements. gives the numbers of the samples that are sync samples in the stream.
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/52
+//
+// Box Type: stsh
+// Container: Sample Table Box (‘stbl’)
+// Mandatory: No
+// Quantity: Zero or one
+type ShadowSyncSampleBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	EntryCount uint32                  // 32 bits, is an integer that gives the number of entries in the following table.
+	Table      []ShadowSyncSampleTable // Shadow Sync Sample Table, entry count elements.
+}
+
+type ShadowSyncSampleTable struct {
+	ShadowedSampleNumber uint32 // 32 bits, gives the number of a sample for which there is an alternative sync sample.
+	SyncSampleNumber     uint32 // 32 bits, gives the number of the alternative sync sample.
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/60
+//
+// Box Type: padb
+// Container: Sample Table (‘stbl’)
+// Mandatory: No
+// Quantity: Zero or one
+//
+// In some streams the media samples do not occupy all bits of the bytes given by the sample size, and are
+// padded at the end to a byte boundary. In some cases, it is necessary to record externally the number of
+// padding bits used. This table supplies that information.
+type PaddingBitsBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	SampleCount uint32             // 32 bits, counts the number of samples in the track; it should match the count in other tables
+	Table       []PaddingBitsTable // Padding Bits Table, (sample count + 1) / 2 elements.
+}
+
+type PaddingBitsTable struct {
+	Reserved1 byte // 1 bit,
+	Pad1      byte // 3 bits, a value from 0 to 7, indicating the number of bits at the end of sample (i*2)+1.
+	Reserved2 byte // 1 bit,
+	Pad2      byte // 3 bits, a value from 0 to 7, indicating the number of bits at the end of sample (i*2)+2.
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/46
+//
+// Box Type: stdp
+// Container: Sample Table Box (‘stbl’).
+// Mandatory: No.
+// Quantity: Zero or one.
+//
+// This box contains the degradation priority of each sample. The values are stored in the table, one for each
+// sample. The size of the table, sample_count is taken from the sample_count in the Sample Size Box
+// ('stsz'). Specifications derived from this define the exact meaning and acceptable range of the priority field.
+type DegradationPriorityBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	Priority []uint16 // 16 bits array, sample count elements, is integer specifying the degradation priority for each sample.
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/53
+//
+// Box Types: sdtp
+// Container: Sample Table Box (‘stbl’)
+// Mandatory: No
+// Quantity: Zero or one
+type IndependentAndDisposableSamplesBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	Table []IndependentAndDisposableSamplesTable // Independent And Disposable Samples Table, sample count elements
+}
+
+type IndependentAndDisposableSamplesTable struct {
+	IsLeading           byte // 2 bits,
+	SampleDependsOn     byte // 2 bits,
+	SampleIsDependedOn  byte // 2 bits,
+	SampleHasTedundancy byte // 2 bits,
+}
+
+// is_leading takes one of the following four values:
+// 0: the leading nature of this sample is unknown;
+// 1: this sample is a leading sample that has a dependency before the referenced I-picture (and is
+// therefore not decodable);
+// 2: this sample is not a leading sample;
+// 3: this sample is a leading sample that has no dependency before the referenced I-picture (and is
+// therefore decodable);
+// sample_depends_on takes one of the following four values:
+// 0: the dependency of this sample is unknown;
+// 1: this sample does depend on others (not an I picture);
+// 2: this sample does not depend on others (I picture);
+// 3: reserved
+// sample_is_depended_on takes one of the following four values:
+// 0: the dependency of other samples on this sample is unknown;
+// 1: other samples may depend on this one (not disposable);
+// 2: no other sample depends on this one (disposable);
+// 3: reserved
+// sample_has_redundancy takes one of the following four values:
+// 0: it is unknown whether there is redundant coding in this sample;
+// 1: there is redundant coding in this sample;
+// 2: there is no redundant coding in this sample;
+// 3: reserved
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/75
+//
+// Box Type: sbgp
+// Container: Sample Table Box (‘stbl’) or Track Fragment Box (‘traf’)
+// Mandatory: No
+// Quantity: Zero or more.
+type SampleToGroupBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	GroupingType uint32               // 32 bits, is an integer that identifies the type (i.e. criterion used to form the sample groups) of the sample grouping and links it to its sample group description table with the same value for grouping type. At most one occurrence of this box with the same value for grouping_type (and, if used, grouping_type_parameter) shall exist for a track.
+	EntryCount   uint32               // 32 bits, is an integer that gives the number of entries in the following table.
+	Table        []SampleToGroupTable // Sample To Group Table, entry count elements.
+}
+
+type SampleToGroupTable struct {
+	SampleCount           uint32 // 32 bits, is an integer that gives the number of consecutive samples with the same sample group descriptor. If the sum of the sample count in this box is less than the total sample count, then the reader should effectively extend it with an entry that associates the remaining samples with no group. It is an error for the total in this box to be greater than the sample_count documented elsewhere, and the reader behaviour would then be undefined.
+	GroupDescriptionIndex uint32 // 32 bits, is an integer that gives the index of the sample group entry which describes the samples in this group. The index ranges from 1 to the number of sample group entries in the SampleGroupDescription Box, or takes the value 0 to indicate that this sample is a member of no group of this type.
+}
+
+// unsigned int(32) grouping_type;
+// if (version == 1) {
+// unsigned int(32) grouping_type_parameter;
+// }
+// unsigned int(32) entry_count;
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/76
+//
+// Box Type: sgpd
+// Container: Sample Table Box (‘stbl’) or Track Fragment Box (‘traf’)
+// Mandatory: No
+// Quantity: Zero or more, with one for each Sample to Group Box.
+type SampleGroupDescriptionBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	GroupingType uint32 // 32 bits, is an integer that identifies the SampleToGroup box that is associated with this sample group description.
+	EntryCount   uint32 // 32 bits, is an integer that gives the number of entries in the following table.
+}
+
+// default_length : indicates the length of every group entry (if the length is constant), or zero (0) if it is variable
+// description_length : indicates the length of an individual group entry, in the case it varies from entry to entry and default_length is therefore 0
+
+// if (version==1) { unsigned int(32) default_length; }
+
+// for (i = 1 ; i <= entry_count ; i++){
+// if (version==1) {
+// if (default_length==0) {
+// unsigned int(32) description_length;
+// }
+// }
+// switch (handler_type){
+// case ‘vide’: // for video tracks
+// VisualSampleGroupEntry (grouping_type);
+// break;
+// case ‘soun’: // for audio tracks
+// AudioSampleGroupEntry(grouping_type);
+// break;
+// case ‘hint’: // for hint tracks
+// HintSampleGroupEntry(grouping_type);
+// break;
+// }
+// }
+
+type SampleGroupDescriptionEntry struct{}
+
+type VisualSampleGroupEntry struct{}
+
+type AudioSampleGroupEntry struct{}
+
+type HintSampleGroupEntry struct{}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/61
+//
+// Box Type: subs
+// Container: Sample Table Box (‘stbl’) or Track Fragment Box (‘traf’)
+// Mandatory: No
+// Quantity: Zero or one
+type SubSampleInformationBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	EntryCount uint32                      // 32 bits, is an integer that gives the number of entries in the following table.
+	Table      []SubSampleInformationTable // Sub-Sample Information Table, entry count elements.
+
+}
+
+type SubSampleInformationTable struct {
+	SampleDelta    uint32                // 32 bits, is an integer that specifies the sample number of the sample having sub-sample structure. It is coded as the difference between the desired sample number, and the sample number indicated in the previous entry. If the current entry is the first entry, the value indicates the sample number of the first sample having sub-sample information, that is, the value is the difference between the sample number and zero (0).
+	SubsampleCount uint16                // 16 bits, is an integer that specifies the number of sub-sample for the current sample. If there is no sub-sample structure, then this field takes the value 0.
+	CountTable     []SubSampleCountTable // Sub-Sample Information Table1, subsample count elements.
+}
+
+type SubSampleCountTable struct {
+	SubsampleSize     interface{} // uint16 or uint32, is an integer that specifies the size, in bytes, of the current sub-sample
+	SubsamplePriority uint8       // 8 bits, is an integer specifying the degradation priority for each sub-sample. Higher values of subsample_priority, indicate sub-samples which are important to, and have a greater impact on, the decoded quality.
+	DiscardAble       uint8       // 8 bits, equal to 0 means that the sub-sample is required to decode the current sample, while equal to 1 means the sub-sample is not required to decode the current sample but may be used for enhancements, e.g., the sub-sample consists of supplemental enhancement information (SEI) messages.
+	Reserved          uint32      // 32 bits,
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/62
+//
+// Box Type: saiz
+// Container: Sample Table Box (‘stbl’) or Track Fragment Box ('traf')
+// Mandatory: No
+// Quantity: Zero or More
+type SampleAuxiliaryInformationSizesBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	Table interface{} // SampleAuxiliaryInformationSizesTable1 or SampleAuxiliaryInformationSizesTable2.
+}
+
+type SampleAuxiliaryInformationSizesTable1 struct {
+	AuxInfoType           uint32 // 32 bits,
+	AuxInfoTypeParameter  uint32 // 32 bits,
+	DefaultSampleInfoSize uint8  // 8 bits,  is an integer specifying the sample auxiliary information size for the case where all the indicated samples have the same sample auxiliary information size. If the size varies then this field shall be zero.
+	SampleCount           uint32 // 32 bits,
+}
+
+type SampleAuxiliaryInformationSizesTable2 struct {
+	DefaultSampleInfoSize uint8  // 8 bits,  is an integer specifying the sample auxiliary information size for the case where all the indicated samples have the same sample auxiliary information size. If the size varies then this field shall be zero.
+	SampleCount           uint32 // 32 bits,
+}
+
+// if (flags & 1) {
+// unsigned int(32) aux_info_type;
+// unsigned int(32) aux_info_type_parameter;
+// }
+// unsigned int(8) default_sample_info_size;
+// unsigned int(32) sample_count;
+// if (default_sample_info_size == 0) {
+// unsigned int(8) sample_info_size[ sample_count ];
+// }
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/63
+//
+// Box Type: saio
+// Container: Sample Table Box (‘stbl’) or Track Fragment Box ('traf')
+// Mandatory: No
+// Quantity: Zero or More
+type SampleAuxiliaryInformationOffsetsBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	//EntryCount uint32 // 32 bits, is an integer that gives the number of entries in the following table.
+}
+
+type AuxInfo struct {
+	AuxInfoType          uint32 // 32 bits,
+	AuxInfoTypeParameter uint32 // 32 bits,
+}
+
+// if (flags & 1) {
+// unsigned int(32) aux_info_type;
+// unsigned int(32) aux_info_type_parameter;
+// }
+// unsigned int(32) entry_count;
+// if ( version == 0 ) {
+// unsigned int(32) offset[ entry_count ];
+// }
+// else {
+// unsigned int(64) offset[ entry_count ];
+// }
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/78
+//
+// Box Type: udta
+// Container: Movie Box (‘moov’) or Track Box (‘trak’)
+// Mandatory: No
+// Quantity: Zero or one
+type UserDataBox struct {
+	MP4BoxHeader // standard header
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/64
+//
+// Box Type: mvex
+// Container: Movie Box (‘moov’)
+// Mandatory: No
+// Quantity: Zero or one
+type MovieExtendsBox struct {
+	MP4BoxHeader // standard header
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/65
+//
+// Box Type: mehd
+// Container: Movie Extends Box(‘mvex’)
+// Mandatory: No
+// Quantity: Zero or one
+//
+// The Movie Extends Header is optional, and provides the overall duration, including fragments, of a fragmented
+// movie. If this box is not present, the overall duration must be computed by examining each fragment.
+type MovieExtendsHeaderBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+}
+
+// if (version==1) {
+// unsigned int(64) fragment_duration;
+// } else { // version==0
+// unsigned int(32) fragment_duration;
+// }
+
+// fragment_duration : is an integer that declares length of the presentation of the whole movie including
+// fragments (in the timescale indicated in the Movie Header Box). The value of this field corresponds to
+// the duration of the longest track, including movie fragments. If an MP4 file is created in real-time, such
+// as used in live streaming, it is not likely that the fragment_duration is known in advance and this
+// box may be omitted.
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/65
+//
+// Box Type: trex
+// Container: Movie Extends Box (‘mvex’)
+// Mandatory: Yes
+// Quantity: Exactly one for each track in the Movie Box
+type TrackExtendsBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	TrackID                       uint32 // 32 bits, identifies the track; this shall be the track ID of a track in the Movie Box
+	DefaultSampleDescriptionIndex uint32 // 32 bits,
+	DefaultSampleDuration         uint32 // 32 bits,
+	DefaultSampleSize             uint32 // 32 bits,
+	DefaultSampleFlags            uint32 // 32 bits,
+}
+
+// default_ : these fields set up defaults used in the track fragments.
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/72
+//
+// Box Type: leva
+// Container: Movie Extends Box (`mvex’)
+// Mandatory: No
+// Quantity: Zero or one
+type LevelAssignmentBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	LevelCount uint8                  // 8 bits, specifies the number of levels each fraction is grouped into. level_count shall be greater than or equal to 2.
+	Table      []LevelAssignmentTable // Level Assignment Table, level count elements.
+}
+
+type LevelAssignmentTable struct {
+	TrackId        uint32 // 32 bits, for loop entry j specifies the track identifier of the track assigned to level j.
+	PaddingFlag    byte   // 1 bit, equal to 1 indicates that a conforming fraction can be formed by concatenating any positive integer number of levels within a fraction and padding the last Media Data box by zero bytes up to the full size that is indicated in the header of the last Media Data box. The semantics of padding_flag equal to 0 are that this is not assured.
+	AssignmentType byte   // 7 bits,
+}
+
+// for (j=1; j <= level_count; j++) {
+// unsigned int(32) track_id;
+// unsigned int(1) padding_flag;
+// unsigned int(7) assignment_type;
+// if (assignment_type == 0) {
+// unsigned int(32) grouping_type;
+// }
+// else if (assignment_type == 1) {
+// unsigned int(32) grouping_type;
+// unsigned int(32) grouping_type_parameter;
+// }
+// else if (assignment_type == 2) {} // no further syntax elements needed
+// else if (assignment_type == 3) {} // no further syntax elements needed
+// else if (assignment_type == 4) {
+// unsigned int(32) sub_track_id;
+// }
+// // other assignment_type values are reserved
+// }
+
+// assignment_type : indicates the mechanism used to specify the assignment to a level.
+// assignment_type values greater than 4 are reserved, while the semantics for the other values are
+// specified as follows. The sequence of assignment_types is restricted to be a set of zero or more of
+// type 2 or 3, followed by zero or more of exactly one type.
+// • 0: sample groups are used to specify levels, i.e., samples mapped to different sample group
+// description indexes of a particular sample grouping lie in different levels within the identified track;
+// other tracks are not affected and must have all their data in precisely one level;
+// • 1: as for assignment_type 0 except assignment is by a parameterized sample group;
+// • 2, 3: level assignment is by track (see the Subsegment Index Box for the difference in processing
+// of these levels)
+// • 4: the respective level contains the samples for a sub-track. The sub-tracks are specified through
+// the Sub Track box; other tracks are not affected and must have all their data in precisely one
+// level;
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/66
+//
+// Box Type: moof
+// Container: File
+// Mandatory: No
+// Quantity: Zero or more
+type MovieFragmentBox struct {
+	MP4BoxHeader // standard header
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/66
+//
+// Box Type: mfhd
+// Container: Movie Fragment Box ('moof')
+// Mandatory: Yes
+// Quantity: Exactly one
+//
+// The movie fragment header contains a sequence number, as a safety check. The sequence number usually
+// starts at 1 and must increase for each movie fragment in the file, in the order in which they occur. This allows
+// readers to verify integrity of the sequence; it is an error to construct a file where the fragments are out of sequence.
+type MovieFragmentHeaderBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	SequenceNumber uint32 // 32 bits, the ordinal number of this fragment, in increasing order
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/67
+//
+// Box Type: traf
+// Container: Movie Fragment Box ('moof')
+// Mandatory: No
+// Quantity: Zero or more
+type TrackFragmentBox struct {
+	MP4BoxHeader // standard header
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/67
+//
+// Box Type: tfhd
+// Container: Track Fragment Box ('traf')
+// Mandatory: Yes
+// Quantity: Exactly one
+type TrackFragmentHeaderBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	TrackID uint32 // 32 bits,
+
+	// all the following are optional fields
+	BaseDataOffset         uint64 // 64 bits, the base offset to use when calculating data offsets
+	SampleDescriptionIndex uint32 // 32 bits,
+	DefaultSampleDuration  uint32 // 32 bits,
+	DefaultSampleSize      uint32 // 32 bits,
+	DefaultSampleFlags     uint32 // 32 bits,
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/68
+//
+// Box Type: trun
+// Container: Track Fragment Box ('traf')
+// Mandatory: No
+// Quantity: Zero or more
+type TrackFragmentRunBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	SampleCount uint32 // 32 bits, the number of samples being added in this run; also the number of rows in the following table (the rows can be empty)
+
+	// the following are optional fields
+	DataOffset       int32  // 32 bits, signed, is added to the implicit or explicit data_offset established in the track fragment header.
+	FirstSampleFlags uint32 // 32 bits, provides a set of flags for the first sample only of this run.
+
+	// all fields in the following array are optional
+	Table []TrackFragmentRunTable // Track Fragment Run Table 1, SampleCount elements.
+}
+
+type TrackFragmentRunTable struct {
+	SampleDuration              uint32      // 32 bits,
+	SampleSize                  uint32      // 32 bits,
+	SampleFlags                 uint32      // 32 bits,
+	SampleCompositionTimeOffset interface{} // uint32 or int32,
+}
+
+// if (version == 0){
+// 	unsigned int(32) sample_composition_time_offset;
+// }
+// else{
+// 	signed int(32) sample_composition_time_offset;
+// }
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/71
+//
+// Box Type: tfdt
+// Container: Track Fragment box (‘traf’)
+// Mandatory: No
+// Quantity: Zero or one
+type TrackFragmentBaseMediaDecodeTimeBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	BaseMediaDecodeTime interface{} // uint32 or uint64, is an integer equal to the sum of the decode durations of all earlier samples in the media, expressed in the media's timescale. It does not include the samples added in the enclosing track fragment.
+}
+
+// if (version==1) {
+// unsigned int(64) baseMediaDecodeTime;
+// } else { // version==0
+// unsigned int(32) baseMediaDecodeTime;
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/69
+//
+// Box Type: mfra
+// Container: File
+// Mandatory: No
+// Quantity: Zero or one
+type MovieFragmentRandomAccessBox struct {
+	MP4BoxHeader // standard header
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/70
+//
+// Box Type: tfra
+// Container: Movie Fragment Random Access Box (‘mfra’)
+// Mandatory: No
+// Quantity: Zero or one per track
+type TrackFragmentRandomAccessBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	TrackID               uint32                           // 32 bits, is an integer identifying the track_ID.
+	Reserved              uint32                           // 26 bits,
+	LengthSizeOfTrafNum   byte                             // 2 bits, indicates the length in byte of the traf_number field minus one.
+	LengthSizeOfTrunNum   byte                             // 2 bits, indicates the length in byte of the trun_number field minus one.
+	LengthSizeOfSampleNum byte                             // 2 bits, indicates the length in byte of the sample_number field minus one.
+	NumberOfEntry         uint32                           // 32 bits, is an integer that gives the number of the entries for this track. If this value is zero, it indicates that every sample is a sync sample and no table entry follows.
+	Table                 []TrackFragmentRandomAccessTable // Track Fragment RandomAccess Table 1, NumberOfEntry elements.
+}
+
+type TrackFragmentRandomAccessTable struct {
+	Time         interface{} // uint32 or uint64, is 32 or 64 bits integer that indicates the presentation time of the sync sample in units defined in the ‘mdhd’ of the associated track.
+	Moofoffset   interface{} // uint32 or uint64, is 32 or 64 bits integer that gives the offset of the ‘moof’ used in this entry. Offset is the byte-offset between the beginning of the file and the beginning of the ‘moof’.
+	TrafNumber   interface{} // unsigned int((length_size_of_traf_num+1) * 8). indicates the ‘traf’ number that contains the sync sample. The number ranges from 1 (the first ‘traf’ is numbered 1) in each ‘moof’.
+	TrunNumber   interface{} // unsigned int((length_size_of_trun_num+1) * 8). indicates the ‘trun’ number that contains the sync sample. The number ranges from 1 in each ‘traf’
+	SampleNumber interface{} // unsigned int((length_size_of_sample_num+1) * 8) . indicates the sample number of the sync sample. The number ranges from 1 in each ‘trun’.
+}
+
+// for(i=1; i <= number_of_entry; i++){
+// if(version==1){
+// unsigned int(64) time;
+// unsigned int(64) moof_offset;
+// }else{
+// unsigned int(32) time;
+// unsigned int(32) moof_offset;
+// }
+// unsigned int((length_size_of_traf_num+1) * 8) traf_number;
+// unsigned int((length_size_of_trun_num+1) * 8) trun_number;
+// unsigned int((length_size_of_sample_num+1) * 8) sample_number;
+// }
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/71
+//
+// Box Type: mfro
+// Container: Movie Fragment Random Access Box (‘mfra’)
+// Mandatory: Yes
+// Quantity: Exactly one
+//
+// The Movie Fragment Random Access Offset Box provides a copy of the length field from the enclosing Movie
+// Fragment Random Access Box. It is placed last within that box, so that the size field is also last in the
+// enclosing Movie Fragment Random Access Box. When the Movie Fragment Random Access Box is also last
+// in the file this permits its easy location. The size field here must be correct. However, neither the presence of
+// the Movie Fragment Random Access Box, nor its placement last in the file, are assured.
+type MovieFragmentRandomAccessOffsetBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	Size uint32 // 32 bits, is an integer gives the number of bytes of the enclosing ‘mfra’ box. This field is placed at the last of the enclosing box to assist readers scanning from the end of the file in finding the ‘mfra’ box.
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/29
+//
+// Box Type: mdat
+// Container: File
+// Mandatory: No
+// Quantity: Zero or more
+type MediaDataBox struct {
+	MP4BoxHeader // standard header
+
+	Data []byte // 8 bits array, is the contained media data.
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/29
+//
+// Box Types: free, skip
+// Container: File or other box
+// Mandatory: No
+// Quantity: Zero or more
+//
+// The contents of a free-space box are irrelevant and may be ignored, or the object deleted, without affecting
+// the presentation. (Care should be exercised when deleting the object, as this may invalidate the offsets used
+// in the sample table, unless this object is after all the media data).
+type FreeSpaceBox struct {
+	MP4BoxHeader // standard header
+
+	Data []uint8 // 8 bits array,
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/78
+//
+// Box Type: cprt
+// Container: User data box (‘udta’)
+// Mandatory: No
+// Quantity: Zero or more
+//
+// The Copyright box contains a copyright declaration which applies to the entire presentation, when contained
+// within the Movie Box, or, when contained in a track, to that entire track. There may be multiple copyright
+// boxes using different language codes.
+type CopyrightBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	Pad      byte    // 1 bit,
+	Language [2]byte // 15 bits, declares the language code for the following text. See ISO 639-2/T for the set of three character codes. Each character is packed as the difference between its ASCII value and 0x60. The code is confined to being three lower-case letters, so these values are strictly positive.
+	Notice   string  // string, is a null-terminated string in either UTF-8 or UTF-16 characters, giving a copyright notice. If UTF- 16 is used, the string shall start with the BYTE ORDER MARK (0xFEFF), to distinguish it from a UTF- 8 string. This mark does not form part of the final string.
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/79
+//
+// Box Type: tsel
+// Container: User Data Box (‘udta’)
+// Mandatory: No
+// Quantity: Zero or One
+//
+// The track selection box is contained in the user data box of the track it modifies.
+type TrackSelectionBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	SwitchGroup   int32    // 32 bits, is an integer that specifies a group or collection of tracks. If this field is 0 (default value) or if the Track Selection box is absent there is no information on whether the track can be used for switching during playing or streaming. If this integer is not 0 it shall be the same for tracks that can be used for switching between each other. Tracks that belong to the same switch group shall belong to the same alternate group. A switch group may have only one member.
+	AttributeList []uint32 // 32 bits array, to end of the box, is a list, to the end of the box, of attributes. The attributes in this list should be used as descriptions of tracks or differentiation criteria for tracks in the same alternate or switch group. Each differentiating attribute is associated with a pointer to the field or information that distinguishes the track.
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/100
+//
+// Box Type: strk
+// Container: User Data box (‘udta’) of the corresponding Track box (‘trak’)
+// Mandatory: No
+// Quantity: Zero or more
+//
+// This box contains objects that define and provide information about a sub track in the present track.
+type SubTrack struct {
+	MP4BoxHeader // standard header
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/100
+//
+// Box Type: stri
+// Container: Sub Track box (‘strk’)
+// Mandatory: Yes
+// Quantity: One
+type SubTrackInformation struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	SwitchGroup    int16    // 16 bits,
+	AlternateGroup int16    // 16 bits,
+	SubTrackID     uint32   // 32 bits, is an integer. A non-zero value uniquely identifies the sub track locally within the track. A zero value (default) means that sub track ID is not assigned.
+	AttributeList  []uint32 // 32 bits array, is a list, to the end of the box, of attributes. The attributes in this list should be used as descriptions of sub tracks or differentiating criteria for tracks and sub tracks in the same alternate or switch group
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/101
+//
+// Box Type: strd
+// Container: Sub Track box (‘strk’)
+// Mandatory: Yes
+// Quantity: One
+//
+// This box contains objects that provide a definition of the sub track.
+type SubTrackDefinition struct {
+	MP4BoxHeader // standard header
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/81
+//
+// Box Type: meta
+// Container: File, Movie Box (‘moov’), Track Box (‘trak’), or Additional Metadata Container Box (‘meco’)
+// Mandatory: No
+// Quantity: Zero or one (in File, ‘moov’, and ‘trak’), One or more (in ‘meco’)
+type MetaBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	PrimaryResource PrimaryItemBox     // optional
+	FileLocations   DataInformationBox // optional
+	ItemLocations   ItemLocationBox    // optional
+	Protections     ItemProtectionBox  // optional
+	ItemInfos       ItemInfoBox        // optional
+	IPMPControl     IPMPControlBox     // optional
+	ItemRefs        ItemReferenceBox   // optional
+	ItemData        ItemDataBox        // optional
+	//OtherBoxes      []Box              // optional
+}
+
+type IPMPControlBox struct{}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/82
+//
+// Box Type: iloc
+// Container: Meta box (‘meta’)
+// Mandatory: No
+// Quantity: Zero or one
+type ItemLocationBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	OffsetSize     byte        // 4 bits,
+	LengthSize     byte        // 4 bits,
+	BaseOffsetSize byte        // 4 bits,
+	IndexSize      byte        // 4 bits, if version == 1, index_size replace to reserved.
+	ItemCount      uint16      // 16 bits,
+	Table          interface{} // version == 1 -> ItemLocationTable1 , version == 2 -> ItemLocationTable2, ItemCount elements.
+}
+
+type ItemLocationTable1 struct {
+	ItemID             uint16                     // 16 bits,
+	Reserved           uint16                     // 12 bits,
+	ConstructionMethod byte                       // 4 bits,
+	DataReferenceIndex uint16                     // 16 bits,
+	BaseOffset         interface{}                // unsigned int(base_offset_size*8),
+	ExtentCount        uint16                     // 16 bits,
+	ExtentTable        []ItemLocationExtentTable1 // Item Location Extent Table1, ExtentCount elements.
+}
+
+type ItemLocationTable2 struct {
+	ItemID             uint16                     // 16 bits,
+	DataReferenceIndex uint16                     // 16 bits,
+	BaseOffset         interface{}                // unsigned int(base_offset_size*8),
+	ExtentCount        uint16                     // 16 bits,
+	ExtentTable        []ItemLocationExtentTable2 // Item Location Extent Table2, ExtentCount elements.
+}
+
+type ItemLocationExtentTable1 struct {
+	ExtentIndex interface{} // unsigned int(index_size*8)
+	ItemLocationExtentTable2
+}
+
+type ItemLocationExtentTable2 struct {
+	ExtentOffset interface{} // unsigned int(offset_size*8)
+	ExtentLength interface{} // unsigned int(length_size*8)
+}
+
+// for (i=0; i<item_count; i++) {
+// 	unsigned int(16) item_ID;
+
+// 	if (version == 1) {
+// 		unsigned int(12) reserved = 0;
+// 		unsigned int(4) construction_method;
+// 	}
+
+// 	unsigned int(16) data_reference_index;
+// 	unsigned int(base_offset_size*8) base_offset;
+// 	unsigned int(16) extent_count;
+
+// 	for (j=0; j<extent_count; j++) {
+// 		if ((version == 1) && (index_size > 0)) {
+// 			unsigned int(index_size*8) extent_index;
+// 		}
+
+// 	unsigned int(offset_size*8) extent_offset;
+// 	unsigned int(length_size*8) extent_length;
+// }
+
+// offset_size 			: is taken from the set {0, 4, 8} and indicates the length in bytes of the offset field.
+// length_size 			: is taken from the set {0, 4, 8} and indicates the length in bytes of the length field.
+// base_offset_size		: is taken from the set {0, 4, 8} and indicates the length in bytes of the base_offset field.
+// index_size 			: is taken from the set {0, 4, 8} and indicates the length in bytes of the extent_index field.
+// item_count 			: counts the number of resources in the following array.
+// item_ID 				: is an arbitrary integer ‘name’ for this resource which can be used to refer to it (e.g. in a URL).
+// construction_method	: is taken from the set 0 (file), 1 (idat) or 2 (item)
+// data-reference-index	: is either zero (‘this file’) or a 1-based index into the data references in the data information box.
+// base_offset 			: provides a base value for offset calculations within the referenced data. If
+// base_offset_size 	: is 0, base_offset takes the value 0, i.e. it is unused.
+// extent_count 		: provides the count of the number of extents into which the resource is fragmented; it must have the value 1 or greater
+// extent_index 		: provides an index as defined for the construction method
+// extent_offset 		: provides the absolute offset in bytes from the beginning of the containing file, of this item. If offset_size is 0, offset takes the value 0
+// extent_length 		: provides the absolute length in bytes of this metadata item. If length_size is 0, length takes the value 0. If the value is 0, then length of the item is the length of the entire referenced file.
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/85
+//
+// Box Type: ipro
+// Container: Meta box (‘meta’)
+// Mandatory: No
+// Quantity: Zero or one
+//
+// The item protection box provides an array of item protection information, for use by the Item Information Box.
+type ItemProtectionBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	ProtectionCount uint16                // 16 bits,
+	Table           []ItemProtectionTable // Item Protection Table, ProtectionCount elements.
+}
+
+type ItemProtectionTable struct {
+	ProtectionInformation ProtectionSchemeInfoBox
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/92
+//
+// Box Types: sinf
+// Container: Protected Sample Entry, or Item Protection Box (‘ipro’)
+// Mandatory: Yes
+// Quantity: One or More
+type ProtectionSchemeInfoBox struct {
+	MP4BoxHeader // standard header
+
+	OriginalFormat OriginalFormatBox    //
+	Type           SchemeTypeBox        // optional
+	Info           SchemeInformationBox // optional
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/93
+//
+// Box Types: frma
+// Container: Protection Scheme Information Box (‘sinf’) or Restricted Scheme Information Box (‘rinf’)
+// Mandatory: Yes when used in a protected sample entry or in a restricted sample entry
+// Quantity: Exactly one
+//
+// The Original Format Box ‘frma’ contains the four-character-code of the original un-transformed sample description:
+
+type OriginalFormatBox struct {
+	MP4BoxHeader // standard header
+
+	DataFormat uint32 // 32 bits, is the four-character-code of the original un-transformed sample entry (e.g. “mp4v” if the stream contains protected or restricted MPEG-4 visual material).
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/93
+//
+// Box Types: schm
+// Container: Protection Scheme Information Box (‘sinf’), Restricted Scheme Information Box (‘rinf’),
+// or SRTP Process box (‘srpp‘)
+// Mandatory: No
+//
+// Quantity: Zero or one in ‘sinf’, depending on the protection structure; Exactly one in ‘rinf’ and ‘srpp’
+// The Scheme Type Box (‘schm’) identifies the protection or restriction scheme.
+type SchemeTypeBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	SchemeType    uint32 // 32 bits, is the code defining the protection or restriction scheme.
+	SchemeVersion uint32 // 32 bits, is the version of the scheme (used to create the content)
+}
+
+// if (flags & 0x000001) {
+// 	unsigned int(8) scheme_uri[]; // browser uri
+// }
+
+// scheme_URI : allows for the option of directing the user to a web-page if they do not have the scheme installed on their system. It is an absolute URI formed as a null-terminated string in UTF-8 characters.
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/94
+//
+// Box Types: schi
+// Container: Protection Scheme Information Box (‘sinf’), Restricted Scheme Information Box (‘rinf’),
+// or SRTP Process box (‘srpp‘)
+// Mandatory: No
+// Quantity: Zero or one
+// The Scheme Information Box is a container Box that is only interpreted by the scheme being used. Any
+// information the encryption or restriction system needs is stored here. The content of this box is a series of
+// boxes whose type and format are defined by the scheme declared in the Scheme Type Box.
+type SchemeInformationBox struct {
+	MP4BoxHeader // standard header
+
+	SchemeSpecificData []SchemeTypeBox
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/85
+//
+// Box Type: iinf
+// Container: Meta Box (‘meta’)
+// Mandatory: No
+// Quantity: Zero or one
+
+type ItemInfoBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	EntryCount uint16          // 16 bits,
+	ItemInfos  []ItemInfoEntry // EntryCount elements.
+}
+
+// Box Type: infe
+type ItemInfoEntry struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	ItemID              uint16 // 16 bits
+	ItemProtectionIndex uint16 // 16 bits
+	ItemType            uint32 // 32 bits,
+	ItemName            string // string,
+	ContentType         string // string,
+	ContentEncoding     string // string, optional
+	ItemUriType         string // string,
+	ExtensionType       uint32 // 32 bits, optional
+	ItemInfoExtension          // optional
+}
+
+type ItemInfoExtension struct {
+}
+
+// if ((version == 0) || (version == 1)) {
+// 	unsigned int(16) item_ID;
+// 	unsigned int(16) item_protection_index
+// 	string item_name;
+// 	string content_type;
+// 	string content_encoding; //optional
+// }
+
+// if (version == 1) {
+// 	unsigned int(32) extension_type; //optional
+// 	ItemInfoExtension(extension_type); //optional
+// }
+
+// if (version == 2) {
+// 	unsigned int(16) item_ID;
+// 	unsigned int(16) item_protection_index;
+// 	unsigned int(32) item_type;
+// 	string item_name;
+
+// 	if (item_type==’mime’) {
+// 		string content_type;
+// 		string content_encoding; //optional
+// 	} else if (item_type == ‘uri ‘) {
+// 		string item_uri_type;
+// 	}
+// }
+
+// item_id 					: contains either 0 for the primary resource (e.g., the XML contained in an ‘xml ‘ box) or the ID of the item for which the following information is defined.
+// item_protection_index 	: contains either 0 for an unprotected item, or the one-based index into the item protection box defining the protection applied to this item (the first box in the item protection box has the index 1).
+// item_name 				: is a null-terminated string in UTF-8 characters containing a symbolic name of the item (source file for file delivery transmissions).
+// item_type 				: is a 32-bit value, typically 4 printable characters, that is a defined valid item type indicator, such as ‘mime’
+// content_type 			: is a null-terminated string in UTF-8 characters with the MIME type of the item. If the item is content encoded (see below), then the content type refers to the item after content decoding.
+// item_uri_type 			: is a string that is an absolute URI, that is used as a type indicator.
+// content_encoding 		: is an optional null-terminated string in UTF-8 characters used to indicate that the binary file is encoded and needs to be decoded before interpreted. The values are as defined for Content-Encoding for HTTP/1.1. Some possible values are “gzip”, “compress” and “deflate”. An empty string indicates no content encoding. Note that the item is stored after the content encoding has been applied.
+// extension_type 			: is a printable four-character code that identifies the extension fields of version 1 with respect to version 0 of the Item information entry.
+// content_location 		: is a null-terminated string in UTF-8 characters containing the URI of the file as defined in HTTP/1.1 (RFC 2616).
+// content_MD5 				: is a null-terminated string in UTF-8 characters containing an MD5 digest of the file. See HTTP/1.1 (RFC 2616) and RFC 1864.
+// content_length 			: gives the total length (in bytes) of the (un-encoded) file.
+// transfer_length 			: gives the total length (in bytes) of the (encoded) file. Note that transfer length is equal to content length if no content encoding is applied (see above).
+// entry_count provides 	: a count of the number of entries in the following array.
+// group_ID 				: indicates a file group to which the file item (source file) belongs. See 3GPP TS 26.346 for more details on file groups.
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/82
+//
+// Box Type: ‘xml ‘ or ‘bxml’
+// Container: Meta box (‘meta’)
+// Mandatory: No
+// Quantity: Zero or one
+//
+// When the primary data is in XML format and it is desired that the XML be stored directly in the meta-box, one
+// of these forms may be used. The Binary XML Box may only be used when there is a single well-defined
+// binarization of the XML for that defined format as identified by the handler.
+// Within an XML box the data is in UTF-8 format unless the data starts with a byte-order-mark (BOM), which
+// indicates that the data is in UTF-16 format.
+type XMLBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	XML string // string,
+}
+
+type BinaryXMLBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	Data []uint8 // 8 bits array,
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/93
+//
+// Box Type: pitm
+// Container: Meta box (‘meta’)
+// Mandatory: No
+// Quantity: Zero or one
+//
+// For a given handler, the primary data may be one of the referenced items when it is desired that it be stored
+// elsewhere, or divided into extents; or the primary metadata may be contained in the meta-box (e.g. in an XML
+// box). Either this box must occur, or there must be a box within the meta-box (e.g. an XML box) containing the
+// primary information in the format required by the identified handler.
+type PrimaryItemBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	ItemID uint16 // 16 bits, is the identifier of the primary item
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/95
+//
+// Box Type: fiin
+// Container: Meta Box (‘meta’)
+// Mandatory: No
+// Quantity: Zero or one
+//
+// The FD item information box is optional, although it is mandatory for files using FD hint tracks. It provides
+// information on the partitioning of source files and how FD hint tracks are combined into FD sessions. Each
+// partition entry provides details on a particular file partitioning, FEC encoding and associated File and FEC
+// reservoirs. It is possible to provide multiple entries for one source file (identified by its item ID) if alternative
+// FEC encoding schemes or partitionings are used in the file. All partition entries are implicitly numbered and
+// the first entry has number 1.
+type FDItemInformationBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	EntryCount uint16            // 16 bits,
+	PE         []PartitionEntry  // EntryCount elements.
+	FDSGB      FDSessionGroupBox // optional
+	GidToNameB GroupIdToNameBox  // optional
+}
+
+// Box Type: paen
+type PartitionEntry struct {
+	FPB   FilePartitionBox //
+	FECRB FECReservoirBox  //optional
+	FRB   FileReservoirBox //optional
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/99
+//
+// Box Type: fire
+// Container: Partition Entry (‘paen’)
+// Mandatory: No
+// Quantity: Zero or One
+//
+// The File reservoir box associates the source file identified in the file partition box ('fpar') with File reservoirs
+// stored as additional items. It contains a list that starts with the first File reservoir associated with the first
+// source block of the source file and continues sequentially through the source blocks of the source file.
+type FileReservoirBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	EntryCount uint16               // 16 bits, gives the number of entries in the following list. An entry count here should match the total number or blocks in the corresponding file partition box.
+	Table      []FileReservoirTable // EntryCount elements.
+}
+
+type FileReservoirTable struct {
+	ItemID      uint16 // 16 bits, indicates the location of the File reservoir associated with a source block.
+	SymbolCount uint32 // 32 bits, indicates the number of source symbols contained in the File reservoir.
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/95
+//
+// Box Type: fpar
+// Container: Partition Entry (‘paen’)
+// Mandatory: Yes
+// Quantity: Exactly one
+//
+// The File Partition box identifies the source file and provides a partitioning of that file into source blocks and
+// symbols. Further information about the source file, e.g., filename, content location and group IDs, is contained
+// in the Item Information box ('iinf'), where the Item Information entry corresponding to the item ID of the
+// source file is of version 1 and includes a File Delivery Item Information Extension ('fdel').
+type FilePartitionBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	ItemID                     uint16               // 16 bits,
+	PacketPayloadSize          uint16               // 16 bits,
+	Reserved                   uint8                // 8 bits,
+	FECEncodingID              uint8                // 8 bits,
+	FECInstanceID              uint16               // 16 bits,
+	MaxSourceBlockLength       uint16               // 16 bits,
+	EncodingSymbolLength       uint16               // 16 bits,
+	MaxNumberOfEncodingSymbols uint16               // 16 bits,
+	SchemeSpecificInfo         string               // string,
+	EntryCount                 uint16               // 16 bits,
+	Tanble                     []FilePartitionTable //File Partition Table, EntryCount elements.
+}
+
+type FilePartitionTable struct {
+	BlockCount uint16 // 16 bits,
+	BlockSize  uint32 // 32 bits,
+}
+
+// item_ID 							: references the item in the item location box ('iloc') that the file partitioning applies to.
+// packet_payload_size 				: gives the target ALC/LCT or FLUTE packet payload size of the partitioning algorithm. Note that UDP packet payloads are larger, as they also contain ALC/LCT or FLUTE headers.
+// FEC_encoding_ID 					: identifies the FEC encoding scheme and is subject to IANA registration (see RFC 5052). Note that i) value zero corresponds to the "Compact No-Code FEC scheme" also known as "Null-FEC" (RFC 3695); ii) value one corresponds to the “MBMS FEC” (3GPP TS 26.346); iii) for values in the range of 0 to 127, inclusive, the FEC scheme is Fully-Specified, whereas for values in the range of 128 to 255, inclusive, the FEC scheme is Under-Specified.
+// FEC_instance_ID			 		: provides a more specific identification of the FEC encoder being used for an UnderSpecified FEC scheme. This value should be set to zero for Fully-Specified FEC schemes and shall be ignored when parsing a file with FEC_encoding_ID in the range of 0 to 127, inclusive. FEC_instance_ID is scoped by the FEC_encoding_ID. See RFC 5052 for further details.
+// max_source_block_length 			: gives the maximum number of source symbols per source block.
+// encoding_symbol_length 			: gives the size (in bytes) of one encoding symbol. All encoding symbols of one item have the same length, except the last symbol which may be shorter.
+// max_number_of_encoding_symbols 	: gives the maximum number of encoding symbols that can be generated for a source block for those FEC schemes in which the maximum number of encoding symbols is relevant, such as FEC encoding ID 129 defined in RFC 5052. For those FEC schemes in which the maximum number of encoding symbols is not relevant, the semantics of this field is unspecified.
+// scheme_specific_info				: is a base64-encoded null-terminated string of the scheme-specific object transfer information (FEC-OTI-Scheme-Specific-Info). The definition of the information depends on the FEC encoding ID.
+// entry_count 						: gives the number of entries in the list of (block_count, block_size) pairs that provides a partitioning of the source file. Starting from the beginning of the file, each entry indicates how the next segment of the file is divided into source blocks and source symbols.
+// block_count 						: indicates the number of consecutive source blocks of size block_size.
+// block_size  						: indicates the size of a block (in bytes). A block_size that is not a multiple of the encoding_symbol_length symbol size indicates with Compact No-Code FEC that the last source symbols includes padding that is not stored in the item. With MBMS FEC (3GPP TS 26.346) the padding may extend across multiple symbols but the size of padding should never be more than encoding_symbol_length.
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/97
+//
+// Box Type: fecr
+// Container: Partition Entry (‘paen’)
+// Mandatory: No
+// Quantity: Zero or One
+//
+// The FEC reservoir box associates the source file identified in the file partition box ('fpar') with FEC
+// reservoirs stored as additional items. It contains a list that starts with the first FEC reservoir associated with
+// the first source block of the source file and continues sequentially through the source blocks of the source file.
+type FECReservoirBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	EntryCount uint16              // 16 bits,
+	Table      []FECReservoirTable // FEC Reservoir Table, EntryCount elements.
+}
+
+type FECReservoirTable struct {
+	ItemID      uint16 // 16 bits, indicates the location of the FEC reservoir associated with a source block.
+	SymbolCount uint32 // 32 bits, indicates the number of repair symbols contained in the FEC reservoir.
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/97
+//
+// Box Type: segr
+// Container: FD Information Box (‘fiin’)
+// Mandatory: No
+// Quantity: Zero or One
+type FDSessionGroupBox struct {
+	MP4BoxHeader // standard header
+
+	NumSessionGroups uint16                // 16 bits,
+	Table            []FDSessionGroupTable // FD Session Group Table, NumSessionGroups elements.
+}
+
+type FDSessionGroupTable struct {
+	EntryCount                uint8                       // 8 bits,
+	GIDTable                  []FDSessionGroupIDTable     // FDSession Group ID Table, EntryCount elements.
+	NumChannelsInSessionGroup uint16                      // 16 bits
+	HTIDTable                 []FDSessionHintTrackIDTable // FDSession Hint Track ID Table, NumChannelsInSessionGroup elements.
+}
+
+type FDSessionGroupIDTable struct {
+	GroupID uint32 // 32 bits
+}
+
+type FDSessionHintTrackIDTable struct {
+	HintTrackID uint32 // 32 bits
+}
+
+// for(i=0; i < num_session_groups; i++) {
+// 	unsigned int(8) entry_count;
+
+// 	for (j=0; j < entry_count; j++) {
+// 		unsigned int(32) group_ID;
+// 	}
+
+// 	unsigned int(16) num_channels_in_session_group;
+
+// 	for(k=0; k < num_channels_in_session_group; k++) {
+// 		unsigned int(32) hint_track_id;
+// 	}
+// }
+
+// num_session_groups 				: specifies the number of session groups.
+// entry_count 						: gives the number of entries in the following list comprising all file groups that the session group complies with. The session group contains all files included in the listed file groups as specified by the item information entry of each source file. Note that the FDT for the session group should only contain those groups that are listed in this structure.
+// group_ID 						: indicates a file group that the session group complies with.
+// num_channels_in_session_groups 	: specifies the number of channels in the session group. The value of num_channels_in_session_groups shall be a positive integer.
+// hint_track_ID 					: specifies the track ID of the FD hint track belonging to a particular session group. Note that one FD hint track corresponds to one LCT channel.
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/98
+//
+// Box Type: gitn
+// Container: FD Information Box (‘fiin’)
+// Mandatory: No
+// Quantity: Zero or One
+//
+// The Group ID to Name box associates file group names to file group IDs used in the version 1 item
+// information entries in the item information box ('iinf').
+type GroupIdToNameBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	EntryCount uint16             // 16 bits, gives the number of entries in the following list.
+	Table      []GroupIdToNameBox // Group Id To Name Table, EntryCount elements.
+}
+
+type GroupIdToNameTable struct {
+	GroupID   uint32 // 32 bits, indicates a file group.
+	GroupName string // string, is a null-terminated string in UTF-8 characters containing a file group name.
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/90
+//
+// Box Type: idat
+// Container: Metadata box (‘meta’)
+// Mandatory: No
+// Quantity: Zero or one
+//
+// This box contains the data of metadata items that use the construction method indicating that an item’s data
+// extents are stored within this box.
+type ItemDataBox struct {
+	MP4BoxHeader // standard header
+
+	Data []byte // 8 bits array, is the contained meta data
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/91
+//
+// Box Type: iref
+// Container: Metadata box (‘meta’)
+// Mandatory: No
+// Quantity: Zero or one
+//
+// The item reference box allows the linking of one item to others via typed references. All the references for one
+// item of a specific type are collected into a single item type reference box, whose type is the reference type,
+// and which has a ‘from item ID’ field indicating which item is linked. The items linked to are then represented by
+// an array of ‘to item ID’s. All these single item type reference boxes are then collected into the item reference
+// box. The reference types defined for the track reference box defined in 8.3.3 may be used here if appropriate,
+// or other registered reference types.
+type ItemReferenceBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	SITRB []SingleItemTypeReferenceBox
+}
+
+type SingleItemTypeReferenceBox struct {
+	MP4BoxHeader // standard header
+
+	FromItemID     uint16                         // 16 bits, contains the ID of the item that refers to other items
+	ReferenceCount uint16                         // 16 bits, is the number of references
+	Table          []SingleItemTypeReferenceTable // Single Item Type Reference Table, ReferenceCount elements.
+}
+
+type SingleItemTypeReferenceTable struct {
+	ToItemID uint16 // 16 bits, contains the ID of the item referred to
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/87
+//
+// Box Type: meco
+// Container: File, Movie Box (‘moov’), or Track Box (‘trak’)
+// Mandatory: No
+// Quantity: Zero or one
+type AdditionalMetadataContainerBox struct {
+	MP4BoxHeader // standard header
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/88
+//
+// Box Type: mere
+// Container: Additional Metadata Container Box (‘meco’)
+// Mandatory: No
+// Quantity: Zero or more
+//
+// The metabox relation box indicates a relation between two meta boxes at the same level, i.e., the top level of
+// the file, the Movie Box, or Track Box. The relation between two meta boxes is unspecified if there is no
+// metabox relation box for those meta boxes. Meta boxes are referenced by specifying their handler types.
+type MetaboxRelationBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	FirstMetaboxHandlerType  uint32 // 32 bits, indicates the first meta box to be related.
+	SecondMetaboxHandlerType uint32 // 32 bits,  indicates the second meta box to be related.
+	MetaboxRelation          uint8  // 8 bits, indicates the relation between the two meta boxes.
+}
+
+// metabox_relation indicates the relation between the two meta boxes. The following values are defined:
+// 1 The relationship between the boxes is unknown (which is the default when this box is not present);
+// 2 the two boxes are semantically un-related (e.g., one is presentation, the other annotation);
+// 3 the two boxes are semantically related but complementary (e.g., two disjoint sets of meta-data expressed in two different meta-data systems);
+// 4 the two boxes are semantically related but overlap (e.g., two sets of meta-data neither of which is a subset of the other); neither is ‘preferred’ to the other;
+// 5 the two boxes are semantically related but the second is a proper subset or weaker version of the first; the first is preferred;
+// 6 the two boxes are semantically related and equivalent (e.g., two essentially identical sets of meta-data expressed in two different meta-data systems).
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/105
+//
+// Box Type: styp
+// Container: File
+// Mandatory: No
+// Quantity: Zero or more
+//
+// If segments are stored in separate files (e.g. on a standard HTTP server) it is recommended that these
+// ‘segment files’ contain a segment-type box, which must be first if present, to enable identification of those files,
+// and declaration of the specifications with which they are compliant.
+// A segment type has the same format as an 'ftyp' box [4.3], except that it takes the box type 'styp'. The
+// brands within it may include the same brands that were included in the 'ftyp' box that preceded the
+// ‘moov’ box, and may also include additional brands to indicate the compatibility of this segment with various
+// specification(s).
+// Valid segment type boxes shall be the first box in a segment. Segment type boxes may be removed if
+// segments are concatenated (e.g. to form a full file), but this is not required. Segment type boxes that are not
+// first in their files may be ignored.
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/106
+//
+// Box Type: sidx
+// Container: File
+// Mandatory: No
+// Quantity: Zero or more
+type SegmentIndexBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	ReferenceID              uint32              // 32 bits,
+	TimeScale                uint32              // 32 bits,
+	EarliestPresentationTime interface{}         // uint32 or uint64,
+	FirstOffset              interface{}         // uint32 or uint64,
+	Reserved                 uint16              // 16 bits,
+	ReferenceCount           uint16              // 16 bits,
+	Table                    []SegmentIndexTable // Segment Index Table, ReferenceCount elements
+}
+
+type SegmentIndexTable struct {
+	ReferenceType      byte   // 1 bit
+	ReferencedSize     uint32 // 32 bits
+	SubSegmentDuration uint32 // 32 bits,
+	StartsWithSAP      byte   // 1 bit
+	SAPType            byte   // 3 bits,
+	SAPDeltaTime       uint32 // 28 bits,
+}
+
+// if (version==0) {
+// 	unsigned int(32) earliest_presentation_time;
+// 	unsigned int(32) first_offset;
+// }
+// else {
+// 	unsigned int(64) earliest_presentation_time;
+// 	unsigned int(64) first_offset;
+// }
+
+// unsigned int(16) reserved = 0;
+// unsigned int(16) reference_count;
+
+// for(i=1; i <= reference_count; i++)
+// {
+// 	bit (1) reference_type;
+// 	unsigned int(31) referenced_size;
+// 	unsigned int(32) subsegment_duration;
+// 	bit(1) starts_with_SAP;
+// 	unsigned int(3) SAP_type;
+// 	unsigned int(28) SAP_delta_time;
+// }
+
+// reference_ID 				: provides the stream ID for the reference stream; if this Segment Index box is referenced from a “parent” Segment Index box, the value of reference_ID shall be the same as the value of reference_ID of the “parent” Segment Index box;
+// timescale 					: provides the timescale, in ticks per second, for the time and duration fields within this box; it is recommended that this match the timescale of the reference stream or track; for files based on this specification, that is the timescale field of the Media Header Box of the track;
+// earliest_presentation_time 	: is the earliest presentation time of any access unit in the reference stream in the first subsegment, in the timescale indicated in the timescale field;
+// first_offset 				: is the distance in bytes, in the file containing media, from the anchor point, to the first byte of the indexed material;
+// reference_count 				: provides the number of referenced items;
+// reference_type 				: when set to 1 indicates that the reference is to a segment index (‘sidx’) box; otherwise the reference is to media content (e.g., in the case of files based on this specification, to a movie fragment box); if a separate index segment is used, then entries with reference type 1 are in the index segment, and entries with reference type 0 are in the media file;
+// referenced_size 				: the distance in bytes from the first byte of the referenced item to the first byte of the next referenced item, or in the case of the last entry, the end of the referenced material;
+// subsegment_duration			: when the reference is to Segment Index box, this field carries the sum of the subsegment_duration fields in that box; when the reference is to a subsegment, this field carries the difference between the earliest presentation time of any access unit of the reference stream in the next subsegment (or the first subsegment of the next segment, if this is the last subsegment of the segment, or the end presentation time of the reference stream if this is the last subsegment of the stream) and the earliest presentation time of any access unit of the reference stream in the referenced subsegment; the duration is in the same units as earliest_presentation_time;
+// starts_with_SAP 				: indicates whether the referenced subsegments start with a SAP. For the detailed semantics of this field in combination with other fields, see the table below.
+// SAP_type 					: indicates a SAP type as specified in Annex I, or the value 0. Other type values are reserved. For the detailed semantics of this field in combination with other fields, see the table below.
+// SAP_delta_time 				: indicates TSAP of the first SAP, in decoding order, in the referenced subsegment for the reference stream. If the referenced subsegments do not contain a SAP, SAP_delta_time is reserved with the value 0; otherwise SAP_delta_time is the difference between the earliest presentation time of the subsegment, and the TSAP (note that this difference may be zero, in the case that the subsegment starts with a SAP).
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/109
+//
+// Box Type: ssix
+// Container: File
+// Mandatory: No
+// Quantity: Zero or more
+type SubsegmentIndexBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	SubSegmentCount uint32                 // 32 bits, is a positive integer specifying the number of subsegments for which partial subsegment information is specified in this box. subsegment_count shall be equal to reference_count (i.e., the number of movie fragment references) in the immediately preceding Segment Index box.
+	Table           []SubsegmentIndexTable // Subsegment Index Table, SubSegmentCount elements.
+}
+
+type SubsegmentIndexTable struct {
+	RangesCount uint32                  // 32 bits, specifies the number of partial subsegment levels into which the media data is grouped. This value shall be greater than or equal to 2.
+	Rtable      []SubsegmentRangesTable // Subsegment Ranges Table, RangesCount elements.
+}
+
+type SubsegmentRangesTable struct {
+	level      uint8   // 8 bits, specifies the level to which this partial subsegment is assigned.
+	range_size [3]byte // 24 bits, indicates the size of the partial subsegment.
+}
+
+// -------------------------------------------------------------------------------------------------------
+
+//
+// ISO_IEC_14496-12_2012.pdf Page/111
+//
+// Box Type: prft
+// Container: File
+// Mandatory: No
+// Quantity: Zero or more
+type ProducerReferenceTimeBox struct {
+	MP4BoxHeader     // standard header
+	MP4FullBoxHeader // full box header
+
+	ReferenceTrackID uint32      // 32 bits, provides the track_ID for the reference track.
+	NtpTimestamp     uint64      // 64 bits, indicates a UTC time in NTP format corresponding to decoding_time.
+	MediaTime        interface{} // uint32 or uint64, corresponds to the same time as ntp_timestamp, but in the time units used for the reference track, and is measured on this media clock as the media is produced.
+}
+
+// if (version==0) {
+// 	unsigned int(32) media_time;
+// } else {
+// 	unsigned int(64) media_time;
+// }
+
+// -------------------------------------------------------------------------------------------------------

+ 578 - 0
m7s.live/engine/v4@v4.14.6/codec/mpegts/mpegts.go

@@ -0,0 +1,578 @@
+package mpegts
+
+import (
+	"bytes"
+	"errors"
+	"io"
+	"io/ioutil"
+
+	"m7s.live/engine/v4/util"
+	//"sync"
+)
+
+// NALU AUD 00 00 00 01 09 F0
+
+const (
+	TS_PACKET_SIZE      = 188
+	TS_DVHS_PACKET_SIZE = 192
+	TS_FEC_PACKET_SIZE  = 204
+
+	TS_MAX_PACKET_SIZE = 204
+
+	PID_PAT        = 0x0000
+	PID_CAT        = 0x0001
+	PID_TSDT       = 0x0002
+	PID_RESERVED1  = 0x0003
+	PID_RESERVED2  = 0x000F
+	PID_NIT_ST     = 0x0010
+	PID_SDT_BAT_ST = 0x0011
+	PID_EIT_ST     = 0x0012
+	PID_RST_ST     = 0x0013
+	PID_TDT_TOT_ST = 0x0014
+	PID_NET_SYNC   = 0x0015
+	PID_RESERVED3  = 0x0016
+	PID_RESERVED4  = 0x001B
+	PID_SIGNALLING = 0x001C
+	PID_MEASURE    = 0x001D
+	PID_DIT        = 0x001E
+	PID_SIT        = 0x001F
+	PID_PMT        = 0x0100
+	PID_VIDEO      = 0x0101
+	PID_AUDIO      = 0x0102
+	// 0x0003 - 0x000F Reserved
+	// 0x0010 - 0x1FFE May be assigned as network_PID, Program_map_PID, elementary_PID, or for other purposes
+	// 0x1FFF Null Packet
+
+	// program_association_section
+	// conditional_access_section
+	// TS_program_map_section
+	// TS_description_section
+	// ISO_IEC_14496_scene_description_section
+	// ISO_IEC_14496_object_descriptor_section
+	// Metadata_section
+	// IPMP_Control_Information_section (defined in ISO/IEC 13818-11)
+	TABLE_PAS               = 0x00
+	TABLE_CAS               = 0x01
+	TABLE_TSPMS             = 0x02
+	TABLE_TSDS              = 0x03
+	TABLE_ISO_IEC_14496_SDC = 0x04
+	TABLE_ISO_IEC_14496_ODC = 0x05
+	TABLE_MS                = 0x06
+	TABLE_IPMP_CIS          = 0x07
+	// 0x06 - 0x37 ITU-T Rec. H.222.0 | ISO/IEC 13818-1 reserved
+	// 0x38 - 0x3F Defined in ISO/IEC 13818-6
+	// 0x40 - 0xFE User private
+	// 0xFF Forbidden
+	STREAM_TYPE_VIDEO_MPEG1      = 0x01
+	STREAM_TYPE_VIDEO_MPEG2      = 0x02
+	STREAM_TYPE_AUDIO_MPEG1      = 0x03
+	STREAM_TYPE_AUDIO_MPEG2      = 0x04
+	STREAM_TYPE_PRIVATE_SECTIONS = 0x05
+	STREAM_TYPE_PRIVATE_DATA     = 0x06
+	STREAM_TYPE_MHEG             = 0x07
+
+	STREAM_TYPE_H264   = 0x1B
+	STREAM_TYPE_H265   = 0x24
+	STREAM_TYPE_AAC    = 0x0F
+	STREAM_TYPE_G711A  = 0x90
+	STREAM_TYPE_G711U  = 0x91
+	STREAM_TYPE_G722_1 = 0x92
+	STREAM_TYPE_G723_1 = 0x93
+	STREAM_TYPE_G726   = 0x94
+	STREAM_TYPE_G729   = 0x99
+
+	STREAM_TYPE_ADPCM = 0x11
+	STREAM_TYPE_PCM   = 0x0A
+	STREAM_TYPE_AC3   = 0x81
+	STREAM_TYPE_DTS   = 0x8A
+	STREAM_TYPE_LPCM  = 0x8B
+	// 1110 xxxx
+	// 110x xxxx
+	STREAM_ID_VIDEO = 0xE0 // ITU-T Rec. H.262 | ISO/IEC 13818-2 or ISO/IEC 11172-2 or ISO/IEC14496-2 video stream number xxxx
+	STREAM_ID_AUDIO = 0xC0 // ISO/IEC 13818-3 or ISO/IEC 11172-3 or ISO/IEC 13818-7 or ISO/IEC14496-3 audio stream number x xxxx
+
+	PAT_PKT_TYPE = 0
+	PMT_PKT_TYPE = 1
+	PES_PKT_TYPE = 2
+)
+
+//
+// MPEGTS -> PAT + PMT + PES
+// ES -> PES -> TS
+//
+
+type MpegTsStream struct {
+	PAT       MpegTsPAT // PAT表信息
+	PMT       MpegTsPMT // PMT表信息
+	PESBuffer map[uint16]*MpegTsPESPacket
+	PESChan   chan *MpegTsPESPacket
+}
+
+// ios13818-1-CN.pdf 33/165
+//
+// TS
+//
+
+// Packet == Header + Payload == 188 bytes
+type MpegTsPacket struct {
+	Header  MpegTsHeader
+	Payload []byte
+}
+
+// 前面32bit的数据即TS分组首部,它指出了这个分组的属性
+type MpegTsHeader struct {
+	SyncByte                   byte   // 8 bits  同步字节,固定为0x47,表示后面是一个TS分组
+	TransportErrorIndicator    byte   // 1 bit  传输错误标志位
+	PayloadUnitStartIndicator  byte   // 1 bit  负载单元开始标志(packet不满188字节时需填充).为1时,表示在4个字节后,有一个调整字节
+	TransportPriority          byte   // 1 bit  传输优先级
+	Pid                        uint16 // 13 bits Packet ID号码,唯一的号码对应不同的包.为0表示携带的是PAT表
+	TransportScramblingControl byte   // 2 bits  加密标志位(00:未加密;其他表示已加密)
+	AdaptionFieldControl       byte   // 2 bits  附加区域控制.表示TS分组首部后面是否跟随有调整字段和有效负载.01仅含有效负载(没有adaptation_field),10仅含调整字段(没有Payload),11含有调整字段和有效负载(有adaptation_field,adaptation_field之后是Payload).为00的话解码器不进行处理.空分组没有调整字段
+	ContinuityCounter          byte   // 4 bits  包递增计数器.范围0-15,具有相同的PID的TS分组传输时每次加1,到15后清0.不过,有些情况下是不计数的.
+
+	MpegTsHeaderAdaptationField
+}
+
+// 调整字段,只可能出现在每一帧的开头(当含有pcr的时候),或者结尾(当不满足188个字节的时候)
+// adaptionFieldControl 00 -> 高字节代表调整字段, 低字节代表负载字段 0x20 0x10
+// PCR字段编码在MPEG-2 TS包的自适应字段(Adaptation field)的6个Byte中,其中6 bits为预留位,42 bits为有效位()
+// MpegTsHeaderAdaptationField + stuffing bytes
+type MpegTsHeaderAdaptationField struct {
+	AdaptationFieldLength             byte // 8bits 本区域除了本字节剩下的长度(不包含本字节!!!切记), if adaptationFieldLength > 0, 那么就有下面8个字段. adaptation_field_length 值必须在 0 到 182 的区间内.当 adaptation_field_control 值为'10'时,adaptation_field_length 值必须为 183
+	DiscontinuityIndicator            byte // 1bit 置于"1"时,指示当前传输流包的不连续性状态为真.当 discontinuity_indicator 设置为"0"或不存在时,不连续性状态为假.不连续性指示符用于指示两种类型的不连续性,系统时间基不连续性和 continuity_counter 不连续性.
+	RandomAccessIndicator             byte // 1bit 指示当前的传输流包以及可能的具有相同 PID 的后续传输流包,在此点包含有助于随机接入的某些信息.特别的,该比特置于"1"时,在具有当前 PID 的传输流包的有效载荷中起始的下一个 PES 包必须包含一个 discontinuity_indicator 字段中规定的基本流接入点.此外,在视频情况中,显示时间标记必须在跟随基本流接入点的第一图像中存在
+	ElementaryStreamPriorityIndicator byte // 1bit 在具有相同 PID 的包之间,它指示此传输流包有效载荷内承载的基本流数据的优先级.1->指示该有效载荷具有比其他传输流包有效载荷更高的优先级
+	PCRFlag                           byte // 1bit 1->指示 adaptation_field 包含以两部分编码的 PCR 字段.0->指示自适应字段不包含任何 PCR 字段
+	OPCRFlag                          byte // 1bit 1->指示 adaptation_field 包含以两部分编码的 OPCR字段.0->指示自适应字段不包含任何 OPCR 字段
+	SplicingPointFlag                 byte // 1bit 1->指示 splice_countdown 字段必须在相关自适应字段中存在,指定拼接点的出现.0->指示自适应字段中 splice_countdown 字段不存在
+	TrasportPrivateDataFlag           byte // 1bit 1->指示自适应字段包含一个或多个 private_data 字节.0->指示自适应字段不包含任何 private_data 字节
+	AdaptationFieldExtensionFlag      byte // 1bit 1->指示自适应字段扩展的存在.0->指示自适应字段中自适应字段扩展不存在
+
+	// Optional Fields
+	ProgramClockReferenceBase              uint64 // 33 bits pcr
+	Reserved1                              byte   // 6 bits
+	ProgramClockReferenceExtension         uint16 // 9 bits
+	OriginalProgramClockReferenceBase      uint64 // 33 bits opcr
+	Reserved2                              byte   // 6 bits
+	OriginalProgramClockReferenceExtension uint16 // 9 bits
+	SpliceCountdown                        byte   // 8 bits
+	TransportPrivateDataLength             byte   // 8 bits 指定紧随传输private_data_length 字段的 private_data 字节数. private_data 字节数不能使专用数据扩展超出自适应字段的范围
+	PrivateDataByte                        byte   // 8 bits 不通过 ITU-T|ISO/IEC 指定
+	AdaptationFieldExtensionLength         byte   // 8 bits 指定紧随此字段的扩展的自适应字段数据的字节数,包括要保留的字节(如果存在)
+	LtwFlag                                byte   // 1 bit 1->指示 ltw_offset 字段存在
+	PiecewiseRateFlag                      byte   // 1 bit 1->指示 piecewise_rate 字段存在
+	SeamlessSpliceFlag                     byte   // 1 bit 1->指示 splice_type 以及 DTS_next_AU 字段存在. 0->指示无论是 splice_type 字段还是 DTS_next_AU 字段均不存在
+
+	// Optional Fields
+	LtwValidFlag  byte   // 1 bit 1->指示 ltw_offset 的值必将生效.0->指示 ltw_offset 字段中该值未定义
+	LtwOffset     uint16 // 15 bits 其值仅当 ltw_valid 标志字段具有'1'值时才定义.定义时,法定时间窗补偿以(300/fs)秒为度量单位,其中 fs 为此 PID 所归属的节目的系统时钟频率
+	Reserved3     byte   // 2 bits 保留
+	PiecewiseRate uint32 // 22 bits 只要当 ltw_flag 和 ltw_valid_flag 均置于‘1’时,此 22 比特字段的含义才确定
+	SpliceType    byte   // 4 bits
+	DtsNextAU     uint64 // 33 bits (解码时间标记下一个存取单元)
+
+	// stuffing bytes
+	// 此为固定的 8 比特值等于'1111 1111',能够通过编码器插入.它亦能被解码器丢弃
+}
+
+// ios13818-1-CN.pdf 77
+//
+// Descriptor
+//
+
+type MpegTsDescriptor struct {
+	Tag    byte // 8 bits 标识每一个描述符
+	Length byte // 8 bits 指定紧随 descriptor_length 字段的描述符的字节数
+	Data   []byte
+}
+
+func ReadTsPacket(r io.Reader) (packet MpegTsPacket, err error) {
+	lr := &io.LimitedReader{R: r, N: TS_PACKET_SIZE}
+
+	// header
+	packet.Header, err = ReadTsHeader(lr)
+	if err != nil {
+		return
+	}
+
+	// payload
+	packet.Payload = make([]byte, lr.N)
+	_, err = lr.Read(packet.Payload)
+	if err != nil {
+		return
+	}
+
+	return
+}
+
+func ReadTsHeader(r io.Reader) (header MpegTsHeader, err error) {
+	var h uint32
+
+	// MPEGTS Header 4个字节
+	h, err = util.ReadByteToUint32(r, true)
+	if err != nil {
+		return
+	}
+
+	// payloadUnitStartIndicator
+	// 为1时,表示在4个字节后,有一个调整字节.包头后需要除去一个字节才是有效数据(payload_unit_start_indicator="1")
+	// header.payloadUnitStartIndicator = uint8(h & 0x400000)
+
+	// | 1111 1111 | 0000 0000 | 0000 0000 | 0000 0000 |
+
+	// | 1111 1111 | 0000 0000 | 0000 0000 | 0000 0000 |
+	header.SyncByte = byte((h & 0xff000000) >> 24)
+
+	if header.SyncByte != 0x47 {
+		err = errors.New("mpegts header sync error!")
+		return
+	}
+
+	// | 0000 0000 | 1000 0000 | 0000 0000 | 0000 0000 |
+	header.TransportErrorIndicator = byte((h & 0x800000) >> 23)
+
+	// | 0000 0000 | 0100 0000 | 0000 0000 | 0000 0000 |
+	header.PayloadUnitStartIndicator = byte((h & 0x400000) >> 22)
+
+	// | 0000 0000 | 0010 0000 | 0000 0000 | 0000 0000 |
+	header.TransportPriority = byte((h & 0x200000) >> 21)
+
+	// | 0000 0000 | 0001 1111 | 1111 1111 | 0000 0000 |
+	header.Pid = uint16((h & 0x1fff00) >> 8)
+
+	// | 0000 0000 | 0000 0000 | 0000 0000 | 1100 0000 |
+	header.TransportScramblingControl = byte((h & 0xc0) >> 6)
+
+	// | 0000 0000 | 0000 0000 | 0000 0000 | 0011 0000 |
+	// 0x30 , 0x20 -> adaptation_field, 0x10 -> Payload
+	header.AdaptionFieldControl = byte((h & 0x30) >> 4)
+
+	// | 0000 0000 | 0000 0000 | 0000 0000 | 0000 1111 |
+	header.ContinuityCounter = byte(h & 0xf)
+
+	// | 0010 0000 |
+	// adaptionFieldControl
+	// 表示TS分组首部后面是否跟随有调整字段和有效负载.
+	// 01仅含有效负载(没有adaptation_field)
+	// 10仅含调整字段(没有Payload)
+	// 11含有调整字段和有效负载(有adaptation_field,adaptation_field之后是Payload).
+	// 为00的话解码器不进行处理.空分组没有调整字段
+	// 当值为'11时,adaptation_field_length 值必须在0 到182 的区间内.
+	// 当值为'10'时,adaptation_field_length 值必须为183.
+	// 对于承载PES 包的传输流包,只要存在欠充足的PES 包数据就需要通过填充来完全填满传输流包的有效载荷字节.
+	// 填充通过规定自适应字段长度比自适应字段中数据元的长度总和还要长来实现,以致于自适应字段在完全容纳有效的PES 包数据后,有效载荷字节仍有剩余.自适应字段中额外空间采用填充字节填满.
+	if header.AdaptionFieldControl >= 2 {
+		// adaptationFieldLength
+		header.AdaptationFieldLength, err = util.ReadByteToUint8(r)
+		if err != nil {
+			return
+		}
+
+		if header.AdaptationFieldLength > 0 {
+			lr := &io.LimitedReader{R: r, N: int64(header.AdaptationFieldLength)}
+
+			// discontinuityIndicator(1)
+			// randomAccessIndicator(1)
+			// elementaryStreamPriorityIndicator
+			// PCRFlag
+			// OPCRFlag
+			// splicingPointFlag
+			// trasportPrivateDataFlag
+			// adaptationFieldExtensionFlag
+			var flags uint8
+			flags, err = util.ReadByteToUint8(lr)
+			if err != nil {
+				return
+			}
+
+			header.DiscontinuityIndicator = flags & 0x80
+			header.RandomAccessIndicator = flags & 0x40
+			header.ElementaryStreamPriorityIndicator = flags & 0x20
+			header.PCRFlag = flags & 0x10
+			header.OPCRFlag = flags & 0x08
+			header.SplicingPointFlag = flags & 0x04
+			header.TrasportPrivateDataFlag = flags & 0x02
+			header.AdaptationFieldExtensionFlag = flags & 0x01
+
+			// randomAccessIndicator
+			// 在此点包含有助于随机接入的某些信息.
+			// 特别的,该比特置于"1"时,在具有当前 PID 的传输流包的有效载荷中起始的下一个 PES 包必须包含一个 discontinuity_indicator 字段中规定的基本流接入点.
+			// 此外,在视频情况中,显示时间标记必须在跟随基本流接入点的第一图像中存在
+			if header.RandomAccessIndicator != 0 {
+			}
+
+			// PCRFlag
+			// 1->指示 adaptation_field 包含以两部分编码的 PCR 字段.
+			// 0->指示自适应字段不包含任何 PCR 字段
+			if header.PCRFlag != 0 {
+				var pcr uint64
+				pcr, err = util.ReadByteToUint48(lr, true)
+				if err != nil {
+					return
+				}
+
+				// PCR(i) = PCR_base(i)*300 + PCR_ext(i)
+				// afd.programClockReferenceBase * 300 + afd.programClockReferenceExtension
+				header.ProgramClockReferenceBase = pcr >> 15                // 9 bits  + 6 bits
+				header.ProgramClockReferenceExtension = uint16(pcr & 0x1ff) // 9 bits -> | 0000 0001 | 1111 1111 |
+			}
+
+			// OPCRFlag
+			if header.OPCRFlag != 0 {
+				var opcr uint64
+				opcr, err = util.ReadByteToUint48(lr, true)
+				if err != nil {
+					return
+				}
+
+				// OPCR(i) = OPCR_base(i)*300 + OPCR_ext(i)
+				// afd.originalProgramClockReferenceBase * 300 + afd.originalProgramClockReferenceExtension
+				header.OriginalProgramClockReferenceBase = opcr >> 15                // 9 bits  + 6 bits
+				header.OriginalProgramClockReferenceExtension = uint16(opcr & 0x1ff) // 9 bits -> | 0000 0001 | 1111 1111 |
+			}
+
+			// splicingPointFlag
+			// 1->指示 splice_countdown 字段必须在相关自适应字段中存在,指定拼接点的出现.
+			// 0->指示自适应字段中 splice_countdown 字段不存在
+			if header.SplicingPointFlag != 0 {
+				header.SpliceCountdown, err = util.ReadByteToUint8(lr)
+				if err != nil {
+					return
+				}
+			}
+
+			// trasportPrivateDataFlag
+			// 1->指示自适应字段包含一个或多个 private_data 字节.
+			// 0->指示自适应字段不包含任何 private_data 字节
+			if header.TrasportPrivateDataFlag != 0 {
+				header.TransportPrivateDataLength, err = util.ReadByteToUint8(lr)
+				if err != nil {
+					return
+				}
+
+				// privateDataByte
+				b := make([]byte, header.TransportPrivateDataLength)
+				if _, err = lr.Read(b); err != nil {
+					return
+				}
+			}
+
+			// adaptationFieldExtensionFlag
+			if header.AdaptationFieldExtensionFlag != 0 {
+			}
+
+			// 消耗掉剩下的数据,我们不关心
+			if lr.N > 0 {
+				// Discard 是一个 io.Writer,对它进行的任何 Write 调用都将无条件成功
+				// 但是ioutil.Discard不记录copy得到的数值
+				// 用于发送需要读取但不想存储的数据,目的是耗尽读取端的数据
+				if _, err = io.CopyN(ioutil.Discard, lr, int64(lr.N)); err != nil {
+					return
+				}
+			}
+
+		}
+	}
+
+	return
+}
+
+func WriteTsHeader(w io.Writer, header MpegTsHeader) (written int, err error) {
+	if header.SyncByte != 0x47 {
+		err = errors.New("mpegts header sync error!")
+		return
+	}
+
+	h := uint32(header.SyncByte)<<24 + uint32(header.TransportErrorIndicator)<<23 + uint32(header.PayloadUnitStartIndicator)<<22 + uint32(header.TransportPriority)<<21 + uint32(header.Pid)<<8 + uint32(header.TransportScramblingControl)<<6 + uint32(header.AdaptionFieldControl)<<4 + uint32(header.ContinuityCounter)
+	if err = util.WriteUint32ToByte(w, h, true); err != nil {
+		return
+	}
+
+	written += 4
+
+	if header.AdaptionFieldControl >= 2 {
+		// adaptationFieldLength(8)
+		if err = util.WriteUint8ToByte(w, header.AdaptationFieldLength); err != nil {
+			return
+		}
+
+		written += 1
+
+		if header.AdaptationFieldLength > 0 {
+
+			// discontinuityIndicator(1)
+			// randomAccessIndicator(1)
+			// elementaryStreamPriorityIndicator(1)
+			// PCRFlag(1)
+			// OPCRFlag(1)
+			// splicingPointFlag(1)
+			// trasportPrivateDataFlag(1)
+			// adaptationFieldExtensionFlag(1)
+			threeIndicatorFiveFlags := uint8(header.DiscontinuityIndicator<<7) + uint8(header.RandomAccessIndicator<<6) + uint8(header.ElementaryStreamPriorityIndicator<<5) + uint8(header.PCRFlag<<4) + uint8(header.OPCRFlag<<3) + uint8(header.SplicingPointFlag<<2) + uint8(header.TrasportPrivateDataFlag<<1) + uint8(header.AdaptationFieldExtensionFlag)
+			if err = util.WriteUint8ToByte(w, threeIndicatorFiveFlags); err != nil {
+				return
+			}
+
+			written += 1
+
+			// PCR(i) = PCR_base(i)*300 + PCR_ext(i)
+			if header.PCRFlag != 0 {
+				pcr := header.ProgramClockReferenceBase<<15 | 0x3f<<9 | uint64(header.ProgramClockReferenceExtension)
+				if err = util.WriteUint48ToByte(w, pcr, true); err != nil {
+					return
+				}
+
+				written += 6
+			}
+
+			// OPCRFlag
+			if header.OPCRFlag != 0 {
+				opcr := header.OriginalProgramClockReferenceBase<<15 | 0x3f<<9 | uint64(header.OriginalProgramClockReferenceExtension)
+				if err = util.WriteUint48ToByte(w, opcr, true); err != nil {
+					return
+				}
+
+				written += 6
+			}
+		}
+
+	}
+
+	return
+}
+
+//
+//func (s *MpegTsStream) TestWrite(fileName string) error {
+//
+//	if fileName != "" {
+//		file, err := os.Create(fileName)
+//		if err != nil {
+//			panic(err)
+//		}
+//		defer file.Close()
+//
+//		patTsHeader := []byte{0x47, 0x40, 0x00, 0x10}
+//
+//		if err := WritePATPacket(file, patTsHeader, *s.pat); err != nil {
+//			panic(err)
+//		}
+//
+//		// TODO:这里的pid应该是由PAT给的
+//		pmtTsHeader := []byte{0x47, 0x41, 0x00, 0x10}
+//
+//		if err := WritePMTPacket(file, pmtTsHeader, *s.pmt); err != nil {
+//			panic(err)
+//		}
+//	}
+//
+//	var videoFrame int
+//	var audioFrame int
+//	for {
+//		tsPesPkt, ok := <-s.TsPesPktChan
+//		if !ok {
+//			fmt.Println("frame index, video , audio :", videoFrame, audioFrame)
+//			break
+//		}
+//
+//		if tsPesPkt.PesPkt.Header.StreamID == STREAM_ID_AUDIO {
+//			audioFrame++
+//		}
+//
+//		if tsPesPkt.PesPkt.Header.StreamID == STREAM_ID_VIDEO {
+//			println(tsPesPkt.PesPkt.Header.Pts)
+//			videoFrame++
+//		}
+//
+//		fmt.Sprintf("%s", tsPesPkt)
+//
+//		// if err := WritePESPacket(file, tsPesPkt.TsPkt.Header, tsPesPkt.PesPkt); err != nil {
+//		// 	return err
+//		// }
+//
+//	}
+//
+//	return nil
+//}
+
+func (s *MpegTsStream) ReadPAT(packet *MpegTsPacket, pr io.Reader) (err error) {
+	// 首先找到PID==0x00的TS包(PAT)
+	if PID_PAT == packet.Header.Pid {
+		if len(packet.Payload) == 188 {
+			pr = &util.Crc32Reader{R: pr, Crc32: 0xffffffff}
+		}
+		// Header + PSI + Paylod
+		s.PAT, err = ReadPAT(pr)
+	}
+	return
+}
+func (s *MpegTsStream) ReadPMT(packet *MpegTsPacket, pr io.Reader) (err error) {
+	// 在读取PAT中已经将所有频道节目信息(PMT_PID)保存了起来
+	// 接着读取所有TS包里面的PID,找出PID==PMT_PID的TS包,就是PMT表
+	for _, v := range s.PAT.Program {
+		if v.ProgramMapPID == packet.Header.Pid {
+			if len(packet.Payload) == 188 {
+				pr = &util.Crc32Reader{R: pr, Crc32: 0xffffffff}
+			}
+			// Header + PSI + Paylod
+			s.PMT, err = ReadPMT(pr)
+		}
+	}
+	return
+}
+func (s *MpegTsStream) Feed(ts io.Reader) (err error) {
+	var reader bytes.Reader
+	var lr io.LimitedReader
+	lr.R = &reader
+	var tsHeader MpegTsHeader
+	tsData := make([]byte, TS_PACKET_SIZE)
+	for {
+		_, err = io.ReadFull(ts, tsData)
+		if err == io.EOF {
+			// 文件结尾 把最后面的数据发出去
+			for _, pesPkt := range s.PESBuffer {
+				if pesPkt != nil {
+					s.PESChan <- pesPkt
+				}
+			}
+			return nil
+		} else if err != nil {
+			return
+		}
+		reader.Reset(tsData)
+		lr.N = TS_PACKET_SIZE
+		if tsHeader, err = ReadTsHeader(&lr); err != nil {
+			return
+		}
+		if tsHeader.Pid == PID_PAT {
+			if s.PAT, err = ReadPAT(&lr); err != nil {
+				return
+			}
+			continue
+		}
+		if len(s.PMT.Stream) == 0 {
+			for _, v := range s.PAT.Program {
+				if v.ProgramMapPID == tsHeader.Pid {
+					if s.PMT, err = ReadPMT(&lr); err != nil {
+						return
+					}
+					for _, v := range s.PMT.Stream {
+						s.PESBuffer[v.ElementaryPID] = nil
+					}
+				}
+				continue
+			}
+		} else if pesPkt, ok := s.PESBuffer[tsHeader.Pid]; ok {
+			if tsHeader.PayloadUnitStartIndicator == 1 {
+				if pesPkt != nil {
+					s.PESChan <- pesPkt
+				}
+				pesPkt = &MpegTsPESPacket{}
+				s.PESBuffer[tsHeader.Pid] = pesPkt
+				if pesPkt.Header, err = ReadPESHeader(&lr); err != nil {
+					return
+				}
+			}
+			io.Copy(&pesPkt.Payload, &lr)
+		}
+	}
+}

+ 520 - 0
m7s.live/engine/v4@v4.14.6/codec/mpegts/mpegts.md

@@ -0,0 +1,520 @@
+#MPEGTS
+
+----------
+
+Name:苏荣
+Data:2016/5/27 09:03:30
+
+
+----------
+
+## PSI(Program Specific Information) 节目特定信息
+PSI 可以认为属于 6 个表:
+1) 节目相关表(PAT)
+2) TS 节目映射表(PMT)
+3) 网络信息表(NIT)
+4) 有条件访问表(CAT)
+5) 传输流描述表
+6) IPMP 控制信息表
+
+##ES流(Elementary Stream):基本码流,不分段的音频、视频或其他信息的连续码流.
+
+##PES流:把基本流ES分割成段,并加上相应头文件打包成形的打包基本码流
+
+##PS流(Program Stream):节目流,将具有共同时间基准的一个或多个PES组合(复合)而成的单一数据流(用于播放或编辑系统,如m2p).
+
+##TS流(Transport Stream):传输流,将具有共同时间基准或独立时间基准的一个或多个PES组合(复合)而成的单一数据流(用于数据传输).
+
+##PES ES TS
+视频压缩成H264码流,可以称之为ES流,将其每帧打包为PES流,然后分拆为多个188字节,称为TS流.
+
+H264(ES) = PES1(一帧ES打包) + PES2(一帧ES打包) + PES3(一帧ES打包) + ...
+
+PES1 = PES1 Header + PES1 Payload = PES1 Packet Start Code Prefix + Stream ID + PES1 Packet Length + Send PES1 Header(不确定大小) + PES1 Payload
+
+PES1 Payload = TS1 Payload + TS2 Payload + TS3 Payload + ...
+
+PES1 = TS1 + TS2 + TS3 + ....
+
+PES1 = TS1(TS1 Header + PES1 Header + TS1 Payload) + TS2(有三种可能) + TS3(有三种可能) + ......
+
+TS1(TS流第一个包) = TS1 Header + PES1 Header + TS1 Payload
+
+TS2(TS流第二个包,第一种情况)  = TS2 Header + 自适应字段 + TS2 Payload (出现概率 1%)
+
+TS2(TS流第二个包,第二种情况) = TS2 Header + 自适应字段 (出现概率 0.1%)
+
+TS2(TS流第二个包,第三种情况) = TS2 Header + TS2 Payload (出现概率 98.9%)
+
+一段ES流 = N个PES(N帧)
+
+同一个PES的TS的PID是相同的
+
+##寻找第一个TS包
+Header PID =  0x000 说明数据包是PAT表信息
+第一个TS包 一般叫做 PAT (Program Association Table,节目相关表)
+
+TS流 : PID=005 + PID=002 + PID=000
+
+一般来说第一个TS包一般在第一个位置,本例举出一个特殊情况(第一个TS包在第三)
+
+在寻找第一个TS包时,不断读取TS包,直到找到pid=000的位置,并将读取过的TS包置入缓冲区
+
+##寻找下一个TS包
+第二个TS包 一般叫做PMT(Program Map Table,节目映射表)
+
+##解析TS包
+payload_unit_start_indicator : 该字段用来表示TS包的有效净荷有PES包或者PSI数据的情况.
+
+当TS包带有PES包数据时(出现概率99.9%).不带PES包(出现概率0.1%).
+
+1. 当TS包带有PES包数据时,payload_unit_start_indicator具有以下的特点:
+a. 置为1,标识TS包的有效净荷以PES包的第一个字节开始.
+b. 置为0,表示TS包的开始不是PES包.
+
+2. 当TS包带有PSI数据时,payload_unit_start_indicator具有以下特点:
+a. 置为1,表示TS包带有PSI部分的第一个字节,即第一个字节带有指针pointer_field.
+b. 置为0,表示TS包不带有一个PSI部分的第一个字节,即在有效净荷中没有指针point_field.
+c. 对于空包的包,payload_unit_start_indicator应该置为0
+
+adaptionFieldControl:
+01 -> 仅含有效负载(TS包第三种情况)
+10 -> 仅含调整字段(TS包第二种情况)
+11 -> 含有调整字段和有效负载(TS包第一种情况)
+
+TS流,通过一个个的TS包来传送. TS包可以是传送PSI SI等各表的数据包,也可以是传送节目音视频数据(携带的PES包:音视频基本流包)的包;TS携带 PSI SI等表的数据时,各个表以各表对应的Section语法格式做为传输单元存放到TS包中 以便传输;
+TS包,有一个TS包的PID,系统就是根据这个PID来找对应的TS包;对于包含音视频数据(PES包)的TS包,系统通过TS的PID找到对应TS数据包,提取其中的数据组合成节目的音视频;对于携带PSI SI等数据的TS包,系统通过TS的PID找到对应TS数据包,提取各个PSI SI数据表格,用来指导系统;因此其中部分PID用来固定传输某些数据内容.
+
+有了TS的PID后, 如果TS包携带的是PSI SI等表格的Section数据时,有时还不能确定该PID的TS包中携带的数据是什么,SDT BAT ST 等表传送时,都用的是PID为0X0011的TS数据包,对于这种携带PSI SI Section单元的TS包,对应的数据(表的Section语法中)还有一个 TABLE_ID字段,用来可以确定是具体的什么表
+
+因此PID+TableID就可以确定负载带了什么,是PES还是PSI.
+
+
+----------
+
+
+1. 第一个包:
+
+包头 : 47 60 00 10 
+0x47  : syncByte
+0x6   : 0110(这里的最后一个字节,要给到下面),payload_unit_start_indicator = 1.
+0x000 : 0 0000 0000 0000, pid = 0,说明是第一个TS包(PAT表)
+0x10  : 0001 0000, adaptionFieldControl = 01,说明仅含有效负载(TS包第三种情况)
+
+负载 : 00 00 B0 0D 00 00 C1 00 00 00 01 E0
+81 0C 8C BE 32 FF FF......FF
+
+指针                : 00
+table id            : 00
+固定值              : B (1011)
+section_length      : 0 0D(值:13)
+transport_stream_id : 00 00
+version number & current_next_indicator : C1
+section_number      : 00
+last_section_number : 00
+program_number      : 00 01
+program_map_PID     : E0 81(因为program_number > 0)
+CRC_32              : 0C 8C BE 32
+
+    if (program_number == 0)
+    {
+        network_PID
+    }else
+    {
+        program_map_PID
+    }
+
+E0 81 = reserved3 + program_map_PID =  | 1110 0000 | 1000 0001 |
+program_map_PID = 0x81(说明PMT的pid为081)
+
+
+----------
+
+
+2. 第二个包
+
+包头 : 47 60 81 10
+0x47  : syncByte
+0x6   : 0110(这里的最后一个字节,要给到下面),payload_unit_start_indicator = 1.
+0x081 : 0 0000 1000 0001, pid = 0x081(说明是PMT表,因为前面的PAT表给出了)
+0x10  : 0001 0000, adaptionFieldControl = 01,说明仅含有效负载(TS包第三种情况)
+
+负载 : 00 02 B0 17 00 01 C1 00 00 E8 10 F0 00 1B E8 10
+F0 00 03 E8 14 F0 00 66 74 A4 2D FF FF FF FF FF......FF
+
+指针                : 00
+table id            : 02
+固定值              : B
+section_length      : 0 17(值:23,表示到后面FF FF FF FF FF FF之前总共有23个字节)
+program_number      : 00 01
+reserved2 & version_number & current_next_indicator : C1
+section_number      : 00
+last_section_number : 00
+PCR_PID             : E8 10
+program_info_length : F0 00  前4位为保留位 后12位为描述信息长度 此处为0
+
+第一流分析 : 1B E8 10 F0 00
+stream_type         : 1B  视频流(H264)(ITU-T H.264建议书| SO/IEC 14496-10 视频中定义的 AVC 视频流)
+elementary_PID      : E8 10  前3位为保留位取后13位  则PID=810 表示此PID的都是视频流
+ES_info_length      : F0 00 前4位为保留位 后12位为描述信息长度 此处为0
+
+第二流分析 : 03 E8 14 F0 00
+stream_type         : 03  音频流(MP3)
+elementary_PID      : E8 14  前3位为保留位取后13位  则PID=814 表示此PID的都是音频流
+ES_info_length      : F0 00 前4位为保留位 后12位为描述信息长度 此处为0
+
+
+
+CRC                 : 66 74 A4 2D
+
+
+reserved4 + program_info_length = | 1111 0000 | 0000 0000 |
+program_info_length = 0
+
+stream_type : 03 表示流是音频流 MP3 格式   814  表示  pid=814 的TS包存储的是MP3格式的音频流.
+stream_type : 01 表示流是视频流 h264格式   810  表示  pid=810 的TS包存储的是h264格式的视频流
+
+
+----------
+
+
+3. 第三个包
+包头 : 47 48 14 10
+0x47  : syncByte
+0x4   : 0100(这里的最后一个字节,要给到下面),payload_unit_start_indicator = 1.
+0x814 : 0 1000 0001 0100, pid = 0x814(音频MP3)
+0x10  : 0001 0000, adaptionFieldControl = 01
+
+这里:
+payload_unit_start_indicator = 1, 说明有效载荷起始符为1,含有PES包头
+adaptionFieldControl = 01, 说明仅含有效负载(TS包第三种情况)
+
+负载 : 00 00 01 C0 01 88 80 80 05 21 00 01 96 07 FF FD 85 00 33 22 22 11 22 11 11 11 11 11 11 24 82 41 00 90 40 00 00 00 00 00 40 00 ....... 70 34 5B CE 64 B7 D2 F5 4E 07 50 8E 11 1E 60 61 21 32 11 59
+
+packetStartCodePrefix   : 00 00 01 
+streamID                : C0
+pes_PacketLength        : 01 88(值为392,占用392个字节,一帧数据长度,也可以置为0)
+Sned PES HEADER         : 占用不确定位 本例为:80 80 05 21 00 01 96 07
+
+
+Sned PES HEADER 包括以下几个字段: 80 80 05 21 00 01 96 07(解析为二进制显示)
+| 8    0   | 8    0    | 0    5    | 2    1    | 0    0    | 0    1    | 9    6    | 0    7    |
+| 1000 0000| 1000 0000 | 0000 0101 | 0010 0001 | 0000 0000 | 0000 0001 | 1001 0110 | 0000 1110 |
+
+(注意,下面的数值是用二进制表示,不特别声明,都是用16进制表示)
+(0x80)
+constTen                    : 10 固定
+PES_scrambling_control      : 00 PES加扰控制
+PES_priority                : 0 PES 包中该有效载荷的优先级
+data_alignment_indicator    : 0 数据定位指示符
+copyright                   : 0 PES 包有效载荷的素材依靠版权所保护
+original_or_copy            : 0 PES 包有效载荷的内容是原始的
+
+(0x80)
+PTS_DTS_flags               : 10 PES 包头中 PTS 字段存在
+ESCR_flag                   : 0
+ES_rate_flag                : 0
+DSM_trick_mode_flag         : 0
+additional_copy_info_flag   : 0
+PES_CRC_flag                : 0
+PES_extension_flag          : 0
+
+(0x05)
+PES_header_data_length      : 0000 0101(值为5)PES头数据长度,表示后面还有5个字节,之后就是一帧的数据
+
+(0x4200032C)(十进制:1107297068)
+PTS(presentation time stamp): 0010 0001 0000 0000 0000 0001 1001 0110 0 
+
+下面字段在本例中都没有:
+ESCR(42) = ESCR_base(33) + ESCR_extension(9)
+ES_rate(22)
+DSM特技方式(8)
+additional_copy_info(7)
+previous_PES_packet_CRC(16)
+PES_Extension(不确定)
+
+
+因为 PTS_DTS_flags == 10,所以本例中只有PTS没有DTS.
+
+
+注意 : 本TS包 包含PES头信息 说明开始下一帧
+
+----------
+
+
+4. 第四个包
+包头 : 47 08 14 11
+0x47  : syncByte
+0x0   : 0000(这里的最后一个字节,要给到下面),payload_unit_start_indicator = 0.
+0x814 : 0 1000 0001 0100, pid = 0x814(音频MP3)
+0x11  : 0001 0001, adaptionFieldControl = 01
+
+这里:
+payload_unit_start_indicator = 0, 说明有效载荷起始符为0,不含有PES包头
+adaptionFieldControl = 01, 说明仅含有效负载(TS包第三种情况)
+
+----------
+
+
+5. 第五个包
+包头 : 47 08 14 32
+0x47  : syncByte
+0x0   : 0000(这里的最后一个字节,要给到下面),payload_unit_start_indicator = 0.
+0x814 : 0 1000 0001 0100, pid = 0x814(音频MP3)
+0x32  : 0011 0010, adaptionFieldControl = 11
+
+这里:
+payload_unit_start_indicator = 0, 说明有效载荷起始符为0,不含有PES包头
+adaptionFieldControl = 11, 说明先有自适应字段,再有有效载荷(TS包第一种情况)
+
+负载 : 99 00 FF FF FF ... FF 52 DE E6 B5 D0 76 CD CB B2 24 B3 92 AD 4E CD 19 D2 CC 82 D4 78 10 80 6C 0E 99 49 A4 59 C0
+
+adaptation_field_length : 99(值为153,表示占用153个字节)
+
+discontinuity_indicator & random_access_indicator & 
+elementary_stream_priority_indicator & PCR_flag & 
+OPCR_flag & splicing_point_flag & 
+transport_private_data_flag & adaptation_field_extension_flag : 00 剩下的所有字段都为0
+
+(00 FF FF FF ... FF)这里都是调整字段,从52 DE E6 B5 D0(从00(FF之前,99之后) 开始算是第1个字节,跳到第153个字节)开始,就是真正的帧数据了
+
+
+----------
+
+
+6. 第六个包
+包头 : 47 48 14 13
+0x47  : syncByte
+0x4   : 0100(这里的最后一个字节,要给到下面),payload_unit_start_indicator = 1.
+0x814 : 0 1000 0001 0100, pid = 0x814(音频MP3)
+0x13  : 0001 0011, adaptionFieldControl = 01,说明仅含有效负载(TS包第三种情况)
+
+这里:
+payload_unit_start_indicator = 1, 说明有效载荷起始符为1,含有PES包头
+adaptionFieldControl = 01, 说明仅含有效负载(TS包第三种情况)
+
+负载 : 00 00 01 C0 01 88 80 80 05 21 00 01 A6 E7 FF FD
+
+packetStartCodePrefix   : 00 00 01 
+streamID                : C0
+pes_PacketLength        : 01 88(值为392,占用392个字节)
+Sned PES HEADER         : 占用不确定位
+
+所以本包数据流ID 和 第二个包的流ID是一样的
+
+注意 : 本TS包 又包含PES头信息 说明开始下一帧
+
+
+----------
+
+7. 第七个包
+包头 : 47 48 10 30
+0x47  : syncByte
+0x4   : 0100(这里的最后一个字节,要给到下面),payload_unit_start_indicator = 1.
+0x810 : 0 1000 0001 0000, pid = 0x810(视频H264)
+0x30  : 0011 0000, adaptionFieldControl = 11,说明含有调整字段和有效负载(TS包第一种情况)
+
+这里:
+payload_unit_start_indicator = 1, 说明有效载荷起始符为1,含有PES包头
+adaptionFieldControl = 11, 说明含有调整字段和有效负载(TS包第一种情况)
+
+负载 : 07 10 00 00 01 0F 7E 88 00 00 01 E0 00 00 80 C0 0A 31 00 01 96 07 11 00 01 7E 91 00 00 00 01 67 4D 40 1E 96 ...... D2 99 71 F3
+
+adaptation_field_length : 07(值为7,表示占用153个字节)
+
+discontinuity_indicator & random_access_indicator & 
+elementary_stream_priority_indicator & PCR_flag & 
+OPCR_flag & splicing_point_flag & 
+transport_private_data_flag & adaptation_field_extension_flag : 10
+
+(10 00 00 01 0F 7E 88)调整字段
+
+packetStartCodePrefix   : 00 00 01 
+streamID                : EO
+pes_PacketLength        : 00 00(值为0,占用0个字节,一帧数据长度,也可以置为0,此时需要自己去计算)
+Sned PES HEADER         : 占用不确定位
+
+
+----------
+
+
+8. 第八个包 
+包头 : 47 08 10 11
+0x47  : syncByte
+0x0   : 0000(这里的最后一个字节,要给到下面),payload_unit_start_indicator = 0.
+0x810 : 0 1000 0001 0000, pid = 0x810(视频H264)
+0x11  : 0001 0001, adaptionFieldControl = 01, 说明仅含有效负载(TS包第三种情况)
+
+这里:
+payload_unit_start_indicator = 0, 说明有效载荷起始符为0,不含有PES包头
+adaptionFieldControl = 01, 说明仅含有效负载(TS包第三种情况)
+
+
+----------
+
+总结这个八个包:
+
+第一个TS包(PID:0X00)  : 包含了PAT.
+第二个TS包(PID:0X81)  : 包含了PMT.
+第三个TS包(PID:0x814) : 音频PES包头所有的TS包.
+第四个TS包(PID:0x814) : 音频TS包.
+第五个TS包(PID:0x814) : 音频TS包.
+第六个TS包(PID:0x814) : 音频PES包头所有的TS包.
+第七个TS包(PID:0x810) : 视频PES包头所有的TS包.
+第八个TS包(PID:0x810) : 视频TS包.
+
+
+----------
+
+
+// Packet Header:
+// PID是TS流中唯一识别标志,Packet Data是什么内容就是由PID决定的.如果一个TS流中的一个Packet的Packet Header中的PID是0x0000,
+// 那么这个Packet的Packet Data就是DVB的PAT表而非其他类型数据(如Video,Audio或其他业务信息).
+
+// 分析一个Header:
+// 二进制:   0100 0111 0000 0111 1110 0101 0001 0010
+// 十六进制: 4    7    0    7    e    5    1    2
+
+// syncByte = 0x47          就是0x47,这是DVB TS规定的同步字节,固定是0x47
+// transportErrorIndicator = 0    表示当前包没有发生传输错误
+// payloadUnitStartIndicator = 0  具体含义参考ISO13818-1标准文档
+// transportPriority = 0      表示当前包是低优先级
+// pid = 0x07e5(0 0111 1110 0101) Video PID
+// transportScramblingControl = 00  表示节目没有加密
+// adaptionFieldControl = 01    具体含义参考ISO13818-1标准文档
+// continuityCounter = 0010     表示当前传送的相同类型的包是第3个
+
+
+----------
+
+
+// 分析一段TS流:(PAT)
+// Packet Header : 0x47 0x40 0x00 0x10
+// Packet Data   : 00 00 b0 11 00 01 c1 00 00 00 00 e0 1f 00 01 e1 00 24 ac48 84 ff ff ... ff ff
+
+// Header PID = 0x0000 说明数据包是PAT表信息,包头后需要除去一个字节才是有效数据(payload_unit_start_indicator="1")
+// 所以,Packet Data就应该是 : 00 b0 11 00 01 c1 00 00 00 00 e0 1f 00 01 e1 00 24 ac48 84 ff ff ... ff ff
+
+//
+// 00 | b0 11 | 00 01 | c1 | 00 | 00 | 00 00 | e0 1f | 00 01 e1 00 |
+//
+
+// table_id = 0000 0000
+
+// section_syntax_indicator = 1
+// zero = 0
+// reserved1 = 11
+// sectionLength = 0000 0001 0001
+
+// transportStreamID = 0000 0000 0000 0001
+
+// reserved2 = 11
+// versionNumber = 0000 0
+// currentNextIndicator 1
+
+// sectionNumber = 0000 0000
+
+// lastSectionNumber = 0000 0000
+
+// programNumber = 0000 0000 0000 0000
+
+// reserved3 = 111
+// networkPID = 0 0000 0001 1111
+
+// crc32
+
+
+----------
+
+
+// 分析一段TS流:(PMT)
+// Packet Header : 0x47 0x43 0xe8 0x12
+// Packet Data   : 00 02 b0 12 00 01 c1 00 00 e3 e9 f0 00 1b e3 e9 f0 00 f0 af b4 4f ff ff ... ff ff
+
+// Header PID = 0x03e8 说明数据包是PMT表信息,包头后需要除去一个字节才是有效数据(payload_unit_start_indicator="1")
+// 所以,Packet Data就应该是 : 02 b0 12 00 01 c1 00 00 e3 e9 f0 00 1b e3 e9 f0 00 f0 af b4 4f ff ff ... ff ff
+
+// 1    2       3       4    5    6    7       8       9    10      11      12
+// 02 | b0 12 | 00 01 | c1 | 00 | 00 | e3 e9 | f0 00 | 1b | e3 e9 | f0 00 | f0 af b4 4f |
+//
+
+// 1:
+// table_id = 0000 0010
+
+// 2:
+// section_syntax_indicator = 1
+// zero = 0
+// reserved1 = 11
+// section_length = 0000 0001 0010
+
+// 3:
+// program_number = 0000 0000 0000 0001
+
+// 4:
+// reserved2 = 11
+// version_number = 00 000
+// current_next_indicator = 1
+
+// 5:
+// section_number = 0000 0000
+
+// 6:
+// last_section_number = 0000 0000
+
+// 7:
+// reserved3 = 111
+// PCR_PID = 0 0011 1110 1001
+
+// 8:
+// reserved4 = 1111
+// program_info_length = 0000 0000 0000
+
+// 9:
+// stream_type = 0001 1011
+
+// 10:
+// reserved5 = 111
+// elementary_PID = 0 0011 1110 1001
+
+// 11:
+// reserved6 = 1111
+// ES_info_length = 0000 0000 0000
+
+// 12:
+// crc
+
+
+----------
+
+
+##TS流解码过程
+1. 获取TS中的PAT
+2. 获取TS中的PMT
+3. 根据PMT可以知道当前网络中传输的视频(音频)类型(H264),相应的PID,PCR的PID等信息.
+4. 设置demux 模块的视频Filter 为相应视频的PID和stream type等.
+5. 从视频Demux Filter 后得到的TS数据包中的payload 数据就是 one piece of PES,在TS header中有一些关于此 payload属于哪个 PES的 第多少个数据包. 因此软件中应该将此payload中的数据copy到PES的buffer中,用于拼接一个PES包.
+6. 拼接好的PES包的包头会有 PTS,DTS信息,去掉PES的header就是 ES.
+7. 直接将 被被拔掉 PES包头的ES包送给decoder就可以进行解码.解码出来的数据就是一帧一帧的视频数据,这些数据至少应当与PES中的PTS关联一下,以便进行视音频同步.
+8. I,B,B,P 信息是在ES中的.
+
+
+----------
+
+
+1. 首先找到PID为0x00的TS包,找到里面的节目映射表(PMT)PID,因为可能有几个节目信息.所以可能有几个PMT_PID,以一个为例
+2. 接着查找该PMT_PID的TS包,通常就紧接着.在该PMT包中找音频和视频的PID.以视频为例.
+3. 开始提取一帧ES数据
+  3.1  查找视频PID的TS包
+  3.2  找PES包头,方法:TS包头第2个字节的高6位(有效载荷单元起始指示符)为1的TS包,跳过自适应字段,找到PES包头,提取时间戳,再跳至ES数据,这就是一帧ES数据的开始部分.
+  3.3  查找有效载荷单元起始指示符为0的TS包.跳过TS包头,跳过自适应字段,提取后面的ES数据
+  3.4  同3.3接着查找
+  3.5  当碰到有效载荷单元起始指示符又变为1的视频TS包,就知道这是下一帧的开始了,将前面的所有ES数据组合成一帧数据.开始下一轮组帧.
+
+
+----------
+
+
+##参考文档:
+
+1. [TS流](http://blog.csdn.net/cabbage2008/article/category/5885203)
+1. [TS各个表 与 SECTION 的解析 CAS原理 ](http://blog.sina.com.cn/s/blog_6b94d5680101r5l6.html)

+ 72 - 0
m7s.live/engine/v4@v4.14.6/codec/mpegts/mpegts_crc32.go

@@ -0,0 +1,72 @@
+package mpegts
+
+import "net"
+
+// http://www.stmc.edu.hk/~vincent/ffmpeg_0.4.9-pre1/libavformat/mpegtsenc.c
+
+var Crc32_Table = []uint32{
+	0x00000000, 0x04c11db7, 0x09823b6e, 0x0d4326d9, 0x130476dc, 0x17c56b6b,
+	0x1a864db2, 0x1e475005, 0x2608edb8, 0x22c9f00f, 0x2f8ad6d6, 0x2b4bcb61,
+	0x350c9b64, 0x31cd86d3, 0x3c8ea00a, 0x384fbdbd, 0x4c11db70, 0x48d0c6c7,
+	0x4593e01e, 0x4152fda9, 0x5f15adac, 0x5bd4b01b, 0x569796c2, 0x52568b75,
+	0x6a1936c8, 0x6ed82b7f, 0x639b0da6, 0x675a1011, 0x791d4014, 0x7ddc5da3,
+	0x709f7b7a, 0x745e66cd, 0x9823b6e0, 0x9ce2ab57, 0x91a18d8e, 0x95609039,
+	0x8b27c03c, 0x8fe6dd8b, 0x82a5fb52, 0x8664e6e5, 0xbe2b5b58, 0xbaea46ef,
+	0xb7a96036, 0xb3687d81, 0xad2f2d84, 0xa9ee3033, 0xa4ad16ea, 0xa06c0b5d,
+	0xd4326d90, 0xd0f37027, 0xddb056fe, 0xd9714b49, 0xc7361b4c, 0xc3f706fb,
+	0xceb42022, 0xca753d95, 0xf23a8028, 0xf6fb9d9f, 0xfbb8bb46, 0xff79a6f1,
+	0xe13ef6f4, 0xe5ffeb43, 0xe8bccd9a, 0xec7dd02d, 0x34867077, 0x30476dc0,
+	0x3d044b19, 0x39c556ae, 0x278206ab, 0x23431b1c, 0x2e003dc5, 0x2ac12072,
+	0x128e9dcf, 0x164f8078, 0x1b0ca6a1, 0x1fcdbb16, 0x018aeb13, 0x054bf6a4,
+	0x0808d07d, 0x0cc9cdca, 0x7897ab07, 0x7c56b6b0, 0x71159069, 0x75d48dde,
+	0x6b93dddb, 0x6f52c06c, 0x6211e6b5, 0x66d0fb02, 0x5e9f46bf, 0x5a5e5b08,
+	0x571d7dd1, 0x53dc6066, 0x4d9b3063, 0x495a2dd4, 0x44190b0d, 0x40d816ba,
+	0xaca5c697, 0xa864db20, 0xa527fdf9, 0xa1e6e04e, 0xbfa1b04b, 0xbb60adfc,
+	0xb6238b25, 0xb2e29692, 0x8aad2b2f, 0x8e6c3698, 0x832f1041, 0x87ee0df6,
+	0x99a95df3, 0x9d684044, 0x902b669d, 0x94ea7b2a, 0xe0b41de7, 0xe4750050,
+	0xe9362689, 0xedf73b3e, 0xf3b06b3b, 0xf771768c, 0xfa325055, 0xfef34de2,
+	0xc6bcf05f, 0xc27dede8, 0xcf3ecb31, 0xcbffd686, 0xd5b88683, 0xd1799b34,
+	0xdc3abded, 0xd8fba05a, 0x690ce0ee, 0x6dcdfd59, 0x608edb80, 0x644fc637,
+	0x7a089632, 0x7ec98b85, 0x738aad5c, 0x774bb0eb, 0x4f040d56, 0x4bc510e1,
+	0x46863638, 0x42472b8f, 0x5c007b8a, 0x58c1663d, 0x558240e4, 0x51435d53,
+	0x251d3b9e, 0x21dc2629, 0x2c9f00f0, 0x285e1d47, 0x36194d42, 0x32d850f5,
+	0x3f9b762c, 0x3b5a6b9b, 0x0315d626, 0x07d4cb91, 0x0a97ed48, 0x0e56f0ff,
+	0x1011a0fa, 0x14d0bd4d, 0x19939b94, 0x1d528623, 0xf12f560e, 0xf5ee4bb9,
+	0xf8ad6d60, 0xfc6c70d7, 0xe22b20d2, 0xe6ea3d65, 0xeba91bbc, 0xef68060b,
+	0xd727bbb6, 0xd3e6a601, 0xdea580d8, 0xda649d6f, 0xc423cd6a, 0xc0e2d0dd,
+	0xcda1f604, 0xc960ebb3, 0xbd3e8d7e, 0xb9ff90c9, 0xb4bcb610, 0xb07daba7,
+	0xae3afba2, 0xaafbe615, 0xa7b8c0cc, 0xa379dd7b, 0x9b3660c6, 0x9ff77d71,
+	0x92b45ba8, 0x9675461f, 0x8832161a, 0x8cf30bad, 0x81b02d74, 0x857130c3,
+	0x5d8a9099, 0x594b8d2e, 0x5408abf7, 0x50c9b640, 0x4e8ee645, 0x4a4ffbf2,
+	0x470cdd2b, 0x43cdc09c, 0x7b827d21, 0x7f436096, 0x7200464f, 0x76c15bf8,
+	0x68860bfd, 0x6c47164a, 0x61043093, 0x65c52d24, 0x119b4be9, 0x155a565e,
+	0x18197087, 0x1cd86d30, 0x029f3d35, 0x065e2082, 0x0b1d065b, 0x0fdc1bec,
+	0x3793a651, 0x3352bbe6, 0x3e119d3f, 0x3ad08088, 0x2497d08d, 0x2056cd3a,
+	0x2d15ebe3, 0x29d4f654, 0xc5a92679, 0xc1683bce, 0xcc2b1d17, 0xc8ea00a0,
+	0xd6ad50a5, 0xd26c4d12, 0xdf2f6bcb, 0xdbee767c, 0xe3a1cbc1, 0xe760d676,
+	0xea23f0af, 0xeee2ed18, 0xf0a5bd1d, 0xf464a0aa, 0xf9278673, 0xfde69bc4,
+	0x89b8fd09, 0x8d79e0be, 0x803ac667, 0x84fbdbd0, 0x9abc8bd5, 0x9e7d9662,
+	0x933eb0bb, 0x97ffad0c, 0xafb010b1, 0xab710d06, 0xa6322bdf, 0xa2f33668,
+	0xbcb4666d, 0xb8757bda, 0xb5365d03, 0xb1f740b4,
+}
+
+func GetCRC32(data []byte) (crc uint32) {
+	crc = 0xffffffff
+
+	for _, v := range data {
+		crc = (crc << 8) ^ Crc32_Table[((crc>>24)^uint32(v))&0xff]
+
+	}
+
+	return
+}
+
+func GetCRC32_2(data net.Buffers) (crc uint32) {
+	crc = 0xffffffff
+	for _, v := range data {
+		for _, v2 := range v {
+			crc = (crc << 8) ^ Crc32_Table[((crc>>24)^uint32(v2))&0xff]
+		}
+	}
+	return
+}

+ 230 - 0
m7s.live/engine/v4@v4.14.6/codec/mpegts/mpegts_pat.go

@@ -0,0 +1,230 @@
+package mpegts
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+
+	"m7s.live/engine/v4/util"
+)
+
+// ios13818-1-CN.pdf 43(57)/166
+//
+// PAT
+//
+
+var DefaultPATPacket = []byte{
+	// TS Header
+	0x47, 0x40, 0x00, 0x10,
+
+	// Pointer Field
+	0x00,
+
+	// PSI
+	0x00, 0xb0, 0x0d, 0x00, 0x01, 0xc1, 0x00, 0x00,
+
+	// PAT
+	0x00, 0x01, 0xe1, 0x00,
+
+	// CRC
+	0xe8, 0xf9, 0x5e, 0x7d,
+
+	// Stuffing 167 bytes
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+	0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
+}
+
+// TS Header :
+// SyncByte = 0x47
+// TransportErrorIndicator = 0(B:0), PayloadUnitStartIndicator = 1(B:0), TransportPriority = 0(B:0),
+// Pid = 0,
+// TransportScramblingControl = 0(B:00), AdaptionFieldControl = 1(B:01), ContinuityCounter = 0(B:0000),
+
+// PSI :
+// TableID = 0x00,
+// SectionSyntaxIndicator = 1(B:1), Zero = 0(B:0), Reserved1 = 3(B:11),
+// SectionLength = 13(0x00d)
+// TransportStreamID = 0x0001
+// Reserved2 = 3(B:11), VersionNumber = (B:00000), CurrentNextIndicator = 1(B:0),
+// SectionNumber = 0x00
+// LastSectionNumber = 0x00
+
+// PAT :
+// ProgramNumber = 0x0001
+// Reserved3 = 15(B:1110), ProgramMapPID = 4097(0x1001)
+
+// PAT表主要包含频道号码和每一个频道对应的PMT的PID号码,这些信息我们在处理PAT表格的时候会保存起来,以后会使用到这些数据
+type MpegTsPATProgram struct {
+	ProgramNumber uint16 // 16 bit 节目号
+	Reserved3     byte   // 3 bits 保留位
+	NetworkPID    uint16 // 13 bits 网络信息表(NIT)的PID,节目号为0时对应的PID为network_PID
+	ProgramMapPID uint16 // 13 bit 节目映射表的PID,节目号大于0时对应的PID.每个节目对应一个
+}
+
+// Program Association Table (节目关联表)
+// 节目号为0x0000时,表示这是NIT,PID=0x001f,即3.
+// 节目号为0x0001时,表示这是PMT,PID=0x100,即256
+type MpegTsPAT struct {
+	// PSI
+	TableID                byte   // 8 bits 0x00->PAT,0x02->PMT
+	SectionSyntaxIndicator byte   // 1 bit  段语法标志位,固定为1
+	Zero                   byte   // 1 bit  0
+	Reserved1              byte   // 2 bits 保留位
+	SectionLength          uint16 // 12 bits 该字段的头两比特必为'00',剩余 10 比特指定该分段的字节数,紧随 section_length 字段开始,并包括 CRC.此字段中的值应不超过 1021(0x3FD)
+	TransportStreamID      uint16 // 16 bits 该字段充当标签,标识网络内此传输流有别于任何其他多路复用流.其值由用户规定
+	Reserved2              byte   // 2 bits  保留位
+	VersionNumber          byte   // 5 bits  范围0-31,表示PAT的版本号
+	CurrentNextIndicator   byte   // 1 bit  发送的PAT是当前有效还是下一个PAT有效,0则要等待下一个表
+	SectionNumber          byte   // 8 bits  分段的号码.PAT可能分为多段传输.第一段为00,以后每个分段加1,最多可能有256个分段
+	LastSectionNumber      byte   // 8 bits  最后一个分段的号码
+
+	// N Loop
+	Program []MpegTsPATProgram // PAT表里面的所有频道索引信息
+
+	Crc32 uint32 // 32 bits 包含处理全部传输流节目映射分段之后,在附件 B 规定的解码器中给出寄存器零输出的 CRC 值
+}
+
+func ReadPAT(r io.Reader) (pat MpegTsPAT, err error) {
+	lr, psi, err := ReadPSI(r, PSI_TYPE_PAT)
+	if err != nil {
+		return
+	}
+
+	pat = psi.Pat
+
+	// N Loop
+	// 一直循环去读4个字节,用lr的原因是确保不会读过头了.
+	for lr.N > 0 {
+
+		// 获取每一个频道的节目信息,保存起来
+		programs := MpegTsPATProgram{}
+
+		programs.ProgramNumber, err = util.ReadByteToUint16(lr, true)
+		if err != nil {
+			return
+		}
+
+		// 如果programNumber为0,则是NetworkPID,否则是ProgramMapPID(13)
+		if programs.ProgramNumber == 0 {
+			programs.NetworkPID, err = util.ReadByteToUint16(lr, true)
+			if err != nil {
+				return
+			}
+
+			programs.NetworkPID = programs.NetworkPID & 0x1fff
+		} else {
+			programs.ProgramMapPID, err = util.ReadByteToUint16(lr, true)
+			if err != nil {
+				return
+			}
+
+			programs.ProgramMapPID = programs.ProgramMapPID & 0x1fff
+		}
+
+		pat.Program = append(pat.Program, programs)
+	}
+	if cr, ok := r.(*util.Crc32Reader); ok {
+		err = cr.ReadCrc32UIntAndCheck()
+		if err != nil {
+			return
+		}
+	}
+
+	return
+}
+
+func WritePAT(w io.Writer, pat MpegTsPAT) (err error) {
+	bw := &bytes.Buffer{}
+
+	// 将pat(所有的节目索引信息)写入到缓冲区中
+	for _, pats := range pat.Program {
+		if err = util.WriteUint16ToByte(bw, pats.ProgramNumber, true); err != nil {
+			return
+		}
+
+		if pats.ProgramNumber == 0 {
+			if err = util.WriteUint16ToByte(bw, pats.NetworkPID&0x1fff|7<<13, true); err != nil {
+				return
+			}
+		} else {
+			// | 0001 1111 | 1111 1111 |
+			// 7 << 13 -> 1110 0000 0000 0000
+			if err = util.WriteUint16ToByte(bw, pats.ProgramMapPID&0x1fff|7<<13, true); err != nil {
+				return
+			}
+		}
+	}
+
+	if pat.SectionLength == 0 {
+		pat.SectionLength = 2 + 3 + 4 + uint16(len(bw.Bytes()))
+	}
+
+	psi := MpegTsPSI{}
+
+	psi.Pat = pat
+
+	if err = WritePSI(w, PSI_TYPE_PAT, psi, bw.Bytes()); err != nil {
+		return
+	}
+
+	return
+}
+
+func WritePATPacket(w io.Writer, tsHeader []byte, pat MpegTsPAT) (err error) {
+	if pat.TableID != TABLE_PAS {
+		err = errors.New("PAT table ID error")
+		return
+	}
+
+	// 将所有要写的数据(PAT),全部放入到buffer中去.
+	// 	buffer 里面已经写好了整个pat表(PointerField+PSI+PAT+CRC)
+	bw := &bytes.Buffer{}
+	if err = WritePAT(bw, pat); err != nil {
+		return
+	}
+
+	// TODO:如果Pat.Program里面包含的信息很大,大于188?
+	stuffingBytes := util.GetFillBytes(0xff, TS_PACKET_SIZE-4-bw.Len())
+
+	// PATPacket = TsHeader + PAT + Stuffing Bytes
+	var PATPacket []byte
+	PATPacket = append(PATPacket, tsHeader...)
+	PATPacket = append(PATPacket, bw.Bytes()...)
+	PATPacket = append(PATPacket, stuffingBytes...)
+
+	fmt.Println("-------------------------")
+	fmt.Println("Write PAT :", PATPacket)
+	fmt.Println("-------------------------")
+
+	// 写PAT负载
+	if _, err = w.Write(PATPacket); err != nil {
+		return
+	}
+
+	return
+}
+
+func WriteDefaultPATPacket(w io.Writer) (err error) {
+	_, err = w.Write(DefaultPATPacket)
+	if err != nil {
+		return
+	}
+
+	return
+}

+ 455 - 0
m7s.live/engine/v4@v4.14.6/codec/mpegts/mpegts_pes.go

@@ -0,0 +1,455 @@
+package mpegts
+
+import (
+	"errors"
+	"io"
+	"net"
+
+	"m7s.live/engine/v4/util"
+)
+
+// ios13818-1-CN.pdf 45/166
+//
+// PES
+//
+
+// 每个传输流和节目流在逻辑上都是由 PES 包构造的
+type MpegTsPesStream struct {
+	TsPkt  MpegTsPacket
+	PesPkt MpegTsPESPacket
+}
+
+// PES--Packetized  Elementary Streams  (分组的ES),ES形成的分组称为PES分组,是用来传递ES的一种数据结构
+// 1110 xxxx 为视频流(0xE0)
+// 110x xxxx 为音频流(0xC0)
+type MpegTsPESPacket struct {
+	Header  MpegTsPESHeader
+	Payload util.Buffer //从TS包中读取的数据
+	Buffers net.Buffers //用于写TS包
+}
+
+type MpegTsPESHeader struct {
+	PacketStartCodePrefix uint32 // 24 bits 同跟随它的 stream_id 一起组成标识包起始端的包起始码.packet_start_code_prefix 为比特串"0000 0000 0000 0000 0000 0001"(0x000001)
+	StreamID              byte   // 8 bits stream_id 指示基本流的类型和编号,如 stream_id 表 2-22 所定义的.传输流中,stream_id 可以设置为准确描述基本流类型的任何有效值,如表 2-22 所规定的.传输流中,基本流类型在 2.4.4 中所指示的节目特定信息中指定
+	PesPacketLength       uint16 // 16 bits 指示 PES 包中跟随该字段最后字节的字节数.0->指示 PES 包长度既未指示也未限定并且仅在这样的 PES 包中才被允许,该 PES 包的有效载荷由来自传输流包中所包含的视频基本流的字节组成
+
+	MpegTsOptionalPESHeader
+
+	PayloadLength uint64 // 这个不是标准文档里面的字段,是自己添加的,方便计算
+}
+
+// 可选的PES Header = MpegTsOptionalPESHeader + stuffing bytes(0xFF) m * 8
+type MpegTsOptionalPESHeader struct {
+	ConstTen               byte // 2 bits 常量10
+	PesScramblingControl   byte // 2 bit 指示 PES 包有效载荷的加扰方式.当加扰在 PES 等级上实施时, PES 包头,其中包括任选字段只要存在,应不加扰(见表 2-23)
+	PesPriority            byte // 1 bit 指示在此 PES 包中该有效载荷的优先级.1->指示该 PES 包有效载荷比具有此字段置于"0"的其他 PES 包有效载荷有更高的有效载荷优先级.多路复用器能够使用该PES_priority 比特最佳化基本流内的数据
+	DataAlignmentIndicator byte // 1 bit 1->指示 PES 包头之后紧随 2.6.10 中data_stream_alignment_descriptor 字段中指示的视频句法单元或音频同步字,只要该描述符字段存在.若置于值"1"并且该描述符不存在,则要求表 2-53,表 2-54 或表 2-55 的 alignment_type"01"中所指示的那种校准.0->不能确定任何此类校准是否发生
+	Copyright              byte // 1 bit 1->指示相关 PES 包有效载荷的素材依靠版权所保护.0->不能确定该素材是否依靠版权所保护
+	OriginalOrCopy         byte // 1 bit 1->指示相关 PES 包有效载荷的内容是原始的.0->指示相关 PES 包有效载荷的内容是复制的
+	PtsDtsFlags            byte // 2 bits 10->PES 包头中 PTS 字段存在. 11->PES 包头中 PTS 字段和 DTS 字段均存在. 00->PES 包头中既无任何 PTS 字段也无任何 DTS 字段存在. 01->禁用
+	EscrFlag               byte // 1 bit 1->指示 PES 包头中 ESCR 基准字段和 ESCR 扩展字段均存在.0->指示无任何 ESCR 字段存在
+	EsRateFlag             byte // 1 bit 1->指示 PES 包头中 ES_rate 字段存在.0->指示无任何 ES_rate 字段存在
+	DsmTrickModeFlag       byte // 1 bit 1->指示 8 比特特技方式字段存在.0->指示此字段不存在
+	AdditionalCopyInfoFlag byte // 1 bit 1->指示 additional_copy_info 存在.0->时指示此字段不存在
+	PesCRCFlag             byte // 1 bit 1->指示 PES 包中 CRC 字段存在.0->指示此字段不存在
+	PesExtensionFlag       byte // 1 bit 1->时指示 PES 包头中扩展字段存在.0->指示此字段不存在
+	PesHeaderDataLength    byte // 8 bits 指示在此 PES包头中包含的由任选字段和任意填充字节所占据的字节总数.任选字段的存在由前导 PES_header_data_length 字段的字节来指定
+
+	// Optional Field
+	Pts                  uint64 // 33 bits 指示时间与解码时间的关系如下: PTS 为三个独立字段编码的 33 比特数.它指示基本流 n 的显示单元 k 在系统目标解码器中的显示时间 tpn(k).PTS 值以系统时钟频率除以 300(产生 90 kHz)的周期为单位指定.显示时间依照以下公式 2-11 从 PTS 中推出.有关编码显示时间标记频率上的限制参阅 2.7.4
+	Dts                  uint64 // 33 bits 指示基本流 n 的存取单元 j 在系统目标解码器中的解码时间 tdn(j). DTS 的值以系统时钟频率除以 300(生成90 kHz)的周期为单位指定.依照以下公式 2-12 从 DTS 中推出解码时间
+	EscrBase             uint64 // 33 bits 其值由 ESCR_base(i) 给出,如公式 2-14 中给出的
+	EscrExtension        uint16 // 9 bits 其值由 ESCR_ext(i) 给出,如公式 2-15 中给出的. ESCR 字段指示包含 ESCR_base 最后比特的字节到达 PES流的 PES-STD 输入端的预期时间(参阅 2.5.2.4)
+	EsRate               uint32 // 22 bits 在PES 流情况中,指定系统目标解码器接收 PES 包字节的速率.ES_rate 在包括它的 PES 包以及相同 PES 流的后续 PES 包中持续有效直至遇到新的 ES_rate 字段时为止.ES 速率值以 50 字节/秒为度量单位.0 值禁用
+	TrickModeControl     byte   // 3 bits 指示适用于相关视频流的特技方式.在其他类型基本流的情况中,此字段以及后随 5 比特所规定的那些含义未确定.对于 trick_mode 状态的定义,参阅 2.4.2.3 的特技方式段落
+	TrickModeValue       byte   // 5 bits
+	AdditionalCopyInfo   byte   // 7 bits 包含与版权信息有关的专用数据
+	PreviousPESPacketCRC uint16 // 16 bits 包含产生解码器中 16 寄存器零输出的 CRC 值, 类似于附件 A 中定义的解码器. 但在处理先前的 PES 包数据字节之后, PES 包头除外,采用多项式
+
+	// PES Extension
+	PesPrivateDataFlag               byte // 1 bit 1->指示该 PES 包头包含专用数据. 0->指示 PES 包头中不存在专用数据
+	PackHeaderFieldFlag              byte // 1 bit 1->指示 ISO/IEC 11172-1 包头或节目流包头在此 PES包头中存储.若此字段处于节目流中包含的 PES 包中,则此字段应设置为"0.传输流中, 0->指示该 PES 头中无任何包头存在
+	ProgramPacketSequenceCounterFlag byte // 1 bit 1->指示 program_packet_sequence_counter, MPEG1_MPEG2_identifier 以及 original_stuff_length 字段在 PES 包中存在.0->它指示这些字段在 PES 头中不存在
+	PSTDBufferFlag                   byte // 1 bit 1->指示 P-STD_buffer_scale 和 P-STD_buffer_size 在 PES包头中存在.0->指示这些字段在 PES 头中不存在
+	Reserved                         byte // 3 bits
+	PesExtensionFlag2                byte // 1 bits 1->指示 PES_extension_field_length 字段及相关的字段存在.0->指示 PES_extension_field_length 字段以及任何相关的字段均不存在.
+
+	// Optional Field
+	PesPrivateData               [16]byte // 128 bits 此数据,同前后字段数据结合,应不能仿真packet_start_code_prefix (0x000001)
+	PackHeaderField              byte     // 8 bits 指示 pack_header_field() 的长度,以字节为单位
+	ProgramPacketSequenceCounter byte     // 7 bits
+	Mpeg1Mpeg2Identifier         byte     // 1 bit 1->指示此 PES 包承载来自 ISO/IEC 11172-1 流的信息.0->指示此 PES 包承载来自节目流的信息
+	OriginalStuffLength          byte     // 6 bits 在原始 ITU-T H.222.0 建议书| ISO/IEC 13818-1 PES 包头或在原始 ISO/IEC 11172-1 包头中所使用的填充字节数
+	PSTDBufferScale              byte     // 1bit 它的含义仅当节目流中包含此 PES 包时才规定.它指示所使用的标度因子用于解释后续的 P-STD_buffer_size 字段.若前导 stream_id 指示音频流,则P-STD 缓冲器标度字段必为"0"值.若前导 stream_id 指示视频流,则 P-STD_buffer_scale 字段必为"1"值.对于所有其他流类型,该值可为"1"或为"0"
+	PSTDBufferSize               uint16   // 13 bits 其含义仅当节目流中包含此 PES包时才规定.它规定在 P-STD 中,输入缓冲器 BSn 的尺寸.若 STD_buffer_scale 为 "0"值,则 P-STD_buffer_size以 128 字节为单位度量该缓冲器尺寸.若 P-STD_buffer_scale 为"1",则 P-STD_buffer_size 以 1024 字节为单位度量该缓冲器尺寸
+	PesExtensionFieldLength      byte     // 7 bits 指示 PES 扩展字段中跟随此长度字段的直至并包括任何保留字节为止的数据长度,以字节为度量单位
+	StreamIDExtensionFlag        byte     // 1 bits
+	//pesExtensionField              []byte   // PES_extension_field_length bits
+	//packField                        []byte   // pack_field_length bits
+}
+
+// pts_dts_Flags == "10" -> PTS
+// 0010				4
+// PTS[32...30]		3
+// marker_bit		1
+// PTS[29...15]		15
+// marker_bit		1
+// PTS[14...0]		15
+// marker_bit		1
+
+// pts_dts_Flags == "11" -> PTS + DTS
+
+type MpegtsPESFrame struct {
+	Pid                       uint16
+	IsKeyFrame                bool
+	ContinuityCounter         byte
+	ProgramClockReferenceBase uint64
+}
+
+func ReadPESHeader(r io.Reader) (header MpegTsPESHeader, err error) {
+	var flags uint8
+	var length uint
+
+	// packetStartCodePrefix(24) (0x000001)
+	header.PacketStartCodePrefix, err = util.ReadByteToUint24(r, true)
+	if err != nil {
+		return
+	}
+
+	if header.PacketStartCodePrefix != 0x0000001 {
+		err = errors.New("read PacketStartCodePrefix is not 0x0000001")
+		return
+	}
+
+	// streamID(8)
+	header.StreamID, err = util.ReadByteToUint8(r)
+	if err != nil {
+		return
+	}
+
+	// pes_PacketLength(16)
+	header.PesPacketLength, err = util.ReadByteToUint16(r, true)
+	if err != nil {
+		return
+	}
+
+	length = uint(header.PesPacketLength)
+
+	// PES包长度可能为0,这个时候,需要自己去算
+	// 0 <= len <= 65535
+	// 如果当length为0,那么先设置为最大值,然后用LimitedReade去读,如果读到最后面剩下的字节数小于65536,才是正确的包大小.
+	// 一个包一般情况下不可能会读1<<31个字节.
+	if length == 0 {
+		length = 1 << 31
+	}
+
+	// lrPacket 和 lrHeader 位置指针是在同一位置的
+	lrPacket := &io.LimitedReader{R: r, N: int64(length)}
+	lrHeader := lrPacket
+
+	// constTen(2)
+	// pes_ScramblingControl(2)
+	// pes_Priority(1)
+	// dataAlignmentIndicator(1)
+	// copyright(1)
+	// originalOrCopy(1)
+	flags, err = util.ReadByteToUint8(lrHeader)
+	if err != nil {
+		return
+	}
+
+	header.ConstTen = flags & 0xc0
+	header.PesScramblingControl = flags & 0x30
+	header.PesPriority = flags & 0x08
+	header.DataAlignmentIndicator = flags & 0x04
+	header.Copyright = flags & 0x02
+	header.OriginalOrCopy = flags & 0x01
+
+	// pts_dts_Flags(2)
+	// escr_Flag(1)
+	// es_RateFlag(1)
+	// dsm_TrickModeFlag(1)
+	// additionalCopyInfoFlag(1)
+	// pes_CRCFlag(1)
+	// pes_ExtensionFlag(1)
+	flags, err = util.ReadByteToUint8(lrHeader)
+	if err != nil {
+		return
+	}
+
+	header.PtsDtsFlags = flags & 0xc0
+	header.EscrFlag = flags & 0x20
+	header.EsRateFlag = flags & 0x10
+	header.DsmTrickModeFlag = flags & 0x08
+	header.AdditionalCopyInfoFlag = flags & 0x04
+	header.PesCRCFlag = flags & 0x02
+	header.PesExtensionFlag = flags & 0x01
+
+	// pes_HeaderDataLength(8)
+	header.PesHeaderDataLength, err = util.ReadByteToUint8(lrHeader)
+	if err != nil {
+		return
+	}
+
+	length = uint(header.PesHeaderDataLength)
+
+	lrHeader = &io.LimitedReader{R: lrHeader, N: int64(length)}
+
+	// 00 -> PES 包头中既无任何PTS 字段也无任何DTS 字段存在
+	// 10 -> PES 包头中PTS 字段存在
+	// 11 -> PES 包头中PTS 字段和DTS 字段均存在
+	// 01 -> 禁用
+
+	// PTS(33)
+	if flags&0x80 != 0 {
+		var pts uint64
+		pts, err = util.ReadByteToUint40(lrHeader, true)
+		if err != nil {
+			return
+		}
+
+		header.Pts = util.GetPtsDts(pts)
+	}
+
+	// DTS(33)
+	if flags&0x80 != 0 && flags&0x40 != 0 {
+		var dts uint64
+		dts, err = util.ReadByteToUint40(lrHeader, true)
+		if err != nil {
+			return
+		}
+
+		header.Dts = util.GetPtsDts(dts)
+	}
+
+	// reserved(2) + escr_Base1(3) + marker_bit(1) +
+	// escr_Base2(15) + marker_bit(1) + escr_Base23(15) +
+	// marker_bit(1) + escr_Extension(9) + marker_bit(1)
+	if header.EscrFlag != 0 {
+		_, err = util.ReadByteToUint48(lrHeader, true)
+		if err != nil {
+			return
+		}
+
+		//s.pes.escr_Base = escrBaseEx & 0x3fffffffe00
+		//s.pes.escr_Extension = uint16(escrBaseEx & 0x1ff)
+	}
+
+	// es_Rate(22)
+	if header.EsRateFlag != 0 {
+		header.EsRate, err = util.ReadByteToUint24(lrHeader, true)
+		if err != nil {
+			return
+		}
+	}
+
+	// 不知道为什么这里不用
+	/*
+		// trickModeControl(3) + trickModeValue(5)
+		if s.pes.dsm_TrickModeFlag != 0 {
+			trickMcMv, err := util.ReadByteToUint8(lrHeader)
+			if err != nil {
+				return err
+			}
+
+			s.pes.trickModeControl = trickMcMv & 0xe0
+			s.pes.trickModeValue = trickMcMv & 0x1f
+		}
+	*/
+
+	// marker_bit(1) + additionalCopyInfo(7)
+	if header.AdditionalCopyInfoFlag != 0 {
+		header.AdditionalCopyInfo, err = util.ReadByteToUint8(lrHeader)
+		if err != nil {
+			return
+		}
+
+		header.AdditionalCopyInfo = header.AdditionalCopyInfo & 0x7f
+	}
+
+	// previous_PES_Packet_CRC(16)
+	if header.PesCRCFlag != 0 {
+		header.PreviousPESPacketCRC, err = util.ReadByteToUint16(lrHeader, true)
+		if err != nil {
+			return
+		}
+	}
+
+	// pes_PrivateDataFlag(1) + packHeaderFieldFlag(1) + programPacketSequenceCounterFlag(1) +
+	// p_STD_BufferFlag(1) + reserved(3) + pes_ExtensionFlag2(1)
+	if header.PesExtensionFlag != 0 {
+		var flags uint8
+		flags, err = util.ReadByteToUint8(lrHeader)
+		if err != nil {
+			return
+		}
+
+		header.PesPrivateDataFlag = flags & 0x80
+		header.PackHeaderFieldFlag = flags & 0x40
+		header.ProgramPacketSequenceCounterFlag = flags & 0x20
+		header.PSTDBufferFlag = flags & 0x10
+		header.PesExtensionFlag2 = flags & 0x01
+
+		// TODO:下面所有的标志位,可能获取到的数据,都简单的读取后,丢弃,如果日后需要,在这里处理
+
+		// pes_PrivateData(128)
+		if header.PesPrivateDataFlag != 0 {
+			if _, err = io.CopyN(io.Discard, lrHeader, int64(16)); err != nil {
+				return
+			}
+		}
+
+		// packFieldLength(8)
+		if header.PackHeaderFieldFlag != 0 {
+			if _, err = io.CopyN(io.Discard, lrHeader, int64(1)); err != nil {
+				return
+			}
+		}
+
+		// marker_bit(1) + programPacketSequenceCounter(7) + marker_bit(1) +
+		// mpeg1_mpeg2_Identifier(1) + originalStuffLength(6)
+		if header.ProgramPacketSequenceCounterFlag != 0 {
+			if _, err = io.CopyN(io.Discard, lrHeader, int64(2)); err != nil {
+				return
+			}
+		}
+
+		// 01 + p_STD_bufferScale(1) + p_STD_bufferSize(13)
+		if header.PSTDBufferFlag != 0 {
+			if _, err = io.CopyN(io.Discard, lrHeader, int64(2)); err != nil {
+				return
+			}
+		}
+
+		// marker_bit(1) + pes_Extension_Field_Length(7) +
+		// streamIDExtensionFlag(1)
+		if header.PesExtensionFlag != 0 {
+			if _, err = io.CopyN(io.Discard, lrHeader, int64(2)); err != nil {
+				return
+			}
+		}
+	}
+
+	// 把剩下的头的数据消耗掉
+	if lrHeader.N > 0 {
+		if _, err = io.CopyN(io.Discard, lrHeader, int64(lrHeader.N)); err != nil {
+			return
+		}
+	}
+
+	// 2的16次方,16个字节
+	if lrPacket.N < 65536 {
+		// 这里得到的其实是负载长度,因为已经偏移过了Header部分.
+		//header.pes_PacketLength = uint16(lrPacket.N)
+		header.PayloadLength = uint64(lrPacket.N)
+	}
+
+	return
+}
+
+func WritePESHeader(w io.Writer, header MpegTsPESHeader) (written int, err error) {
+	if header.PacketStartCodePrefix != 0x0000001 {
+		err = errors.New("write PacketStartCodePrefix is not 0x0000001")
+		return
+	}
+
+	// packetStartCodePrefix(24) (0x000001)
+	if err = util.WriteUint24ToByte(w, header.PacketStartCodePrefix, true); err != nil {
+		return
+	}
+
+	written += 3
+
+	// streamID(8)
+	if err = util.WriteUint8ToByte(w, header.StreamID); err != nil {
+		return
+	}
+
+	written += 1
+
+	// pes_PacketLength(16)
+	// PES包长度可能为0,这个时候,需要自己去算
+	// 0 <= len <= 65535
+	if err = util.WriteUint16ToByte(w, header.PesPacketLength, true); err != nil {
+		return
+	}
+
+	//fmt.Println("Length :", payloadLength)
+	//fmt.Println("PES Packet Length :", header.pes_PacketLength)
+
+	written += 2
+
+	// constTen(2)
+	// pes_ScramblingControl(2)
+	// pes_Priority(1)
+	// dataAlignmentIndicator(1)
+	// copyright(1)
+	// originalOrCopy(1)
+	// 1000 0001
+	if header.ConstTen != 0x80 {
+		err = errors.New("pes header ConstTen != 0x80")
+		return
+	}
+
+	flags := header.ConstTen | header.PesScramblingControl | header.PesPriority | header.DataAlignmentIndicator | header.Copyright | header.OriginalOrCopy
+	if err = util.WriteUint8ToByte(w, flags); err != nil {
+		return
+	}
+
+	written += 1
+
+	// pts_dts_Flags(2)
+	// escr_Flag(1)
+	// es_RateFlag(1)
+	// dsm_TrickModeFlag(1)
+	// additionalCopyInfoFlag(1)
+	// pes_CRCFlag(1)
+	// pes_ExtensionFlag(1)
+	sevenFlags := header.PtsDtsFlags | header.EscrFlag | header.EsRateFlag | header.DsmTrickModeFlag | header.AdditionalCopyInfoFlag | header.PesCRCFlag | header.PesExtensionFlag
+	if err = util.WriteUint8ToByte(w, sevenFlags); err != nil {
+		return
+	}
+
+	written += 1
+
+	// pes_HeaderDataLength(8)
+	if err = util.WriteUint8ToByte(w, header.PesHeaderDataLength); err != nil {
+		return
+	}
+
+	written += 1
+
+	// PtsDtsFlags == 192(11), 128(10), 64(01)禁用, 0(00)
+	if header.PtsDtsFlags&0x80 != 0 {
+		// PTS和DTS都存在(11),否则只有PTS(10)
+		if header.PtsDtsFlags&0x80 != 0 && header.PtsDtsFlags&0x40 != 0 {
+			// 11:PTS和DTS
+			// PTS(33) + 4 + 3
+			pts := util.PutPtsDts(header.Pts) | 3<<36
+			if err = util.WriteUint40ToByte(w, pts, true); err != nil {
+				return
+			}
+
+			written += 5
+
+			// DTS(33) + 4 + 3
+			dts := util.PutPtsDts(header.Dts) | 1<<36
+			if err = util.WriteUint40ToByte(w, dts, true); err != nil {
+				return
+			}
+
+			written += 5
+		} else {
+			// 10:只有PTS
+			// PTS(33) + 4 + 3
+			pts := util.PutPtsDts(header.Pts) | 2<<36
+			if err = util.WriteUint40ToByte(w, pts, true); err != nil {
+				return
+			}
+
+			written += 5
+		}
+	}
+
+	return
+}

+ 368 - 0
m7s.live/engine/v4@v4.14.6/codec/mpegts/mpegts_pmt.go

@@ -0,0 +1,368 @@
+package mpegts
+
+import (
+	"bytes"
+	"io"
+	"net"
+
+	"m7s.live/engine/v4/codec"
+	"m7s.live/engine/v4/util"
+)
+
+// ios13818-1-CN.pdf 46(60)-153(167)/page
+//
+// PMT
+
+var (
+	TSHeader = []byte{0x47, 0x40 | (PID_PMT >> 8), PID_PMT & 0xff, 0x10, 0x00} //PID:0x100
+	PSI      = []byte{0x02, 0xb0, 0x17, 0x00, 0x01, 0xc1, 0x00, 0x00}
+	PMT      = []byte{0xe0 | (PID_VIDEO >> 8), PID_VIDEO & 0xff, 0xf0, 0x00} //PcrPID:0x101
+	h264     = []byte{STREAM_TYPE_H264, 0xe0 | (PID_VIDEO >> 8), PID_VIDEO & 0xff, 0xf0, 0x00}
+	h265     = []byte{STREAM_TYPE_H265, 0xe0 | (PID_VIDEO >> 8), PID_VIDEO & 0xff, 0xf0, 0x00}
+	aac      = []byte{STREAM_TYPE_AAC, 0xe0 | (PID_AUDIO >> 8), PID_AUDIO & 0xff, 0xf0, 0x00}
+	pcma     = []byte{STREAM_TYPE_G711A, 0xe0 | (PID_AUDIO >> 8), PID_AUDIO & 0xff, 0xf0, 0x00}
+	pcmu     = []byte{STREAM_TYPE_G711U, 0xe0 | (PID_AUDIO >> 8), PID_AUDIO & 0xff, 0xf0, 0x00}
+	Stuffing []byte
+)
+
+func init() {
+	Stuffing = util.GetFillBytes(0xff, TS_PACKET_SIZE)
+}
+
+// TS Header :
+// SyncByte = 0x47
+// TransportErrorIndicator = 0(B:0), PayloadUnitStartIndicator = 1(B:0), TransportPriority = 0(B:0),
+// Pid = 4097(0x1001),
+// TransportScramblingControl = 0(B:00), AdaptionFieldControl = 1(B:01), ContinuityCounter = 0(B:0000),
+
+// PSI :
+// TableID = 0x02,
+// SectionSyntaxIndicator = 1(B:1), Zero = 0(B:0), Reserved1 = 3(B:11),
+// SectionLength = 23(0x17)
+// ProgramNumber = 0x0001
+// Reserved2 = 3(B:11), VersionNumber = (B:00000), CurrentNextIndicator = 1(B:0),
+// SectionNumber = 0x00
+// LastSectionNumber = 0x00
+
+// PMT:
+// Reserved3 = 15(B:1110), PcrPID = 256(0x100)
+// Reserved4 = 16(B:1111), ProgramInfoLength = 0(0x000)
+// H264:
+// StreamType = 0x1b,
+// Reserved5 = 15(B:1110), ElementaryPID = 256(0x100)
+// Reserved6 = 16(B:1111), EsInfoLength = 0(0x000)
+// AAC:
+// StreamType = 0x0f,
+// Reserved5 = 15(B:1110), ElementaryPID = 257(0x101)
+// Reserved6 = 16(B:1111), EsInfoLength = 0(0x000)
+
+type MpegTsPmtStream struct {
+	StreamType    byte   // 8 bits 指示具有 PID值的包内承载的节目元类型,其 PID值由 elementary_PID所指定
+	Reserved5     byte   // 3 bits 保留位
+	ElementaryPID uint16 // 13 bits 指定承载相关节目元的传输流包的 PID
+	Reserved6     byte   // 4 bits 保留位
+	EsInfoLength  uint16 // 12 bits 该字段的头两比特必为'00',剩余 10比特指示紧随 ES_info_length字段的相关节目元描述符的字节数
+
+	// N Loop Descriptors
+	Descriptor []MpegTsDescriptor // 不确定字节数,可变
+}
+
+// Program Map Table (节目映射表)
+type MpegTsPMT struct {
+	// PSI
+	TableID                byte   // 8 bits 0x00->PAT,0x02->PMT
+	SectionSyntaxIndicator byte   // 1 bit  段语法标志位,固定为1
+	Zero                   byte   // 1 bit  0
+	Reserved1              byte   // 2 bits 保留位
+	SectionLength          uint16 // 12 bits 该字段的头两比特必为'00',剩余 10 比特指定该分段的字节数,紧随 section_length 字段开始,并包括 CRC.此字段中的值应不超过 1021(0x3FD)
+	ProgramNumber          uint16 // 16 bits 指定 program_map_PID 所适用的节目
+	Reserved2              byte   // 2 bits  保留位
+	VersionNumber          byte   // 5 bits  范围0-31,表示PAT的版本号
+	CurrentNextIndicator   byte   // 1 bit  发送的PAT是当前有效还是下一个PAT有效
+	SectionNumber          byte   // 8 bits  分段的号码.PAT可能分为多段传输.第一段为00,以后每个分段加1,最多可能有256个分段
+	LastSectionNumber      byte   // 8 bits  最后一个分段的号码
+
+	Reserved3             byte               // 3 bits  保留位 0x07
+	PcrPID                uint16             // 13 bits 指明TS包的PID值.该TS包含有PCR域,该PCR值对应于由节目号指定的对应节目.如果对于私有数据流的节目定义与PCR无关.这个域的值将为0x1FFF
+	Reserved4             byte               // 4 bits  预留位 0x0F
+	ProgramInfoLength     uint16             // 12 bits 前两位bit为00.该域指出跟随其后对节目信息的描述的byte数
+	ProgramInfoDescriptor []MpegTsDescriptor // N Loop Descriptors 可变 节目信息描述
+
+	// N Loop
+	Stream []MpegTsPmtStream // PMT表里面的所有音视频索引信息
+
+	Crc32 uint32 // 32 bits 包含处理全部传输流节目映射分段之后,在附件 B 规定的解码器中给出寄存器零输出的 CRC 值
+}
+
+func ReadPMT(r io.Reader) (pmt MpegTsPMT, err error) {
+	lr, psi, err := ReadPSI(r, PSI_TYPE_PMT)
+	if err != nil {
+		return
+	}
+
+	pmt = psi.Pmt
+
+	// reserved3(3) + pcrPID(13)
+	pcrPID, err := util.ReadByteToUint16(lr, true)
+	if err != nil {
+		return
+	}
+
+	pmt.PcrPID = pcrPID & 0x1fff
+
+	// reserved4(4) + programInfoLength(12)
+	// programInfoLength(12) == 0x00(固定为0) + programInfoLength(10)
+	programInfoLength, err := util.ReadByteToUint16(lr, true)
+	if err != nil {
+		return
+	}
+
+	pmt.ProgramInfoLength = programInfoLength & 0x3ff
+
+	// 如果length>0那么,紧跟programInfoLength后面就有length个字节
+	if pmt.ProgramInfoLength > 0 {
+		lr := &io.LimitedReader{R: lr, N: int64(pmt.ProgramInfoLength)}
+		pmt.ProgramInfoDescriptor, err = ReadPMTDescriptor(lr)
+		if err != nil {
+			return
+		}
+	}
+
+	// N Loop
+	// 开始N循环,读取所有的流的信息
+	for lr.N > 0 {
+		var streams MpegTsPmtStream
+		// streamType(8)
+		streams.StreamType, err = util.ReadByteToUint8(lr)
+		if err != nil {
+			return
+		}
+
+		// reserved5(3) + elementaryPID(13)
+		streams.ElementaryPID, err = util.ReadByteToUint16(lr, true)
+		if err != nil {
+			return
+		}
+
+		streams.ElementaryPID = streams.ElementaryPID & 0x1fff
+
+		// reserved6(4) + esInfoLength(12)
+		// esInfoLength(12) == 0x00(固定为0) + esInfoLength(10)
+		streams.EsInfoLength, err = util.ReadByteToUint16(lr, true)
+		if err != nil {
+			return
+		}
+
+		streams.EsInfoLength = streams.EsInfoLength & 0x3ff
+
+		// 如果length>0那么,紧跟esInfoLength后面就有length个字节
+		if streams.EsInfoLength > 0 {
+			lr := &io.LimitedReader{R: lr, N: int64(streams.EsInfoLength)}
+			streams.Descriptor, err = ReadPMTDescriptor(lr)
+			if err != nil {
+				return
+			}
+		}
+
+		// 每读取一个流的信息(音频流或者视频流或者其他),都保存起来
+		pmt.Stream = append(pmt.Stream, streams)
+	}
+	if cr, ok := r.(*util.Crc32Reader); ok {
+		err = cr.ReadCrc32UIntAndCheck()
+		if err != nil {
+			return
+		}
+	}
+	return
+}
+
+func ReadPMTDescriptor(lr *io.LimitedReader) (Desc []MpegTsDescriptor, err error) {
+	var desc MpegTsDescriptor
+	for lr.N > 0 {
+		// tag (8)
+		desc.Tag, err = util.ReadByteToUint8(lr)
+		if err != nil {
+			return
+		}
+
+		// length (8)
+		desc.Length, err = util.ReadByteToUint8(lr)
+		if err != nil {
+			return
+		}
+
+		desc.Data = make([]byte, desc.Length)
+		_, err = lr.Read(desc.Data)
+		if err != nil {
+			return
+		}
+
+		Desc = append(Desc, desc)
+	}
+
+	return
+}
+
+func WritePMTDescriptor(w io.Writer, descs []MpegTsDescriptor) (err error) {
+	for _, desc := range descs {
+		// tag(8)
+		if err = util.WriteUint8ToByte(w, desc.Tag); err != nil {
+			return
+		}
+
+		// length (8)
+		if err = util.WriteUint8ToByte(w, uint8(len(desc.Data))); err != nil {
+			return
+		}
+
+		// data
+		if _, err = w.Write(desc.Data); err != nil {
+			return
+		}
+	}
+
+	return
+}
+
+func WritePMTBody(w io.Writer, pmt MpegTsPMT) (err error) {
+	// reserved3(3) + pcrPID(13)
+	if err = util.WriteUint16ToByte(w, pmt.PcrPID|7<<13, true); err != nil {
+		return
+	}
+
+	// programInfoDescriptor 节目信息描述,字节数不能确定
+	bw := &bytes.Buffer{}
+	if err = WritePMTDescriptor(bw, pmt.ProgramInfoDescriptor); err != nil {
+		return
+	}
+
+	pmt.ProgramInfoLength = uint16(bw.Len())
+
+	// reserved4(4) + programInfoLength(12)
+	// programInfoLength(12) == 0x00(固定为0) + programInfoLength(10)
+	if err = util.WriteUint16ToByte(w, pmt.ProgramInfoLength|0xf000, true); err != nil {
+		return
+	}
+
+	// programInfoDescriptor
+	if _, err = w.Write(bw.Bytes()); err != nil {
+		return
+	}
+
+	// 循环读取所有的流的信息(音频或者视频)
+	for _, esinfo := range pmt.Stream {
+		// streamType(8)
+		if err = util.WriteUint8ToByte(w, esinfo.StreamType); err != nil {
+			return
+		}
+
+		// reserved5(3) + elementaryPID(13)
+		if err = util.WriteUint16ToByte(w, esinfo.ElementaryPID|7<<13, true); err != nil {
+			return
+		}
+
+		// descriptor ES流信息描述,字节数不能确定
+		bw := &bytes.Buffer{}
+		if err = WritePMTDescriptor(bw, esinfo.Descriptor); err != nil {
+			return
+		}
+
+		esinfo.EsInfoLength = uint16(bw.Len())
+
+		// reserved6(4) + esInfoLength(12)
+		// esInfoLength(12) == 0x00(固定为0) + esInfoLength(10)
+		if err = util.WriteUint16ToByte(w, esinfo.EsInfoLength|0xf000, true); err != nil {
+			return
+		}
+
+		// descriptor
+		if _, err = w.Write(bw.Bytes()); err != nil {
+			return
+		}
+	}
+
+	return
+}
+
+func WritePMT(w io.Writer, pmt MpegTsPMT) (err error) {
+	bw := &bytes.Buffer{}
+
+	if err = WritePMTBody(bw, pmt); err != nil {
+		return
+	}
+
+	if pmt.SectionLength == 0 {
+		pmt.SectionLength = 2 + 3 + 4 + uint16(len(bw.Bytes()))
+	}
+
+	psi := MpegTsPSI{}
+
+	psi.Pmt = pmt
+
+	if err = WritePSI(w, PSI_TYPE_PMT, psi, bw.Bytes()); err != nil {
+		return
+	}
+
+	return
+}
+
+// func WritePMTPacket(w io.Writer, tsHeader []byte, pmt MpegTsPMT) (err error) {
+// 	if pmt.TableID != TABLE_TSPMS {
+// 		err = errors.New("PMT table ID error")
+// 		return
+// 	}
+
+// 	// 将所有要写的数据(PMT),全部放入到buffer中去.
+// 	// 	buffer 里面已经写好了整个PMT表(PointerField+PSI+PMT+CRC)
+// 	bw := &bytes.Buffer{}
+// 	if err = WritePMT(bw, pmt); err != nil {
+// 		return
+// 	}
+
+// 	// TODO:如果Pmt.Stream里面包含的信息很大,大于188?
+// 	stuffingBytes := util.GetFillBytes(0xff, TS_PACKET_SIZE-4-bw.Len())
+
+// 	var PMTPacket []byte
+// 	PMTPacket = append(PMTPacket, tsHeader...)
+// 	PMTPacket = append(PMTPacket, bw.Bytes()...)
+// 	PMTPacket = append(PMTPacket, stuffingBytes...)
+
+// 	fmt.Println("-------------------------")
+// 	fmt.Println("Write PMT :", PMTPacket)
+// 	fmt.Println("-------------------------")
+
+// 	// 写PMT负载
+// 	if _, err = w.Write(PMTPacket); err != nil {
+// 		return
+// 	}
+
+// 	return
+// }
+
+func WritePMTPacket(w io.Writer, videoCodec codec.VideoCodecID, audioCodec codec.AudioCodecID) {
+	w.Write(TSHeader)
+	crc := make([]byte, 4)
+	paddingSize := TS_PACKET_SIZE - len(crc) - len(PSI) - len(PMT) - len(TSHeader) - 10
+	pmt := net.Buffers{PSI, PMT}
+	switch videoCodec {
+	case codec.CodecID_H264:
+		pmt = append(pmt, h264)
+	case codec.CodecID_H265:
+		pmt = append(pmt, h265)
+	default:
+		paddingSize += 5
+	}
+	switch audioCodec {
+	case codec.CodecID_AAC:
+		pmt = append(pmt, aac)
+	case codec.CodecID_PCMA:
+		pmt = append(pmt, pcma)
+	case codec.CodecID_PCMU:
+		pmt = append(pmt, pcmu)
+	default:
+		paddingSize += 5
+	}
+	util.PutBE(crc, GetCRC32_2(pmt))
+	pmt = append(pmt, crc, Stuffing[:paddingSize])
+	pmt.WriteTo(w)
+}

+ 231 - 0
m7s.live/engine/v4@v4.14.6/codec/mpegts/mpegts_psi.go

@@ -0,0 +1,231 @@
+package mpegts
+
+import (
+	"errors"
+	"fmt"
+	"io"
+
+	"m7s.live/engine/v4/util"
+)
+
+//
+// PSI
+//
+
+const (
+	PSI_TYPE_PAT      = 1
+	PSI_TYPE_PMT      = 2
+	PSI_TYPE_NIT      = 3
+	PSI_TYPE_CAT      = 4
+	PSI_TYPE_TST      = 5
+	PSI_TYPE_IPMP_CIT = 6
+)
+
+type MpegTsPSI struct {
+	// PAT
+	// PMT
+	// CAT
+	// NIT
+	Pat MpegTsPAT
+	Pmt MpegTsPMT
+}
+
+// 当传输流包有效载荷包含 PSI 数据时,payload_unit_start_indicator 具有以下意义:
+// 若传输流包承载 PSI分段的首字节,则 payload_unit_start_indicator 值必为 1,指示此传输流包的有效载荷的首字节承载pointer_field.
+// 若传输流包不承载 PSI 分段的首字节,则 payload_unit_start_indicator 值必为 0,指示在此有效载荷中不存在 pointer_field
+// 只要是PSI就一定会有pointer_field
+func ReadPSI(r io.Reader, pt uint32) (lr *io.LimitedReader, psi MpegTsPSI, err error) {
+	// pointer field(8)
+	cr, ok := r.(*util.Crc32Reader)
+	if ok {
+		r = cr.R
+	}
+	pointer_field, err := util.ReadByteToUint8(r)
+	if err != nil {
+		return
+	}
+
+	if pointer_field != 0 {
+		// 无论如何都应该确保能将pointer_field读取到,并且io.Reader指针向下移动
+		// ioutil.Discard常用在,http中,如果Get请求,获取到了很大的Body,要丢弃Body,就用这个方法.
+		// 因为http默认重链接的时候,必须等body读取完成.
+		// 用于发送需要读取但不想存储的数据,目的是耗尽读取端的数据
+		if _, err = io.CopyN(io.Discard, r, int64(pointer_field)); err != nil {
+			return
+		}
+	}
+	if ok {
+		r = cr
+	}
+
+	// table id(8)
+	tableId, err := util.ReadByteToUint8(r)
+	if err != nil {
+		return
+	}
+
+	// sectionSyntaxIndicator(1) + zero(1)  + reserved1(2) + sectionLength(12)
+	// sectionLength 前两个字节固定为00
+	sectionSyntaxIndicatorAndSectionLength, err := util.ReadByteToUint16(r, true)
+	if err != nil {
+		return
+	}
+
+	// 指定该分段的字节数,紧随 section_length 字段开始,并包括 CRC
+	// 因此剩下最多只能在读sectionLength长度的字节
+	lr = &io.LimitedReader{R: r, N: int64(sectionSyntaxIndicatorAndSectionLength & 0x3FF)}
+
+	// PAT TransportStreamID(16) or PMT ProgramNumber(16)
+	transportStreamIdOrProgramNumber, err := util.ReadByteToUint16(lr, true)
+	if err != nil {
+		return
+	}
+
+	// reserved2(2) + versionNumber(5) + currentNextIndicator(1)
+	versionNumberAndCurrentNextIndicator, err := util.ReadByteToUint8(lr)
+	if err != nil {
+		return
+	}
+
+	// sectionNumber(8)
+	sectionNumber, err := util.ReadByteToUint8(lr)
+	if err != nil {
+		return
+	}
+
+	// lastSectionNumber(8)
+	lastSectionNumber, err := util.ReadByteToUint8(lr)
+	if err != nil {
+		return
+	}
+
+	// 因为lr.N是从sectionLength开始计算,所以要减去 pointer_field(8) + table id(8) +  sectionSyntaxIndicator(1) + zero(1)  + reserved1(2) + sectionLength(12)
+	lr.N -= 4
+
+	switch pt {
+	case PSI_TYPE_PAT:
+		{
+			if tableId != TABLE_PAS {
+				err = errors.New(fmt.Sprintf("%s, id=%d", "read pmt table id != 2", tableId))
+				return
+			}
+
+			psi.Pat.TableID = tableId
+			psi.Pat.SectionSyntaxIndicator = uint8((sectionSyntaxIndicatorAndSectionLength & 0x8000) >> 15)
+			psi.Pat.SectionLength = sectionSyntaxIndicatorAndSectionLength & 0x3FF
+			psi.Pat.TransportStreamID = transportStreamIdOrProgramNumber
+			psi.Pat.VersionNumber = versionNumberAndCurrentNextIndicator & 0x3e
+			psi.Pat.CurrentNextIndicator = versionNumberAndCurrentNextIndicator & 0x01
+			psi.Pat.SectionNumber = sectionNumber
+			psi.Pat.LastSectionNumber = lastSectionNumber
+		}
+	case PSI_TYPE_PMT:
+		{
+			if tableId != TABLE_TSPMS {
+				err = errors.New(fmt.Sprintf("%s, id=%d", "read pmt table id != 2", tableId))
+				return
+			}
+
+			psi.Pmt.TableID = tableId
+			psi.Pmt.SectionSyntaxIndicator = uint8((sectionSyntaxIndicatorAndSectionLength & 0x8000) >> 15)
+			psi.Pmt.SectionLength = sectionSyntaxIndicatorAndSectionLength & 0x3FF
+			psi.Pmt.ProgramNumber = transportStreamIdOrProgramNumber
+			psi.Pmt.VersionNumber = versionNumberAndCurrentNextIndicator & 0x3e
+			psi.Pmt.CurrentNextIndicator = versionNumberAndCurrentNextIndicator & 0x01
+			psi.Pmt.SectionNumber = sectionNumber
+			psi.Pmt.LastSectionNumber = lastSectionNumber
+		}
+	}
+
+	return
+}
+
+func WritePSI(w io.Writer, pt uint32, psi MpegTsPSI, data []byte) (err error) {
+	var tableId, versionNumberAndCurrentNextIndicator, sectionNumber, lastSectionNumber uint8
+	var sectionSyntaxIndicatorAndSectionLength, transportStreamIdOrProgramNumber uint16
+
+	switch pt {
+	case PSI_TYPE_PAT:
+		{
+			if psi.Pat.TableID != TABLE_PAS {
+				err = errors.New(fmt.Sprintf("%s, id=%d", "write pmt table id != 0", tableId))
+				return
+			}
+
+			tableId = psi.Pat.TableID
+			sectionSyntaxIndicatorAndSectionLength = uint16(psi.Pat.SectionSyntaxIndicator)<<15 | 3<<12 | psi.Pat.SectionLength
+			transportStreamIdOrProgramNumber = psi.Pat.TransportStreamID
+			versionNumberAndCurrentNextIndicator = psi.Pat.VersionNumber<<1 | psi.Pat.CurrentNextIndicator
+			sectionNumber = psi.Pat.SectionNumber
+			lastSectionNumber = psi.Pat.LastSectionNumber
+		}
+	case PSI_TYPE_PMT:
+		{
+			if psi.Pmt.TableID != TABLE_TSPMS {
+				err = errors.New(fmt.Sprintf("%s, id=%d", "write pmt table id != 2", tableId))
+				return
+			}
+
+			tableId = psi.Pmt.TableID
+			sectionSyntaxIndicatorAndSectionLength = uint16(psi.Pmt.SectionSyntaxIndicator)<<15 | 3<<12 | psi.Pmt.SectionLength
+			transportStreamIdOrProgramNumber = psi.Pmt.ProgramNumber
+			versionNumberAndCurrentNextIndicator = psi.Pmt.VersionNumber<<1 | psi.Pmt.CurrentNextIndicator
+			sectionNumber = psi.Pmt.SectionNumber
+			lastSectionNumber = psi.Pmt.LastSectionNumber
+		}
+	}
+
+	// pointer field(8)
+	if err = util.WriteUint8ToByte(w, 0); err != nil {
+		return
+	}
+
+	cw := &util.Crc32Writer{W: w, Crc32: 0xffffffff}
+
+	// table id(8)
+	if err = util.WriteUint8ToByte(cw, tableId); err != nil {
+		return
+	}
+
+	// sectionSyntaxIndicator(1) + zero(1)  + reserved1(2) + sectionLength(12)
+	// sectionLength 前两个字节固定为00
+	// 1 0 11 sectionLength
+	if err = util.WriteUint16ToByte(cw, sectionSyntaxIndicatorAndSectionLength, true); err != nil {
+		return
+	}
+
+	// PAT TransportStreamID(16) or PMT ProgramNumber(16)
+	if err = util.WriteUint16ToByte(cw, transportStreamIdOrProgramNumber, true); err != nil {
+		return
+	}
+
+	// reserved2(2) + versionNumber(5) + currentNextIndicator(1)
+	// 0x3 << 6 -> 1100 0000
+	// 0x3 << 6  | 1 -> 1100 0001
+	if err = util.WriteUint8ToByte(cw, versionNumberAndCurrentNextIndicator); err != nil {
+		return
+	}
+
+	// sectionNumber(8)
+	if err = util.WriteUint8ToByte(cw, sectionNumber); err != nil {
+		return
+	}
+
+	// lastSectionNumber(8)
+	if err = util.WriteUint8ToByte(cw, lastSectionNumber); err != nil {
+		return
+	}
+
+	// data
+	if _, err = cw.Write(data); err != nil {
+		return
+	}
+
+	// crc32
+	crc32 := util.BigLittleSwap(uint(cw.Crc32))
+	if err = util.WriteUint32ToByte(cw, uint32(crc32), true); err != nil {
+		return
+	}
+
+	return
+}

+ 227 - 0
m7s.live/engine/v4@v4.14.6/codec/sps.go

@@ -0,0 +1,227 @@
+package codec
+
+import (
+	"bytes"
+
+	"github.com/cnotch/ipchub/av/codec/hevc"
+
+	"m7s.live/engine/v4/util/bits"
+)
+
+type SPSInfo struct {
+	ProfileIdc uint
+	LevelIdc   uint
+
+	MbWidth  uint
+	MbHeight uint
+
+	CropLeft   uint
+	CropRight  uint
+	CropTop    uint
+	CropBottom uint
+
+	Width  uint
+	Height uint
+}
+
+func ParseSPS(data []byte) (self SPSInfo, err error) {
+	r := &bits.GolombBitReader{R: bytes.NewReader(data)}
+
+	if _, err = r.ReadBits(8); err != nil {
+		return
+	}
+
+	if self.ProfileIdc, err = r.ReadBits(8); err != nil {
+		return
+	}
+
+	// constraint_set0_flag-constraint_set6_flag,reserved_zero_2bits
+	if _, err = r.ReadBits(8); err != nil {
+		return
+	}
+
+	// level_idc
+	if self.LevelIdc, err = r.ReadBits(8); err != nil {
+		return
+	}
+
+	// seq_parameter_set_id
+	if _, err = r.ReadExponentialGolombCode(); err != nil {
+		return
+	}
+
+	if self.ProfileIdc == 100 || self.ProfileIdc == 110 ||
+		self.ProfileIdc == 122 || self.ProfileIdc == 244 ||
+		self.ProfileIdc == 44 || self.ProfileIdc == 83 ||
+		self.ProfileIdc == 86 || self.ProfileIdc == 118 {
+
+		var chroma_format_idc uint
+		if chroma_format_idc, err = r.ReadExponentialGolombCode(); err != nil {
+			return
+		}
+
+		if chroma_format_idc == 3 {
+			// residual_colour_transform_flag
+			if _, err = r.ReadBit(); err != nil {
+				return
+			}
+		}
+
+		// bit_depth_luma_minus8
+		if _, err = r.ReadExponentialGolombCode(); err != nil {
+			return
+		}
+		// bit_depth_chroma_minus8
+		if _, err = r.ReadExponentialGolombCode(); err != nil {
+			return
+		}
+		// qpprime_y_zero_transform_bypass_flag
+		if _, err = r.ReadBit(); err != nil {
+			return
+		}
+
+		var seq_scaling_matrix_present_flag uint
+		if seq_scaling_matrix_present_flag, err = r.ReadBit(); err != nil {
+			return
+		}
+
+		if seq_scaling_matrix_present_flag != 0 {
+			for i := 0; i < 8; i++ {
+				var seq_scaling_list_present_flag uint
+				if seq_scaling_list_present_flag, err = r.ReadBit(); err != nil {
+					return
+				}
+				if seq_scaling_list_present_flag != 0 {
+					var sizeOfScalingList uint
+					if i < 6 {
+						sizeOfScalingList = 16
+					} else {
+						sizeOfScalingList = 64
+					}
+					lastScale := uint(8)
+					nextScale := uint(8)
+					for j := uint(0); j < sizeOfScalingList; j++ {
+						if nextScale != 0 {
+							var delta_scale uint
+							if delta_scale, err = r.ReadSE(); err != nil {
+								return
+							}
+							nextScale = (lastScale + delta_scale + 256) % 256
+						}
+						if nextScale != 0 {
+							lastScale = nextScale
+						}
+					}
+				}
+			}
+		}
+	}
+
+	// log2_max_frame_num_minus4
+	if _, err = r.ReadExponentialGolombCode(); err != nil {
+		return
+	}
+
+	var pic_order_cnt_type uint
+	if pic_order_cnt_type, err = r.ReadExponentialGolombCode(); err != nil {
+		return
+	}
+	if pic_order_cnt_type == 0 {
+		// log2_max_pic_order_cnt_lsb_minus4
+		if _, err = r.ReadExponentialGolombCode(); err != nil {
+			return
+		}
+	} else if pic_order_cnt_type == 1 {
+		// delta_pic_order_always_zero_flag
+		if _, err = r.ReadBit(); err != nil {
+			return
+		}
+		// offset_for_non_ref_pic
+		if _, err = r.ReadSE(); err != nil {
+			return
+		}
+		// offset_for_top_to_bottom_field
+		if _, err = r.ReadSE(); err != nil {
+			return
+		}
+		var num_ref_frames_in_pic_order_cnt_cycle uint
+		if num_ref_frames_in_pic_order_cnt_cycle, err = r.ReadExponentialGolombCode(); err != nil {
+			return
+		}
+		for i := uint(0); i < num_ref_frames_in_pic_order_cnt_cycle; i++ {
+			if _, err = r.ReadSE(); err != nil {
+				return
+			}
+		}
+	}
+
+	// max_num_ref_frames
+	if _, err = r.ReadExponentialGolombCode(); err != nil {
+		return
+	}
+
+	// gaps_in_frame_num_value_allowed_flag
+	if _, err = r.ReadBit(); err != nil {
+		return
+	}
+
+	if self.MbWidth, err = r.ReadExponentialGolombCode(); err != nil {
+		return
+	}
+	self.MbWidth++
+
+	if self.MbHeight, err = r.ReadExponentialGolombCode(); err != nil {
+		return
+	}
+	self.MbHeight++
+
+	var frame_mbs_only_flag uint
+	if frame_mbs_only_flag, err = r.ReadBit(); err != nil {
+		return
+	}
+	if frame_mbs_only_flag == 0 {
+		// mb_adaptive_frame_field_flag
+		if _, err = r.ReadBit(); err != nil {
+			return
+		}
+	}
+
+	// direct_8x8_inference_flag
+	if _, err = r.ReadBit(); err != nil {
+		return
+	}
+
+	var frame_cropping_flag uint
+	if frame_cropping_flag, err = r.ReadBit(); err != nil {
+		return
+	}
+	if frame_cropping_flag != 0 {
+		if self.CropLeft, err = r.ReadExponentialGolombCode(); err != nil {
+			return
+		}
+		if self.CropRight, err = r.ReadExponentialGolombCode(); err != nil {
+			return
+		}
+		if self.CropTop, err = r.ReadExponentialGolombCode(); err != nil {
+			return
+		}
+		if self.CropBottom, err = r.ReadExponentialGolombCode(); err != nil {
+			return
+		}
+	}
+
+	self.Width = (self.MbWidth * 16) - self.CropLeft*2 - self.CropRight*2
+	self.Height = ((2 - frame_mbs_only_flag) * self.MbHeight * 16) - self.CropTop*2 - self.CropBottom*2
+
+	return
+}
+
+func ParseHevcSPS(data []byte) (self SPSInfo, err error) {
+	var rawsps hevc.H265RawSPS
+	if err = rawsps.Decode(data); err == nil {
+		self.CropLeft, self.CropRight, self.CropTop, self.CropBottom = uint(rawsps.Conf_win_left_offset), uint(rawsps.Conf_win_right_offset), uint(rawsps.Conf_win_top_offset), uint(rawsps.Conf_win_bottom_offset)
+		self.Width = uint(rawsps.Pic_width_in_luma_samples)
+		self.Height = uint(rawsps.Pic_height_in_luma_samples)
+	}
+	return
+}

+ 70 - 0
m7s.live/engine/v4@v4.14.6/common/dtsestimator.go

@@ -0,0 +1,70 @@
+package common
+
+// DTSEstimator is a DTS estimator.
+type DTSEstimator struct {
+	hasB    bool
+	prevPTS uint32
+	prevDTS uint32
+	cache   []uint32
+}
+
+// NewDTSEstimator allocates a DTSEstimator.
+func NewDTSEstimator() *DTSEstimator {
+	result := &DTSEstimator{}
+	return result
+}
+
+func (d *DTSEstimator) Clone() *DTSEstimator {
+	return &DTSEstimator{
+		d.hasB,
+		d.prevPTS,
+		d.prevDTS,
+		append([]uint32(nil), d.cache...),
+	}
+}
+
+func (d *DTSEstimator) add(pts uint32) {
+	i := 0
+	l := len(d.cache)
+	if l >= 4 {
+		l--
+		// i = l - 3
+		d.cache = append(d.cache[:0], d.cache[1:]...)[:l]
+	}
+
+	for ; i < l; i = i + 1 {
+		if d.cache[i] > pts {
+			break
+		}
+	}
+	d.cache = append(d.cache, pts)
+	d.cache = append(d.cache[:i+1], d.cache[i:l]...)
+	d.cache[i] = pts
+}
+
+// Feed provides PTS to the estimator, and returns the estimated DTS.
+func (d *DTSEstimator) Feed(pts uint32) uint32 {
+	if pts < d.prevPTS && d.prevPTS-pts > 0x80000000 {
+		*d = *NewDTSEstimator()
+	}
+	d.add(pts)
+	dts := pts
+	if !d.hasB {
+		if pts < d.prevPTS {
+			d.hasB = true
+			dts = d.cache[0]
+		}
+	} else {
+		dts = d.cache[0]
+	}
+
+	if d.prevDTS > dts {
+		dts = d.prevDTS
+	}
+	// if d.prevDTS >= dts {
+	// 	dts = d.prevDTS + 90
+	// }
+	d.prevPTS = pts
+	d.prevDTS = dts
+	return dts
+}

+ 17 - 0
m7s.live/engine/v4@v4.14.6/common/dtsestimator_test.go

@@ -0,0 +1,17 @@
+package common
+
+import (
+	"testing"
+)
+
+func TestDts(t *testing.T) {
+	t.Run(t.Name(), func(t *testing.T) {
+		dtsg := NewDTSEstimator()
+		var pts uint32 = 0xFFFFFFFF - 5
+		for i := 0; i < 10; i++ {
+			dts := dtsg.Feed(pts)
+			pts++
+			t.Logf("dts=%d", dts)
+		}
+	})
+}

+ 206 - 0
m7s.live/engine/v4@v4.14.6/common/frame.go

@@ -0,0 +1,206 @@
+package common
+
+import (
+	"bytes"
+	"io"
+	"net"
+	"sync"
+	"sync/atomic"
+	"time"
+
+	"github.com/pion/rtp"
+	"m7s.live/engine/v4/codec"
+	"m7s.live/engine/v4/log"
+	"m7s.live/engine/v4/util"
+)
+
+func SplitAnnexB[T ~[]byte](frame T, process func(T), delimiter []byte) {
+	for after := frame; len(frame) > 0; frame = after {
+		if frame, after, _ = bytes.Cut(frame, delimiter); len(frame) > 0 {
+			process(frame)
+		}
+	}
+}
+
+type RTPFrame struct {
+	*rtp.Packet
+	Raw []byte
+}
+
+func (r *RTPFrame) H264Type() (naluType codec.H264NALUType) {
+	return naluType.Parse(r.Payload[0])
+}
+func (r *RTPFrame) H265Type() (naluType codec.H265NALUType) {
+	return naluType.Parse(r.Payload[0])
+}
+
+func (r *RTPFrame) Unmarshal(raw []byte) *RTPFrame {
+	if r.Packet == nil {
+		r.Packet = &rtp.Packet{}
+	}
+	if err := r.Packet.Unmarshal(raw); err != nil {
+		log.Error(err)
+		return nil
+	}
+	return r
+}
+
+type IDataFrame[T any] interface {
+	Init()               // 初始化
+	Reset()              // 重置数据,复用内存
+	Ready()              // 标记为可读取
+	ReaderEnter() int32  // 读取者数量+1
+	ReaderLeave() int32  // 读取者数量-1
+	StartWrite() bool    // 开始写入
+	SetSequence(uint32)  // 设置序号
+	GetSequence() uint32 // 获取序号
+	ReaderCount() int32  // 读取者数量
+	Discard() int32      // 如果写入时还有读取者没有离开则废弃该帧,剥离RingBuffer,防止并发读写
+	IsDiscarded() bool   // 是否已废弃
+	IsWriting() bool     // 是否正在写入
+	Wait()               // 阻塞等待可读取
+	Broadcast()          // 广播可读取
+}
+
+type DataFrame[T any] struct {
+	DeltaTime   uint32       // 相对上一帧时间戳,毫秒
+	WriteTime   time.Time    // 写入时间,可用于比较两个帧的先后
+	Sequence    uint32       // 在一个Track中的序号
+	BytesIn     int          // 输入字节数用于计算BPS
+	CanRead     bool         `json:"-" yaml:"-"` // 是否可读取
+	readerCount atomic.Int32 `json:"-" yaml:"-"` // 读取者数量
+	Data        T            `json:"-" yaml:"-"`
+	sync.Cond   `json:"-" yaml:"-"`
+}
+
+func NewDataFrame[T any]() *DataFrame[T] {
+	return &DataFrame[T]{}
+}
+func (df *DataFrame[T]) IsWriting() bool {
+	return !df.CanRead
+}
+
+func (df *DataFrame[T]) IsDiscarded() bool {
+	return df.L == nil
+}
+
+func (df *DataFrame[T]) Discard() int32 {
+	df.L = nil //标记为废弃
+	return df.readerCount.Load()
+}
+
+func (df *DataFrame[T]) SetSequence(sequence uint32) {
+	df.Sequence = sequence
+}
+
+func (df *DataFrame[T]) GetSequence() uint32 {
+	return df.Sequence
+}
+
+func (df *DataFrame[T]) ReaderEnter() int32 {
+	return df.readerCount.Add(1)
+}
+
+func (df *DataFrame[T]) ReaderCount() int32 {
+	return df.readerCount.Load()
+}
+
+func (df *DataFrame[T]) ReaderLeave() int32 {
+	return df.readerCount.Add(-1)
+}
+
+func (df *DataFrame[T]) StartWrite() bool {
+	if df.readerCount.Load() > 0 {
+		df.Discard() //标记为废弃
+		return false
+	} else {
+		df.CanRead = false //标记为正在写入
+		return true
+	}
+}
+
+func (df *DataFrame[T]) Ready() {
+	df.WriteTime = time.Now()
+	df.CanRead = true //标记为可读取
+	df.Broadcast()
+}
+
+func (df *DataFrame[T]) Init() {
+	df.L = EmptyLocker
+}
+
+func (df *DataFrame[T]) Reset() {
+	df.BytesIn = 0
+	df.DeltaTime = 0
+}
+
+type AVFrame struct {
+	DataFrame[any]
+	IFrame    bool
+	PTS       time.Duration
+	DTS       time.Duration
+	Timestamp time.Duration               // 绝对时间戳
+	ADTS      *util.ListItem[util.Buffer] `json:"-" yaml:"-"` // ADTS头
+	AVCC      util.BLL                    `json:"-" yaml:"-"` // 打包好的AVCC格式(MPEG-4格式、Byte-Stream Format)
+	RTP       util.List[RTPFrame]         `json:"-" yaml:"-"`
+	AUList    util.BLLs                   `json:"-" yaml:"-"` // 裸数据
+}
+
+func NewAVFrame() *AVFrame {
+	return &AVFrame{}
+}
+
+func (av *AVFrame) WriteAVCC(ts uint32, frame *util.BLL) {
+	if ts == 0 {
+		ts = 1
+	}
+	av.Timestamp = time.Duration(ts) * time.Millisecond
+	av.BytesIn += frame.ByteLength
+	for {
+		item := frame.Shift()
+		if item == nil {
+			break
+		}
+		av.AVCC.Push(item)
+	}
+	// frame.Transfer(&av.AVCC)
+	// frame.ByteLength = 0
+}
+
+// Reset 重置数据,复用内存
+func (av *AVFrame) Reset() {
+	av.RTP.Recycle()
+	av.AVCC.Recycle()
+	av.AUList.Recycle()
+	if av.ADTS != nil {
+		av.ADTS.Recycle()
+		av.ADTS = nil
+	}
+	av.Timestamp = 0
+	av.IFrame = false
+	av.DataFrame.Reset()
+}
+
+type ParamaterSets [][]byte
+
+func (v ParamaterSets) GetAnnexB() (r net.Buffers) {
+	for _, v := range v {
+		r = append(r, codec.NALU_Delimiter2, v)
+	}
+	return
+}
+
+func (v ParamaterSets) WriteAnnexBTo(w io.Writer) (n int, err error) {
+	var n1, n2 int
+	for _, v := range v {
+		if n1, err = w.Write(codec.NALU_Delimiter2); err != nil {
+			return
+		}
+		n += n1
+		if n2, err = w.Write(v); err != nil {
+			return
+		}
+		n += n2
+	}
+	return
+}

+ 139 - 0
m7s.live/engine/v4@v4.14.6/common/index.go

@@ -0,0 +1,139 @@
+package common
+
+import (
+	"sync/atomic"
+	"time"
+
+	"github.com/pion/rtp"
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/log"
+	"m7s.live/engine/v4/util"
+)
+
+type TimelineData[T any] struct {
+	Timestamp time.Time
+	Value     T
+}
+type TrackState byte
+
+const (
+	TrackStateOnline  TrackState = iota // 上线
+	TrackStateOffline                   // 下线
+)
+
+// Base 基础Track类
+type Base[T any, F IDataFrame[T]] struct {
+	RingWriter[T, F]
+	Name      string
+	log.Zap   `json:"-" yaml:"-"`
+	Stream    IStream     `json:"-" yaml:"-"`
+	Attached  atomic.Bool `json:"-" yaml:"-"`
+	State     TrackState
+	ts        time.Time
+	bytes     int
+	frames    int
+	DropCount int `json:"-" yaml:"-"` //丢帧数
+	BPS       int
+	FPS       int
+	Drops     int   // 丢帧率
+	RawSize   int   // 裸数据长度
+	RawPart   []int // 裸数据片段用于UI上显示
+}
+
+func (bt *Base[T, F]) ComputeBPS(bytes int) {
+	bt.bytes += bytes
+	bt.frames++
+	if elapse := time.Since(bt.ts).Seconds(); elapse > 1 {
+		bt.BPS = int(float64(bt.bytes) / elapse)
+		bt.FPS = int(float64(bt.frames) / elapse)
+		bt.Drops = int(float64(bt.DropCount) / elapse)
+		bt.bytes = 0
+		bt.frames = 0
+		bt.DropCount = 0
+		bt.ts = time.Now()
+	}
+}
+
+func (bt *Base[T, F]) GetName() string {
+	return bt.Name
+}
+
+func (bt *Base[T, F]) GetBPS() int {
+	return bt.BPS
+}
+
+func (bt *Base[T, F]) GetFPS() int {
+	return bt.FPS
+}
+
+func (bt *Base[T, F]) GetDrops() int {
+	return bt.Drops
+}
+
+// GetRBSize 获取缓冲区大小
+func (bt *Base[T, F]) GetRBSize() int {
+	return bt.RingWriter.Size
+}
+
+func (bt *Base[T, F]) SnapForJson() {
+}
+
+func (bt *Base[T, F]) SetStuff(stuff ...any) {
+	for _, s := range stuff {
+		switch v := s.(type) {
+		case IStream:
+			bt.Stream = v
+			bt.Zap = v.With(zap.String("track", bt.Name))
+		case TrackState:
+			bt.State = v
+		case string:
+			bt.Name = v
+		}
+	}
+}
+
+func (bt *Base[T, F]) Dispose() {
+	bt.Value.Broadcast()
+}
+
+type Track interface {
+	GetReaderCount() int32
+	GetName() string
+	GetBPS() int
+	GetFPS() int
+	GetDrops() int
+	LastWriteTime() time.Time
+	SnapForJson()
+	SetStuff(stuff ...any)
+	GetRBSize() int
+	Dispose()
+}
+
+type AVTrack interface {
+	Track
+	PreFrame() *AVFrame
+	CurrentFrame() *AVFrame
+	Attach()
+	Detach()
+	WriteAVCC(ts uint32, frame *util.BLL) error //写入AVCC格式的数据
+	WriteRTP(*util.ListItem[RTPFrame])
+	WriteRTPPack(*rtp.Packet)
+	WriteSequenceHead(sh []byte) error
+	Flush()
+	SetSpeedLimit(time.Duration)
+	GetRTPFromPool() *util.ListItem[RTPFrame]
+	GetFromPool(util.IBytes) *util.ListItem[util.Buffer]
+}
+type VideoTrack interface {
+	AVTrack
+	WriteSliceBytes(slice []byte)
+	WriteNalu(uint32, uint32, []byte)
+	WriteAnnexB(uint32, uint32, []byte)
+	SetLostFlag()
+}
+
+type AudioTrack interface {
+	AVTrack
+	WriteADTS(uint32, util.IBytes)
+	WriteRawBytes(uint32, util.IBytes)
+}

+ 118 - 0
m7s.live/engine/v4@v4.14.6/common/ring-writer.go

@@ -0,0 +1,118 @@
+package common
+
+import (
+	"sync/atomic"
+
+	"m7s.live/engine/v4/util"
+)
+
+type emptyLocker struct{}
+
+func (emptyLocker) Lock()   {}
+func (emptyLocker) Unlock() {}
+
+var EmptyLocker emptyLocker
+
+type RingWriter[T any, F IDataFrame[T]] struct {
+	*util.Ring[F] `json:"-" yaml:"-"`
+	ReaderCount   atomic.Int32 `json:"-" yaml:"-"`
+	pool          *util.Ring[F]
+	poolSize      int
+	Size          int
+	LastValue     F
+	constructor   func() F
+}
+
+func (rb *RingWriter[T, F]) create(n int) (ring *util.Ring[F]) {
+	ring = util.NewRing[F](n)
+	for p, i := ring, n; i > 0; p, i = p.Next(), i-1 {
+		p.Value = rb.constructor()
+		p.Value.Init()
+	}
+	return
+}
+
+func (rb *RingWriter[T, F]) Init(n int, constructor func() F) *RingWriter[T, F] {
+	rb.constructor = constructor
+	rb.Ring = rb.create(n)
+	rb.Size = n
+	rb.LastValue = rb.Value
+	return rb
+}
+
+// func (rb *RingBuffer[T, F]) MoveNext() F {
+// 	rb.LastValue = rb.Value
+// 	rb.Ring = rb.Next()
+// 	return rb.Value
+// }
+
+func (rb *RingWriter[T, F]) Glow(size int) (newItem *util.Ring[F]) {
+	if size < rb.poolSize {
+		newItem = rb.pool.Unlink(size)
+		rb.poolSize -= size
+	} else if size == rb.poolSize {
+		newItem = rb.pool
+		rb.poolSize = 0
+		rb.pool = nil
+	} else {
+		newItem = rb.create(size - rb.poolSize).Link(rb.pool)
+		rb.poolSize = 0
+		rb.pool = nil
+	}
+	rb.Link(newItem)
+	rb.Size += size
+	return
+}
+
+func (rb *RingWriter[T, F]) Recycle(r *util.Ring[F]) {
+	rb.poolSize++
+	r.Value.Init()
+	r.Value.Reset()
+	if rb.pool == nil {
+		rb.pool = r
+	} else {
+		rb.pool.Link(r)
+	}
+}
+
+func (rb *RingWriter[T, F]) Reduce(size int) {
+	r := rb.Unlink(size)
+	if size > 1 {
+		for p := r.Next(); p != r; {
+			next := p.Next() //先保存下一个节点
+			if p.Value.Discard() == 0 {
+				rb.Recycle(p.Prev().Unlink(1))
+			} else {
+				// fmt.Println("Reduce", p.Value.ReaderCount())
+			}
+			p = next
+		}
+	}
+	if r.Value.Discard() == 0 {
+		rb.Recycle(r)
+	}
+	rb.Size -= size
+	return
+}
+
+func (rb *RingWriter[T, F]) Step() (normal bool) {
+	rb.LastValue.Broadcast() // 防止订阅者还在等待
+	rb.LastValue = rb.Value
+	nextSeq := rb.LastValue.GetSequence() + 1
+	next := rb.Next()
+	if normal = next.Value.StartWrite(); normal {
+		next.Value.Reset()
+		rb.Ring = next
+	} else {
+		rb.Reduce(1)         //抛弃还有订阅者的节点
+		rb.Ring = rb.Glow(1) //补充一个新节点
+		rb.Value.StartWrite()
+	}
+	rb.Value.SetSequence(nextSeq)
+	rb.LastValue.Ready()
+	return
+}
+
+func (rb *RingWriter[T, F]) GetReaderCount() int32 {
+	return rb.ReaderCount.Load()
+}

+ 23 - 0
m7s.live/engine/v4@v4.14.6/common/stream.go

@@ -0,0 +1,23 @@
+package common
+
+import (
+	"time"
+
+	"m7s.live/engine/v4/config"
+	"m7s.live/engine/v4/log"
+	"m7s.live/engine/v4/util"
+)
+
+type IStream interface {
+	AddTrack(Track) *util.Promise[Track]
+	RemoveTrack(Track)
+	Close()
+	IsClosed() bool
+	SSRC() uint32
+	log.Zap
+	Receive(any) bool
+	SetIDR(Track)
+	GetPublisherConfig() *config.Publish
+	GetStartTime() time.Time
+	GetType() string
+}

+ 352 - 0
m7s.live/engine/v4@v4.14.6/config/config.go

@@ -0,0 +1,352 @@
+package config
+
+import (
+	"encoding/json"
+	"fmt"
+	"net"
+	"net/http"
+	"os"
+	"reflect"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/quic-go/quic-go"
+	"gopkg.in/yaml.v3"
+	"m7s.live/engine/v4/log"
+)
+
+type Config struct {
+	Ptr     reflect.Value //指向配置结构体值
+	Value   any           //当前值,优先级:动态修改值>环境变量>配置文件>defaultYaml>全局配置>默认值
+	Modify  any           //动态修改的值
+	Env     any           //环境变量中的值
+	File    any           //配置文件中的值
+	Global  *Config       //全局配置中的值,指针类型
+	Default any           //默认值
+	Enum    []struct {
+		Label string `json:"label"`
+		Value any    `json:"value"`
+	}
+	name     string // 小写
+	propsMap map[string]*Config
+	props    []*Config
+	tag      reflect.StructTag
+}
+
+var durationType = reflect.TypeOf(time.Duration(0))
+var regexpType = reflect.TypeOf(Regexp{})
+
+type Plugin interface {
+	// 可能的入参类型:FirstConfig 第一次初始化配置,Config 后续配置更新,SE系列(StateEvent)流状态变化事件
+	OnEvent(any)
+}
+
+type TCPPlugin interface {
+	Plugin
+	ServeTCP(net.Conn)
+}
+
+type HTTPPlugin interface {
+	Plugin
+	http.Handler
+}
+
+type QuicPlugin interface {
+	Plugin
+	ServeQuic(quic.Connection)
+}
+
+func (config *Config) Range(f func(key string, value Config)) {
+	if m, ok := config.Value.(map[string]Config); ok {
+		for k, v := range m {
+			f(k, v)
+		}
+	}
+}
+
+func (config *Config) IsMap() bool {
+	_, ok := config.Value.(map[string]Config)
+	return ok
+}
+
+func (config *Config) Get(key string) (v *Config) {
+	if config.propsMap == nil {
+		config.propsMap = make(map[string]*Config)
+	}
+	if v, ok := config.propsMap[key]; ok {
+		return v
+	} else {
+		v = &Config{
+			name: key,
+		}
+		config.propsMap[key] = v
+		config.props = append(config.props, v)
+		return v
+	}
+}
+
+func (config Config) Has(key string) (ok bool) {
+	if config.propsMap == nil {
+		return false
+	}
+	_, ok = config.propsMap[strings.ToLower(key)]
+	return ok
+}
+
+func (config *Config) MarshalJSON() ([]byte, error) {
+	if config.propsMap == nil {
+		return json.Marshal(config.Value)
+	}
+	return json.Marshal(config.propsMap)
+}
+
+// Parse 第一步读取配置结构体的默认值
+func (config *Config) Parse(s any, prefix ...string) {
+	var t reflect.Type
+	var v reflect.Value
+	if vv, ok := s.(reflect.Value); ok {
+		t, v = vv.Type(), vv
+	} else {
+		t, v = reflect.TypeOf(s), reflect.ValueOf(s)
+	}
+	if t.Kind() == reflect.Pointer {
+		t, v = t.Elem(), v.Elem()
+	}
+	config.Ptr = v
+	config.Default = v.Interface()
+	config.Value = v.Interface()
+	if len(prefix) > 0 { // 读取环境变量
+		envKey := strings.Join(prefix, "_")
+		if envValue := os.Getenv(envKey); envValue != "" {
+			envv := config.assign(strings.ToLower(prefix[0]), envValue)
+			config.Env = envv.Interface()
+			config.Value = config.Env
+			config.Ptr.Set(envv)
+		}
+	}
+	if t.Kind() == reflect.Struct && t != regexpType {
+		for i, j := 0, t.NumField(); i < j; i++ {
+			ft, fv := t.Field(i), v.Field(i)
+			if !ft.IsExported() {
+				continue
+			}
+			name := strings.ToLower(ft.Name)
+			if tag := ft.Tag.Get("yaml"); tag != "" {
+				if tag == "-" {
+					continue
+				}
+				name, _, _ = strings.Cut(tag, ",")
+			}
+			prop := config.Get(name)
+			prop.Parse(fv, append(prefix, strings.ToUpper(ft.Name))...)
+			prop.tag = ft.Tag
+			for _, kv := range strings.Split(ft.Tag.Get("enum"), ",") {
+				kvs := strings.Split(kv, ":")
+				if len(kvs) != 2 {
+					continue
+				}
+				var tmp struct {
+					Value any
+				}
+				yaml.Unmarshal([]byte(fmt.Sprintf("value: %s", strings.TrimSpace(kvs[0]))), &tmp)
+				prop.Enum = append(prop.Enum, struct {
+					Label string `json:"label"`
+					Value any    `json:"value"`
+				}{
+					Label: strings.TrimSpace(kvs[1]),
+					Value: tmp.Value,
+				})
+			}
+		}
+	}
+}
+
+// ParseDefaultYaml 第二步读取全局配置
+func (config *Config) ParseGlobal(g *Config) {
+	config.Global = g
+	if config.propsMap != nil {
+		for k, v := range config.propsMap {
+			v.ParseGlobal(g.Get(k))
+		}
+	} else {
+		config.Value = g.Value
+	}
+}
+
+// ParseDefaultYaml 第三步读取内嵌默认配置
+func (config *Config) ParseDefaultYaml(defaultYaml map[string]any) {
+	if defaultYaml == nil {
+		return
+	}
+	for k, v := range defaultYaml {
+		if config.Has(k) {
+			if prop := config.Get(k); prop.props != nil {
+				if v != nil {
+					prop.ParseDefaultYaml(v.(map[string]any))
+				}
+			} else {
+				dv := prop.assign(k, v)
+				prop.Default = dv.Interface()
+				if prop.Env == nil {
+					prop.Value = dv.Interface()
+					prop.Ptr.Set(dv)
+				}
+			}
+		}
+	}
+}
+
+// ParseFile 第四步读取用户配置文件
+func (config *Config) ParseUserFile(conf map[string]any) {
+	if conf == nil {
+		return
+	}
+	config.File = conf
+	for k, v := range conf {
+		if config.Has(k) {
+			if prop := config.Get(k); prop.props != nil {
+				if v != nil {
+					prop.ParseUserFile(v.(map[string]any))
+				}
+			} else {
+				fv := prop.assign(k, v)
+				prop.File = fv.Interface()
+				if prop.Env == nil {
+					prop.Value = fv.Interface()
+					prop.Ptr.Set(fv)
+				}
+			}
+		}
+	}
+}
+
+// ParseModifyFile 第五步读取动态修改配置文件
+func (config *Config) ParseModifyFile(conf map[string]any) {
+	if conf == nil {
+		return
+	}
+	config.Modify = conf
+	for k, v := range conf {
+		if config.Has(k) {
+			if prop := config.Get(k); prop.props != nil {
+				if v != nil {
+					vmap := v.(map[string]any)
+					prop.ParseModifyFile(vmap)
+					if len(vmap) == 0 {
+						delete(conf, k)
+					}
+				}
+			} else {
+				mv := prop.assign(k, v)
+				v = mv.Interface()
+				vwm := prop.valueWithoutModify()
+				if equal(vwm, v) {
+					delete(conf, k)
+					if prop.Modify != nil {
+						prop.Modify = nil
+						prop.Value = vwm
+						prop.Ptr.Set(reflect.ValueOf(vwm))
+					}
+					continue
+				}
+				prop.Modify = v
+				prop.Value = v
+				prop.Ptr.Set(mv)
+			}
+		}
+	}
+	if len(conf) == 0 {
+		config.Modify = nil
+	}
+}
+
+func (config *Config) valueWithoutModify() any {
+	if config.Env != nil {
+		return config.Env
+	}
+	if config.File != nil {
+		return config.File
+	}
+	if config.Global != nil {
+		return config.Global.Value
+	}
+	return config.Default
+}
+
+func equal(vwm, v any) bool {
+	ft := reflect.TypeOf(vwm)
+	switch ft {
+	case regexpType:
+		return vwm.(Regexp).String() == v.(Regexp).String()
+	default:
+		switch ft.Kind() {
+		case reflect.Slice, reflect.Array, reflect.Map:
+			return reflect.DeepEqual(vwm, v)
+		}
+		return vwm == v
+	}
+}
+
+func (config *Config) GetMap() map[string]any {
+	m := make(map[string]any)
+	for k, v := range config.propsMap {
+		if v.props != nil {
+			if vv := v.GetMap(); vv != nil {
+				m[k] = vv
+			}
+		} else if v.Value != nil {
+			m[k] = v.Value
+		}
+	}
+	if len(m) > 0 {
+		return m
+	}
+	return nil
+}
+
+var regexPureNumber = regexp.MustCompile(`^\d+$`)
+
+func (config *Config) assign(k string, v any) (target reflect.Value) {
+	ft := config.Ptr.Type()
+
+	source := reflect.ValueOf(v)
+
+	switch ft {
+	case durationType:
+		target = reflect.New(ft).Elem()
+		if source.Type() == durationType {
+			target.Set(source)
+		} else if source.IsZero() || !source.IsValid() {
+			target.SetInt(0)
+		} else {
+			timeStr := source.String()
+			if d, err := time.ParseDuration(timeStr); err == nil && !regexPureNumber.MatchString(timeStr) {
+				target.SetInt(int64(d))
+			} else {
+				if Global.LogLang == "zh" {
+					log.Errorf("%s 无效的时间值: %v 请添加单位(s,m,h,d),例如:100ms, 10s, 4m, 1h", k, source)
+				} else {
+					log.Errorf("%s invalid duration value: %v please add unit (s,m,h,d),eg: 100ms, 10s, 4m, 1h", k, source)
+				}
+				os.Exit(1)
+			}
+		}
+	case regexpType:
+		target = reflect.New(ft).Elem()
+		regexpStr := source.String()
+		target.Set(reflect.ValueOf(Regexp{regexp.MustCompile(regexpStr)}))
+	default:
+		tmpStruct := reflect.StructOf([]reflect.StructField{
+			{
+				Name: strings.ToUpper(k),
+				Type: ft,
+			},
+		})
+		tmpValue := reflect.New(tmpStruct)
+		tmpByte, _ := yaml.Marshal(map[string]any{k: v})
+		yaml.Unmarshal(tmpByte, tmpValue.Interface())
+		target = tmpValue.Elem().Field(0)
+	}
+	return
+}

+ 32 - 0
m7s.live/engine/v4@v4.14.6/config/config_test.go

@@ -0,0 +1,32 @@
+package config
+
+import (
+	"testing"
+)
+// TestModify 测试动态修改配置文件,比较值是否修改,修改后是否有Modify属性
+func TestModify(t *testing.T) {
+	t.Run(t.Name(), func(t *testing.T) {
+		var defaultValue struct{
+			Subscribe
+		}
+		defaultValue.SubAudio = false
+		var conf Config
+		conf.Parse(&defaultValue)
+		conf.ParseModifyFile(map[string]any{
+			"subscribe": map[string]any{
+				"subaudio": false,
+			},
+		})
+		if conf.Modify != nil {
+			t.Fail()
+		}
+		conf.ParseModifyFile(map[string]any{
+			"subscribe": map[string]any{
+				"subaudio": true,
+			},
+		})
+		if conf.Modify == nil {
+			t.Fail()
+		}
+	})
+}

+ 244 - 0
m7s.live/engine/v4@v4.14.6/config/formily.go

@@ -0,0 +1,244 @@
+package config
+
+import (
+	"fmt"
+	"reflect"
+	"strings"
+	"time"
+
+	"m7s.live/engine/v4/log"
+)
+
+type Property struct {
+	Type        string `json:"type"`
+	Title       string `json:"title"`
+	Description string `json:"description"`
+	Enum        []struct {
+		Label string `json:"label"`
+		Value any    `json:"value"`
+	} `json:"enum,omitempty"`
+	Items          *Object        `json:"items,omitempty"`
+	Properties     map[string]any `json:"properties,omitempty"`
+	Default        any            `json:"default,omitempty"`
+	Decorator      string         `json:"x-decorator"`
+	DecoratorProps map[string]any `json:"x-decorator-props,omitempty"`
+	Component      string         `json:"x-component"`
+	ComponentProps map[string]any `json:"x-component-props,omitempty"`
+	Index          int            `json:"x-index"`
+}
+
+type Card struct {
+	Type           string         `json:"type"`
+	Properties     map[string]any `json:"properties,omitempty"`
+	Component      string         `json:"x-component"`
+	ComponentProps map[string]any `json:"x-component-props,omitempty"`
+	Index          int            `json:"x-index"`
+}
+
+type Object struct {
+	Type       string         `json:"type"`
+	Properties map[string]any `json:"properties"`
+}
+
+func (config *Config) schema(index int) (r any) {
+	defer func() {
+		err := recover()
+		if err != nil {
+			log.Error(err)
+		}
+	}()
+	if config.props != nil {
+		r := Card{
+			Type:       "void",
+			Component:  "Card",
+			Properties: make(map[string]any),
+			Index:      index,
+		}
+		r.ComponentProps = map[string]any{
+			"title": config.name,
+		}
+		for i, v := range config.props {
+			if strings.HasPrefix(v.tag.Get("desc"), "废弃") {
+				continue
+			}
+			r.Properties[v.name] = v.schema(i)
+		}
+		return r
+	} else {
+		p := Property{
+			Title:   config.name,
+			Default: config.Value,
+			DecoratorProps: map[string]any{
+				"tooltip": config.tag.Get("desc"),
+			},
+			ComponentProps: map[string]any{},
+			Decorator:      "FormItem",
+			Index:          index,
+		}
+		if config.Modify != nil {
+			p.Description = "已动态修改"
+		} else if config.Env != nil {
+			p.Description = "使用环境变量中的值"
+		} else if config.File != nil {
+			p.Description = "使用配置文件中的值"
+		} else if config.Global != nil {
+			p.Description = "已使用全局配置中的值"
+		}
+		p.Enum = config.Enum
+		switch config.Ptr.Type() {
+		case regexpType:
+			p.Type = "string"
+			p.Component = "Input"
+			p.DecoratorProps["addonAfter"] = "正则表达式"
+			str := config.Value.(Regexp).String()
+			p.ComponentProps = map[string]any{
+				"placeholder": str,
+			}
+			p.Default = str
+		case durationType:
+			p.Type = "string"
+			p.Component = "Input"
+			str := config.Value.(time.Duration).String()
+			p.ComponentProps = map[string]any{
+				"placeholder": str,
+			}
+			p.Default = str
+			p.DecoratorProps["addonAfter"] = "时间,单位:s,m,h,d,例如:100ms, 10s, 4m, 1h"
+		default:
+			switch config.Ptr.Kind() {
+			case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Float32, reflect.Float64:
+				p.Type = "number"
+				p.Component = "InputNumber"
+				p.ComponentProps = map[string]any{
+					"placeholder": fmt.Sprintf("%v", config.Value),
+				}
+			case reflect.Bool:
+				p.Type = "boolean"
+				p.Component = "Switch"
+			case reflect.String:
+				p.Type = "string"
+				p.Component = "Input"
+				p.ComponentProps = map[string]any{
+					"placeholder": config.Value,
+				}
+			case reflect.Slice:
+				p.Type = "array"
+				p.Component = "Input"
+				p.ComponentProps = map[string]any{
+					"placeholder": config.Value,
+				}
+				p.DecoratorProps["addonAfter"] = "数组,每个元素用逗号分隔"
+			case reflect.Map:
+				var children []struct {
+					Key   string `json:"mkey"`
+					Value any    `json:"mvalue"`
+				}
+				p := Property{
+					Type:      "array",
+					Component: "ArrayTable",
+					Decorator: "FormItem",
+					Properties: map[string]any{
+						"addition": map[string]string{
+							"type":        "void",
+							"title":       "添加",
+							"x-component": "ArrayTable.Addition",
+						},
+					},
+					Index: index,
+					Title: config.name,
+					Items: &Object{
+						Type: "object",
+						Properties: map[string]any{
+							"c1": Card{
+								Type:      "void",
+								Component: "ArrayTable.Column",
+								ComponentProps: map[string]any{
+									"title": config.tag.Get("key"),
+									"width": 300,
+								},
+								Properties: map[string]any{
+									"mkey": Property{
+										Type:      "string",
+										Decorator: "FormItem",
+										Component: "Input",
+									},
+								},
+								Index: 0,
+							},
+							"c2": Card{
+								Type:      "void",
+								Component: "ArrayTable.Column",
+								ComponentProps: map[string]any{
+									"title": config.tag.Get("value"),
+								},
+								Properties: map[string]any{
+									"mvalue": Property{
+										Type:      "string",
+										Decorator: "FormItem",
+										Component: "Input",
+									},
+								},
+								Index: 1,
+							},
+							"operator": Card{
+								Type:      "void",
+								Component: "ArrayTable.Column",
+								ComponentProps: map[string]any{
+									"title": "操作",
+								},
+								Properties: map[string]any{
+									"remove": Card{
+										Type:      "void",
+										Component: "ArrayTable.Remove",
+									},
+								},
+								Index: 2,
+							},
+						},
+					},
+				}
+				iter := config.Ptr.MapRange()
+				for iter.Next() {
+					children = append(children, struct {
+						Key   string `json:"mkey"`
+						Value any    `json:"mvalue"`
+					}{
+						Key:   iter.Key().String(),
+						Value: iter.Value().Interface(),
+					})
+				}
+				p.Default = children
+				return p
+			default:
+
+			}
+		}
+		if len(p.Enum) > 0 {
+			p.Component = "Radio.Group"
+		}
+		return p
+	}
+}
+
+func (config *Config) GetFormily() (r Object) {
+	var fromItems = make(map[string]any)
+	r.Type = "object"
+	r.Properties = map[string]any{
+		"layout": Card{
+			Type:      "void",
+			Component: "FormLayout",
+			ComponentProps: map[string]any{
+				"labelCol":   4,
+				"wrapperCol": 20,
+			},
+			Properties: fromItems,
+		},
+	}
+	for i, v := range config.props {
+		if strings.HasPrefix(v.tag.Get("desc"), "废弃") {
+			continue
+		}
+		fromItems[v.name] = v.schema(i)
+	}
+	return
+}

+ 138 - 0
m7s.live/engine/v4@v4.14.6/config/http.go

@@ -0,0 +1,138 @@
+package config
+
+import (
+	"context"
+	"crypto/tls"
+	"net/http"
+	"time"
+
+	"github.com/logrusorgru/aurora/v4"
+	"golang.org/x/sync/errgroup"
+	"m7s.live/engine/v4/log"
+	"m7s.live/engine/v4/util"
+)
+
+var _ HTTPConfig = (*HTTP)(nil)
+
+type Middleware func(string, http.Handler) http.Handler
+type HTTP struct {
+	ListenAddr    string        `desc:"监听地址"`
+	ListenAddrTLS string        `desc:"监听地址HTTPS"`
+	CertFile      string        `desc:"HTTPS证书文件"`
+	KeyFile       string        `desc:"HTTPS密钥文件"`
+	CORS          bool          `default:"true" desc:"是否自动添加CORS头"` //是否自动添加CORS头
+	UserName      string        `desc:"基本身份认证用户名"`
+	Password      string        `desc:"基本身份认证密码"`
+	ReadTimeout   time.Duration `desc:"读取超时"`
+	WriteTimeout  time.Duration `desc:"写入超时"`
+	IdleTimeout   time.Duration `desc:"空闲超时"`
+	mux           *http.ServeMux
+	middlewares   []Middleware
+}
+type HTTPConfig interface {
+	GetHTTPConfig() *HTTP
+	Listen(ctx context.Context) error
+	Handle(string, http.Handler)
+	Handler(*http.Request) (http.Handler, string)
+	AddMiddleware(Middleware)
+}
+
+func (config *HTTP) AddMiddleware(middleware Middleware) {
+	config.middlewares = append(config.middlewares, middleware)
+}
+
+func (config *HTTP) Handle(path string, f http.Handler) {
+	if config.mux == nil {
+		config.mux = http.NewServeMux()
+	}
+	if config.CORS {
+		f = util.CORS(f)
+	}
+	if config.UserName != "" && config.Password != "" {
+		f = util.BasicAuth(config.UserName, config.Password, f)
+	}
+	for _, middleware := range config.middlewares {
+		f = middleware(path, f)
+	}
+	config.mux.Handle(path, f)
+}
+
+func (config *HTTP) GetHTTPConfig() *HTTP {
+	return config
+}
+
+func (config *HTTP) Handler(r *http.Request) (h http.Handler, pattern string) {
+	return config.mux.Handler(r)
+}
+
+// ListenAddrs Listen http and https
+func (config *HTTP) Listen(ctx context.Context) error {
+	if config.mux == nil {
+		return nil
+	}
+	var g errgroup.Group
+	if config.ListenAddrTLS != "" && (config == &Global.HTTP || config.ListenAddrTLS != Global.ListenAddrTLS) {
+		g.Go(func() error {
+			if Global.LogLang == "zh" {
+				log.Info("🌐 https 监听在 ", aurora.Blink(config.ListenAddrTLS))
+			} else {
+				log.Info("🌐 https listen at ", aurora.Blink(config.ListenAddrTLS))
+			}
+			cer, _ := tls.X509KeyPair(LocalCert, LocalKey)
+			var server = http.Server{
+				Addr:         config.ListenAddrTLS,
+				ReadTimeout:  config.ReadTimeout,
+				WriteTimeout: config.WriteTimeout,
+				IdleTimeout:  config.IdleTimeout,
+				Handler:      config.mux,
+				TLSConfig: &tls.Config{
+					Certificates: []tls.Certificate{cer},
+					CipherSuites: []uint16{
+						tls.TLS_AES_128_GCM_SHA256,
+						tls.TLS_CHACHA20_POLY1305_SHA256,
+						tls.TLS_AES_256_GCM_SHA384,
+						//tls.TLS_RSA_WITH_AES_128_CBC_SHA,
+						//tls.TLS_RSA_WITH_AES_256_CBC_SHA,
+						//tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
+						//tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
+						tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
+						tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
+						tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+						tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+						tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+						tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+						tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
+						tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
+						tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
+						tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
+						tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
+						tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
+					},
+				},
+			}
+			return server.ListenAndServeTLS(config.CertFile, config.KeyFile)
+		})
+	}
+	if config.ListenAddr != "" && (config == &Global.HTTP || config.ListenAddr != Global.ListenAddr) {
+		g.Go(func() error {
+			if Global.LogLang == "zh" {
+				log.Info("🌐 http 监听在 ", aurora.Blink(config.ListenAddr))
+			} else {
+				log.Info("🌐 http listen at ", aurora.Blink(config.ListenAddr))
+			}
+			var server = http.Server{
+				Addr:         config.ListenAddr,
+				ReadTimeout:  config.ReadTimeout,
+				WriteTimeout: config.WriteTimeout,
+				IdleTimeout:  config.IdleTimeout,
+				Handler:      config.mux,
+			}
+			return server.ListenAndServe()
+		})
+	}
+	g.Go(func() error {
+		<-ctx.Done()
+		return ctx.Err()
+	})
+	return g.Wait()
+}

+ 27 - 0
m7s.live/engine/v4@v4.14.6/config/local.monibuca.com.key

@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEogIBAAKCAQEAn2zPOFL8i4EqVwoa7fzlGXXPaTxVoUthlGEuWP5jPZCRctAe
+JNPxZT7vhj6PZsRi0jGJKD3KNosprSxN335KzZmm8FPxghRXEt/y7DvKQuuTXyst
+eaQoTbZtdX3a2cXNC4aPbDeCCYhrthHrCo8GL07a3lV9FlKpcrmn+zep3+BXHDD4
+QiG/6k4/AyZImKIi6de25riP7yIjJgENgXzQvEDRYj/Qkth+2uoMzvjb5dZvDzSF
+syZ4haPdNv66UDMFVzsIQ7ZPSILVw91vdQn7yI7sCVdlQT1CgXkbXOHvyZGhVBQh
+ikPEDBCQpvR8m45kXaterYtXDrW4AXb/VfbILQIDAQABAoIBAAm3jZfOwxLTsBt3
+A7YuvF4FZrtw0H1QxWVQWQ1WkADwH1VipvuyqVt07w99H6txW96Y41W/EmSprcQe
+165AGdoXO+wZCbbWe4oseTd523Whuy1JSe/ImCZIcLqBDcZJPpqtvG8poPToyjvi
+MrPFjOh0Q3XauxGRvz89XqY6udFp1+jzt7pmUSk9AWi/CYNGWzU32iEgZZNAxhgg
+T3f6HkitiyoyqQURuxMxUVdohjdavVQwDrRqwgLM4MkfIhT+1B52xNYhCVzxT5Eg
+5VE533fumUULxCK0t8/aneSn4rJ/5+CU0WBDex9cVtDsXCncK9oCkw/PQZO+lzzt
+LHzBytcCgYEA1tZISKL2gFb24aDaS/OcHfDkHrFRVQDHP+iWL8TMs5HHUGsM675z
+JxYNTgKH4tgg19V7G1N6SfF9wluNYd+4m681DO4kHMdzQ5+RcuLM/p2T8o1hLjf6
+CDTjMoSntyecouwbuqmibBzQw834+LR/0h7N12eLgV5MEjdKMLaktqcCgYEAvfiU
+FAIePAwSlroJcmL1AVCOYIyoVK9Vg5hEukclDtrzy0KkSKoMsyGrM/ggPvMNL/vz
+W6rJSFahoRb8XyDautZVj1oxjYrrVBOKSKLTKr8+ckfztDO6+aljxtxHdv9sRQg1
+zQ+NSozgXJSW08Y464I6CPaS5GW1DGTLFAjCeQsCgYAh06WYAkjL1mWTCy+0C8yG
+Dlrs1kCXIMM+tdGH/fW5RHfcmq5zJA6fleJMaSuaNSuesFds6wzzPZnuk1nEkmRP
+5xt0SL7Y5TKp8CMHstxSLt+PrmEh1OCCkElBuA9sUEligciv8GvJmBPq8LCGAG2r
+2PvSMdSObxmNOLVuzCNNOwKBgAo+c02454R5ai8yjPvcFjYh7+uI6jLW2ZelCF+7
+ImZwrCDT0SQR92lZcW/1+1cpqBZkUbUpunzqHwEeyjEfBmx4zlhSlsV5LkN0YkqU
+bSqq8WUcOCoJeBWqarT4f+oMz/vQ+4W5Rvc0LY0QfimhUMRyW0rMcRNb4K1wafsE
+legNAoGADESfqKfuuL2C4+lmwaPb/7K3yirOEaZECKgc5F9YFRFEJ6Wic+8U4Yvn
+89cvY8ye1RM4ZDe1GtuzlgRw11kXc65QtjEnj1j8EqESu+3EcZnq/wOzFFbeELFn
+kTwlRfbRHELksH04OAphQwa/BFTXPni+zv0tFYBkj0RyXAVumBQ=
+-----END RSA PRIVATE KEY-----

+ 66 - 0
m7s.live/engine/v4@v4.14.6/config/local.monibuca.com_bundle.pem

@@ -0,0 +1,66 @@
+-----BEGIN CERTIFICATE-----
+MIIGbzCCBNegAwIBAgIQUnjZ1U6EvGqTYJGEoD5X7zANBgkqhkiG9w0BAQwFADBZ
+MQswCQYDVQQGEwJDTjElMCMGA1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywg
+SW5jLjEjMCEGA1UEAxMaVHJ1c3RBc2lhIFJTQSBEViBUTFMgQ0EgRzIwHhcNMjMw
+MjIyMDAwMDAwWhcNMjQwMjIyMjM1OTU5WjAdMRswGQYDVQQDExJsb2NhbC5tb25p
+YnVjYS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCfbM84UvyL
+gSpXChrt/OUZdc9pPFWhS2GUYS5Y/mM9kJFy0B4k0/FlPu+GPo9mxGLSMYkoPco2
+iymtLE3ffkrNmabwU/GCFFcS3/LsO8pC65NfKy15pChNtm11fdrZxc0Lho9sN4IJ
+iGu2EesKjwYvTtreVX0WUqlyuaf7N6nf4FccMPhCIb/qTj8DJkiYoiLp17bmuI/v
+IiMmAQ2BfNC8QNFiP9CS2H7a6gzO+Nvl1m8PNIWzJniFo902/rpQMwVXOwhDtk9I
+gtXD3W91CfvIjuwJV2VBPUKBeRtc4e/JkaFUFCGKQ8QMEJCm9HybjmRdq16ti1cO
+tbgBdv9V9sgtAgMBAAGjggLtMIIC6TAfBgNVHSMEGDAWgBRfOnwREH4MZ3Fh3Iuj
+tQADZ/VXHDAdBgNVHQ4EFgQUjqZHMBwnZrrlYVLZnNFBaCgfdU0wDgYDVR0PAQH/
+BAQDAgWgMAwGA1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUF
+BwMCMEkGA1UdIARCMEAwNAYLKwYBBAGyMQECAjEwJTAjBggrBgEFBQcCARYXaHR0
+cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQIBMH0GCCsGAQUFBwEBBHEwbzBC
+BggrBgEFBQcwAoY2aHR0cDovL2NydC50cnVzdC1wcm92aWRlci5jbi9UcnVzdEFz
+aWFSU0FEVlRMU0NBRzIuY3J0MCkGCCsGAQUFBzABhh1odHRwOi8vb2NzcC50cnVz
+dC1wcm92aWRlci5jbjAdBgNVHREEFjAUghJsb2NhbC5tb25pYnVjYS5jb20wggF/
+BgorBgEEAdZ5AgQCBIIBbwSCAWsBaQB3AHb/iD8KtvuVUcJhzPWHujS0pM27Kdxo
+Qgqf5mdMWjp0AAABhnkjvQkAAAQDAEgwRgIhAMBG7q1j0b/vZwGD27eUP0+nBh73
+P7nC+CSokfpts8SoAiEA+HCrNNZ4jrgiLyi9035m1Hvf414Bn8ksSjWzxNUEiCgA
+dgDatr9rP7W2Ip+bwrtca+hwkXFsu1GEhTS9pD0wSNf7qwAAAYZ5I7zdAAAEAwBH
+MEUCIQCdIMs1nf5cheetmz4/9om8/6KDsoej8VFXHzaDt4BkAAIgLePqBifr6zUA
+lTpl17CRDG7q09kUPzElK8uTLjnugtkAdgDuzdBk1dsazsVct520zROiModGfLzs
+3sNRSFlGcR+1mwAAAYZ5I7yiAAAEAwBHMEUCIAbWcfP71joz+2wBVRU78RD0bV6V
+ugIiATVYUh+k9duJAiEA7x0JIdFuLBj3ggGH9QpAWdG03z48kZ9Cy1DjibRn4fYw
+DQYJKoZIhvcNAQEMBQADggGBAItE6rr+1vFoVA3R71+23W0ctYrBTWyxCOse8i0x
+/BEo13FjERXJkKWGSj1mwmTikO94JPcPqm83PVyZ0eIbEPu4IO/E6xFbOkTSQu7c
+o+5i7TdqtPfv6AOApt+yBb3t3NHRXk2WYLV4onvuSnorbFaj9wRS7GNr+rXCIbqJ
+HaFKpneHoV9XhKYUwdgDr2w6JkGattyFdee5o60+8EtL068Mf6Qg3OmyMosEZuBw
+R/Gs4DPVKwxj/qt7cJPZoUDV7L6LzLCkgxre8nvvLbOBkC34i4KQGF4CkTjRPWw+
+OpRKOzWIw9fQ2+m+z7QwWi+fZQ31EAT6KGnHqPLePJNj09qqUSff3e/y9szzKGHc
+TpKVSCgEuuCiBNze7PG9D8QgBMYHkOuGkMeP1EO0pZ3mxd3obUn+bPz0tsqvMR2t
+gBx56pMeFnVNQR26VqT2YE+Xw7j6AQUwLa6SCMEsfPeotnhl5tiIKxjqWjWf1lLV
+/YWY7m7yb5ctZnq9FJU926ZLLA==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFBzCCA++gAwIBAgIRALIM7VUuMaC/NDp1KHQ76aswDQYJKoZIhvcNAQELBQAw
+ezELMAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
+A1UEBwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNV
+BAMMGEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczAeFw0yMjAxMTAwMDAwMDBaFw0y
+ODEyMzEyMzU5NTlaMFkxCzAJBgNVBAYTAkNOMSUwIwYDVQQKExxUcnVzdEFzaWEg
+VGVjaG5vbG9naWVzLCBJbmMuMSMwIQYDVQQDExpUcnVzdEFzaWEgUlNBIERWIFRM
+UyBDQSBHMjCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBAKjGDe0GSaBs
+Yl/VhMaTM6GhfR1TAt4mrhN8zfAMwEfLZth+N2ie5ULbW8YvSGzhqkDhGgSBlafm
+qq05oeESrIJQyz24j7icGeGyIZ/jIChOOvjt4M8EVi3O0Se7E6RAgVYcX+QWVp5c
+Sy+l7XrrtL/pDDL9Bngnq/DVfjCzm5ZYUb1PpyvYTP7trsV+yYOCNmmwQvB4yVjf
+IIpHC1OcsPBntMUGeH1Eja4D+qJYhGOxX9kpa+2wTCW06L8T6OhkpJWYn5JYiht5
+8exjAR7b8Zi3DeG9oZO5o6Qvhl3f8uGU8lK1j9jCUN/18mI/5vZJ76i+hsgdlfZB
+Rh5lmAQjD80M9TY+oD4MYUqB5XrigPfFAUwXFGehhlwCVw7y6+5kpbq/NpvM5Ba8
+SeQYUUuMA8RXpTtGlrrTPqJryfa55hTuX/ThhX4gcCVkbyujo0CYr+Uuc14IOyNY
+1fD0/qORbllbgV41wiy/2ZUWZQUodqHWkjT1CwIMbQOY5jmrSYGBwwIDAQABo4IB
+JjCCASIwHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQwHQYDVR0OBBYE
+FF86fBEQfgxncWHci6O1AANn9VccMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8E
+CDAGAQH/AgEAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAiBgNVHSAE
+GzAZMA0GCysGAQQBsjEBAgIxMAgGBmeBDAECATBDBgNVHR8EPDA6MDigNqA0hjJo
+dHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNy
+bDA0BggrBgEFBQcBAQQoMCYwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9k
+b2NhLmNvbTANBgkqhkiG9w0BAQsFAAOCAQEAHMUom5cxIje2IiFU7mOCsBr2F6CY
+eU5cyfQ/Aep9kAXYUDuWsaT85721JxeXFYkf4D/cgNd9+hxT8ZeDOJrn+ysqR7NO
+2K9AdqTdIY2uZPKmvgHOkvH2gQD6jc05eSPOwdY/10IPvmpgUKaGOa/tyygL8Og4
+3tYyoHipMMnS4OiYKakDJny0XVuchIP7ZMKiP07Q3FIuSS4omzR77kmc75/6Q9dP
+v4wa90UCOn1j6r7WhMmX3eT3Gsdj3WMe9bYD0AFuqa6MDyjIeXq08mVGraXiw73s
+Zale8OMckn/BU3O/3aFNLHLfET2H2hT6Wb3nwxjpLIfXmSVcVd8A58XH0g==
+-----END CERTIFICATE-----

+ 68 - 0
m7s.live/engine/v4@v4.14.6/config/quic.go

@@ -0,0 +1,68 @@
+package config
+
+import (
+	"context"
+	"crypto/tls"
+
+	"github.com/quic-go/quic-go"
+	"m7s.live/engine/v4/log"
+)
+
+type QuicConfig interface {
+	ListenQuic(context.Context, QuicPlugin) error
+}
+
+type Quic struct {
+	ListenAddr string `desc:"监听地址,格式为ip:port,ip 可省略默认监听所有网卡"`
+	CertFile   string `desc:"证书文件"`
+	KeyFile    string `desc:"私钥文件"`
+}
+
+func (q *Quic) ListenQuic(ctx context.Context, plugin QuicPlugin) error {
+	listener, err := quic.ListenAddr(q.ListenAddr, q.generateTLSConfig(), &quic.Config{
+		EnableDatagrams: true,
+	})
+	if err != nil {
+		return err
+	}
+	log.Infof("quic listen at %s", q.ListenAddr)
+	for {
+		conn, err := listener.Accept(ctx)
+		if err != nil {
+			return err
+		}
+		go plugin.ServeQuic(conn)
+	}
+}
+
+func (q *Quic) generateTLSConfig() *tls.Config {
+	// key, err := rsa.GenerateKey(rand.Reader, 1024)
+	// if err != nil {
+	// 	panic(err)
+	// }
+	// template := x509.Certificate{SerialNumber: big.NewInt(1)}
+	// certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
+	// if err != nil {
+	// 	panic(err)
+	// }
+	// keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
+	// certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
+	// tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
+
+	keyPair, err := tls.X509KeyPair(LocalCert, LocalKey)
+	if q.CertFile != "" || q.KeyFile != "" {
+		keyPair, err = tls.LoadX509KeyPair(q.CertFile, q.KeyFile)
+	}
+	if err != nil {
+		if Global.LogLang == "zh" {
+			log.Fatalf("加载证书失败: %v", err)
+		} else {
+			log.Fatalf("LoadX509KeyPair error: %v", err)
+		}
+		panic(err)
+	}
+	return &tls.Config{
+		Certificates: []tls.Certificate{keyPair},
+		NextProtos:   []string{"monibuca"},
+	}
+}

+ 49 - 0
m7s.live/engine/v4@v4.14.6/config/regexp.go

@@ -0,0 +1,49 @@
+package config
+
+import (
+	"regexp"
+
+	"gopkg.in/yaml.v3"
+)
+
+type Regexp struct {
+	*regexp.Regexp
+}
+
+func (r *Regexp) Valid() bool {
+	return r.Regexp != nil
+}
+
+func (r Regexp) String() string {
+	if !r.Valid() {
+		return ""
+	}
+	return r.Regexp.String()
+}
+
+func (r *Regexp) UnmarshalYAML(node *yaml.Node) error {
+	r.Regexp = regexp.MustCompile(node.Value)
+	return nil
+}
+
+func (r Regexp) MarshalYAML() (interface{}, error) {
+	return r.String(), nil
+}
+
+func (r Regexp) MarshalJSON() ([]byte, error) {
+	return []byte(`"` + r.String() + `"`), nil
+}
+
+func (r *Regexp) UnmarshalJSON(b []byte) error {
+	if len(b) == 0 {
+		return nil
+	}
+	if b[0] == '"' {
+		b = b[1:]
+	}
+	if b[len(b)-1] == '"' {
+		b = b[:len(b)-1]
+	}
+	r.Regexp = regexp.MustCompile(string(b))
+	return nil
+}

+ 165 - 0
m7s.live/engine/v4@v4.14.6/config/remote.go

@@ -0,0 +1,165 @@
+package config
+
+import (
+	"bufio"
+	"context"
+	"crypto/tls"
+	"encoding/json"
+	"io"
+	"net"
+	"net/http"
+	"strings"
+	"time"
+
+	"github.com/quic-go/quic-go"
+	"m7s.live/engine/v4/log"
+)
+
+type myResponseWriter2 struct {
+	quic.Stream
+	myResponseWriter
+}
+
+type myResponseWriter3 struct {
+	handshake bool
+	myResponseWriter2
+	quic.Connection
+}
+
+func (w *myResponseWriter3) Write(b []byte) (int, error) {
+	if !w.handshake {
+		w.handshake = true
+		return len(b), nil
+	}
+	println(string(b))
+	return w.Stream.Write(b)
+}
+
+func (w *myResponseWriter3) Hijack() (net.Conn, *bufio.ReadWriter, error) {
+	return net.Conn(w), bufio.NewReadWriter(bufio.NewReader(w), bufio.NewWriter(w)), nil
+}
+
+func (cfg *Engine) WtRemote(ctx context.Context) {
+	retryDelay := [...]int{2, 3, 5, 8, 13}
+	for i := 0; ctx.Err() == nil; i++ {
+		connected, err := cfg.Remote(ctx)
+		if err == nil {
+			//不需要重试了,服务器返回了错误
+			return
+		}
+		if Global.LogLang == "zh" {
+			log.Error("连接到控制台服务器", cfg.Server, "失败", err)
+		} else {
+			log.Error("connect to console server ", cfg.Server, " ", err)
+		}
+		if connected {
+			i = 0
+		} else if i >= 5 {
+			i = 4
+		}
+		time.Sleep(time.Second * time.Duration(retryDelay[i]))
+	}
+}
+
+func (cfg *Engine) Remote(ctx context.Context) (wasConnected bool, err error) {
+	tlsConf := &tls.Config{
+		InsecureSkipVerify: true,
+		NextProtos:         []string{"monibuca"},
+	}
+
+	conn, err := quic.DialAddr(ctx, cfg.Server, tlsConf, &quic.Config{
+		KeepAlivePeriod: time.Second * 10,
+		EnableDatagrams: true,
+	})
+	wasConnected = err == nil
+	if stream := quic.Stream(nil); err == nil {
+		if stream, err = conn.OpenStreamSync(ctx); err == nil {
+			_, err = stream.Write(append([]byte{1}, (cfg.Secret + "\n")...))
+			if msg := []byte(nil); err == nil {
+				if msg, err = bufio.NewReader(stream).ReadSlice(0); err == nil {
+					var rMessage map[string]any
+					if err = json.Unmarshal(msg[:len(msg)-1], &rMessage); err == nil {
+						if rMessage["code"].(float64) != 0 {
+							if Global.LogLang == "zh" {
+								log.Error("控制台服务器", cfg.Server, "返回错误", rMessage["msg"])
+							} else {
+								log.Error("response from console server ", cfg.Server, " ", rMessage["msg"])
+							}
+							return false, nil
+						} else {
+							cfg.reportStream = stream
+							if Global.LogLang == "zh" {
+								log.Info("连接到控制台服务器", cfg.Server, "成功", rMessage)
+							} else {
+								log.Info("response from console server ", cfg.Server, " success ", rMessage)
+							}
+							if v, ok := rMessage["enableReport"]; ok {
+								cfg.enableReport = v.(bool)
+							}
+							if v, ok := rMessage["instanceId"]; ok {
+								cfg.instanceId = v.(string)
+							}
+						}
+					}
+				}
+			}
+		}
+	}
+
+	for err == nil {
+		var s quic.Stream
+		if s, err = conn.AcceptStream(ctx); err == nil {
+			go cfg.ReceiveRequest(s, conn)
+		}
+	}
+	return wasConnected, err
+}
+
+func (cfg *Engine) ReceiveRequest(s quic.Stream, conn quic.Connection) error {
+	defer s.Close()
+	wr := &myResponseWriter2{Stream: s}
+	reader := bufio.NewReader(s)
+	var req *http.Request
+	url, _, err := reader.ReadLine()
+	if err == nil {
+		ctx, cancel := context.WithCancel(s.Context())
+		defer cancel()
+		req, err = http.NewRequestWithContext(ctx, "GET", string(url), reader)
+		for err == nil {
+			var h []byte
+			if h, _, err = reader.ReadLine(); len(h) > 0 {
+				if b, a, f := strings.Cut(string(h), ": "); f {
+					req.Header.Set(b, a)
+				}
+			} else {
+				break
+			}
+		}
+
+		if err == nil {
+			h, _ := cfg.mux.Handler(req)
+			if req.Header.Get("Accept") == "text/event-stream" {
+				go h.ServeHTTP(wr, req)
+			} else if req.Header.Get("Upgrade") == "websocket" {
+				var writer myResponseWriter3
+				writer.Stream = s
+				writer.Connection = conn
+				req.Host = req.Header.Get("Host")
+				if req.Host == "" {
+					req.Host = req.URL.Host
+				}
+				if req.Host == "" {
+					req.Host = "m7s.live"
+				}
+				h.ServeHTTP(&writer, req) //建立websocket连接,握手
+			} else {
+				h.ServeHTTP(wr, req)
+			}
+		}
+		io.ReadAll(s)
+	}
+	if err != nil {
+		log.Error("read console server error:", err)
+	}
+	return err
+}

+ 118 - 0
m7s.live/engine/v4@v4.14.6/config/tcp.go

@@ -0,0 +1,118 @@
+package config
+
+import (
+	"context"
+	"crypto/tls"
+	_ "embed"
+	"net"
+	"runtime"
+	"time"
+
+	"m7s.live/engine/v4/log"
+)
+
+//go:embed local.monibuca.com_bundle.pem
+var LocalCert []byte
+
+//go:embed local.monibuca.com.key
+var LocalKey []byte
+
+var _ TCPConfig = (*TCP)(nil)
+
+type TCPConfig interface {
+	ListenTCP(context.Context, TCPPlugin) error
+}
+
+type TCP struct {
+	ListenAddr    string `desc:"监听地址,格式为ip:port,ip 可省略默认监听所有网卡"`
+	ListenAddrTLS string `desc:"监听地址,格式为ip:port,ip 可省略默认监听所有网卡"`
+	CertFile      string `desc:"证书文件"`
+	KeyFile       string `desc:"私钥文件"`
+	ListenNum     int    `desc:"同时并行监听数量,0为CPU核心数量"` //同时并行监听数量,0为CPU核心数量
+	NoDelay       bool   `desc:"是否禁用Nagle算法"`        //是否禁用Nagle算法
+}
+
+func (tcp *TCP) listen(l net.Listener, handler func(net.Conn)) {
+	var tempDelay time.Duration
+	for {
+		conn, err := l.Accept()
+		if err != nil {
+			if ne, ok := err.(net.Error); ok && ne.Temporary() {
+				if tempDelay == 0 {
+					tempDelay = 5 * time.Millisecond
+				} else {
+					tempDelay *= 2
+				}
+				if max := 1 * time.Second; tempDelay > max {
+					tempDelay = max
+				}
+				log.Warnf("%s: Accept error: %v; retrying in %v", tcp.ListenAddr, err, tempDelay)
+				time.Sleep(tempDelay)
+				continue
+			}
+			return
+		}
+		var tcpConn *net.TCPConn
+		switch v := conn.(type) {
+		case *net.TCPConn:
+			tcpConn = v
+		case *tls.Conn:
+			tcpConn = v.NetConn().(*net.TCPConn)
+		}
+		if !tcp.NoDelay {
+			tcpConn.SetNoDelay(false)
+		}
+		tempDelay = 0
+		go handler(conn)
+	}
+}
+func (tcp *TCP) ListenTCP(ctx context.Context, plugin TCPPlugin) error {
+	l, err := net.Listen("tcp", tcp.ListenAddr)
+	if err != nil {
+		if Global.LogLang == "zh" {
+			log.Fatalf("%s: 监听失败: %v", tcp.ListenAddr, err)
+		} else {
+			log.Fatalf("%s: Listen error: %v", tcp.ListenAddr, err)
+		}
+		return err
+	}
+	count := tcp.ListenNum
+	if count == 0 {
+		count = runtime.NumCPU()
+	}
+	log.Infof("tcp listen %d at %s", count, tcp.ListenAddr)
+	for i := 0; i < count; i++ {
+		go tcp.listen(l, plugin.ServeTCP)
+	}
+	if tcp.ListenAddrTLS != "" {
+		keyPair, _ := tls.X509KeyPair(LocalCert, LocalKey)
+		if tcp.CertFile != "" || tcp.KeyFile != "" {
+			keyPair, err = tls.LoadX509KeyPair(tcp.CertFile, tcp.KeyFile)
+		}
+		if err != nil {
+			if Global.LogLang == "zh" {
+				log.Fatalf("加载证书失败: %v", err)
+			} else {
+				log.Fatalf("LoadX509KeyPair error: %v", err)
+			}
+			return err
+		}
+		l, err = tls.Listen("tcp", tcp.ListenAddrTLS, &tls.Config{
+			Certificates: []tls.Certificate{keyPair},
+		})
+		if err != nil {
+			if Global.LogLang == "zh" {
+				log.Fatalf("%s: 监听失败: %v", tcp.ListenAddrTLS, err)
+			} else {
+				log.Fatalf("%s: Listen error: %v", tcp.ListenAddrTLS, err)
+			}
+			return err
+		}
+		log.Infof("tls tcp listen %d at %s", count, tcp.ListenAddrTLS)
+		for i := 0; i < count; i++ {
+			go tcp.listen(l, plugin.ServeTCP)
+		}
+	}
+	<-ctx.Done()
+	return l.Close()
+}

+ 331 - 0
m7s.live/engine/v4@v4.14.6/config/types.go

@@ -0,0 +1,331 @@
+package config
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"regexp"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/quic-go/quic-go"
+	"golang.org/x/net/websocket"
+	"m7s.live/engine/v4/log"
+	"m7s.live/engine/v4/util"
+)
+
+type PublishConfig interface {
+	GetPublishConfig() Publish
+}
+
+type SubscribeConfig interface {
+	GetSubscribeConfig() *Subscribe
+}
+type PullConfig interface {
+	GetPullConfig() *Pull
+}
+
+type PushConfig interface {
+	GetPushConfig() *Push
+}
+
+type Publish struct {
+	PubAudio          bool          `default:"true" desc:"是否发布音频"`
+	PubVideo          bool          `default:"true" desc:"是否发布视频"`
+	InsertSEI         bool          `desc:"是否启用SEI插入"`                        // 是否启用SEI插入
+	KickExist         bool          `desc:"是否踢掉已经存在的发布者"`                     // 是否踢掉已经存在的发布者
+	PublishTimeout    time.Duration `default:"10s" desc:"发布无数据超时"`            // 发布无数据超时
+	WaitCloseTimeout  time.Duration `desc:"延迟自动关闭(等待重连)"`                     // 延迟自动关闭(等待重连)
+	DelayCloseTimeout time.Duration `desc:"延迟自动关闭(无订阅时)"`                     // 延迟自动关闭(无订阅时)
+	IdleTimeout       time.Duration `desc:"空闲(无订阅)超时"`                        // 空闲(无订阅)超时
+	PauseTimeout      time.Duration `default:"30s" desc:"暂停超时时间"`             // 暂停超时
+	BufferTime        time.Duration `desc:"缓冲长度(单位:秒),0代表取最近关键帧"`             // 缓冲长度(单位:秒),0代表取最近关键帧
+	SpeedLimit        time.Duration `default:"500ms" desc:"速度限制最大等待时间,0则不等待"` //速度限制最大等待时间
+	Key               string        `desc:"发布鉴权key"`                          // 发布鉴权key
+	SecretArgName     string        `default:"secret" desc:"发布鉴权参数名"`         // 发布鉴权参数名
+	ExpireArgName     string        `default:"expire" desc:"发布鉴权失效时间参数名"`     // 发布鉴权失效时间参数名
+	RingSize          string        `default:"256-1024" desc:"缓冲范围"`          // 初始缓冲区大小
+}
+
+func (c Publish) GetPublishConfig() Publish {
+	return c
+}
+
+type Subscribe struct {
+	SubAudio        bool          `default:"true" desc:"是否订阅音频"`
+	SubVideo        bool          `default:"true" desc:"是否订阅视频"`
+	SubVideoArgName string        `default:"vts" desc:"定订阅的视频轨道参数名"`                     // 指定订阅的视频轨道参数名
+	SubAudioArgName string        `default:"ats" desc:"指定订阅的音频轨道参数名"`                    // 指定订阅的音频轨道参数名
+	SubDataArgName  string        `default:"dts" desc:"指定订阅的数据轨道参数名"`                    // 指定订阅的数据轨道参数名
+	SubModeArgName  string        `desc:"指定订阅的模式参数名"`                                    // 指定订阅的模式参数名
+	SubAudioTracks  []string      `desc:"指定订阅的音频轨道"`                                     // 指定订阅的音频轨道
+	SubVideoTracks  []string      `desc:"指定订阅的视频轨道"`                                     // 指定订阅的视频轨道
+	SubDataTracks   []string      `desc:"指定订阅的数据轨道"`                                     // 指定订阅的数据轨道
+	SubMode         int           `desc:"订阅模式" enum:"0:实时模式,1:首屏后不进行追赶,2:从缓冲最大的关键帧开始播放"` // 0,实时模式:追赶发布者进度,在播放首屏后等待发布者的下一个关键帧,然后跳到该帧。1、首屏后不进行追赶。2、从缓冲最大的关键帧开始播放,也不追赶,需要发布者配置缓存长度
+	SyncMode        int           `desc:"同步模式" enum:"0:采用时间戳同步,1:采用写入时间同步"`              // 0,采用时间戳同步,1,采用写入时间同步
+	IFrameOnly      bool          `desc:"只要关键帧"`                                         // 只要关键帧
+	WaitTimeout     time.Duration `default:"10s" desc:"等待流超时时间"`                         // 等待流超时
+	WriteBufferSize int           `desc:"写缓冲大小"`                                         // 写缓冲大小
+	Key             string        `desc:"订阅鉴权key"`                                       // 订阅鉴权key
+	SecretArgName   string        `default:"secret" desc:"订阅鉴权参数名"`                      // 订阅鉴权参数名
+	ExpireArgName   string        `default:"expire" desc:"订阅鉴权失效时间参数名"`                  // 订阅鉴权失效时间参数名
+	Internal        bool          `default:"false" desc:"是否内部订阅"`                        // 是否内部订阅
+}
+
+func (c *Subscribe) GetSubscribeConfig() *Subscribe {
+	return c
+}
+
+type Pull struct {
+	RePull            int               `desc:"断开后自动重试次数,0:不重试,-1:无限重试"` // 断开后自动重拉,0 表示不自动重拉,-1 表示无限重拉,高于0 的数代表最大重拉次数
+	EnableRegexp      bool              `desc:"是否启用正则表达式"`               // 是否启用正则表达式
+	PullOnStart       map[string]string `desc:"启动时拉流的列表"`                // 启动时拉流的列表
+	PullOnSub         map[string]string `desc:"订阅时自动拉流的列表"`              // 订阅时自动拉流的列表
+	Proxy             string            `desc:"代理地址"`                    // 代理地址
+	PullOnSubLocker   sync.RWMutex      `yaml:"-" json:"-"`
+	PullOnStartLocker sync.RWMutex      `yaml:"-" json:"-"`
+}
+
+func (p *Pull) GetPullConfig() *Pull {
+	return p
+}
+
+func (p *Pull) CheckPullOnStart(streamPath string) string {
+	p.PullOnStartLocker.RLock()
+	defer p.PullOnStartLocker.RUnlock()
+	if p.PullOnStart == nil {
+		return ""
+	}
+	url, ok := p.PullOnStart[streamPath]
+	if !ok && p.EnableRegexp {
+		for k, url := range p.PullOnStart {
+			if r, err := regexp.Compile(k); err != nil {
+				if group := r.FindStringSubmatch(streamPath); group != nil {
+					for i, value := range group {
+						url = strings.Replace(url, fmt.Sprintf("$%d", i), value, -1)
+					}
+					return url
+				}
+			}
+			return ""
+		}
+	}
+	return url
+}
+
+func (p *Pull) CheckPullOnSub(streamPath string) string {
+	p.PullOnSubLocker.RLock()
+	defer p.PullOnSubLocker.RUnlock()
+	if p.PullOnSub == nil {
+		return ""
+	}
+	url, ok := p.PullOnSub[streamPath]
+	if !ok && p.EnableRegexp {
+		for k, url := range p.PullOnSub {
+			if r, err := regexp.Compile(k); err == nil {
+				if group := r.FindStringSubmatch(streamPath); group != nil {
+					for i, value := range group {
+						url = strings.Replace(url, fmt.Sprintf("$%d", i), value, -1)
+					}
+					return url
+				}
+			}
+			return ""
+		}
+	}
+	return url
+}
+
+// func (p *Pull) AddPullOnStart(streamPath string, url string) {
+// 	p.PullOnStartLocker.Lock()
+// 	defer p.PullOnStartLocker.Unlock()
+// 	if p.PullOnStart == nil {
+// 		p.PullOnStart = make(map[string]string)
+// 	}
+// 	p.PullOnStart[streamPath] = url
+// }
+
+// func (p *Pull) AddPullOnSub(streamPath string, url string) {
+// 	p.PullOnSubLocker.Lock()
+// 	defer p.PullOnSubLocker.Unlock()
+// 	if p.PullOnSub == nil {
+// 		p.PullOnSub = make(map[string]string)
+// 	}
+// 	p.PullOnSub[streamPath] = url
+// }
+
+type Push struct {
+	EnableRegexp bool              `desc:"是否启用正则表达式"`               // 是否启用正则表达式
+	RePush       int               `desc:"断开后自动重试次数,0:不重试,-1:无限重试"` // 断开后自动重推,0 表示不自动重推,-1 表示无限重推,高于0 的数代表最大重推次数
+	PushList     map[string]string `desc:"自动推流列表"`                  // 自动推流列表
+	Proxy        string            `desc:"代理地址"`                    // 代理地址
+}
+
+func (p *Push) GetPushConfig() *Push {
+	return p
+}
+
+func (p *Push) AddPush(url string, streamPath string) {
+	if p.PushList == nil {
+		p.PushList = make(map[string]string)
+	}
+	p.PushList[streamPath] = url
+}
+
+func (p *Push) CheckPush(streamPath string) string {
+	url, ok := p.PushList[streamPath]
+	if !ok && p.EnableRegexp {
+		for k, url := range p.PushList {
+			if r, err := regexp.Compile(k); err == nil {
+				if group := r.FindStringSubmatch(streamPath); group != nil {
+					for i, value := range group {
+						url = strings.Replace(url, fmt.Sprintf("$%d", i), value, -1)
+					}
+					return url
+				}
+			}
+			return ""
+		}
+	}
+	return url
+}
+
+type Console struct {
+	Server        string `default:"console.monibuca.com:44944" desc:"远程控制台地址"` //远程控制台地址
+	Secret        string `desc:"远程控制台密钥"`                                      //远程控制台密钥
+	PublicAddr    string `desc:"远程控制台公网地址"`                                    //公网地址,提供远程控制台访问的地址,不配置的话使用自动识别的地址
+	PublicAddrTLS string `desc:"远程控制台公网TLS地址"`
+}
+
+type Engine struct {
+	Publish
+	Subscribe
+	HTTP
+	Console
+	EnableAVCC          bool          `default:"true" desc:"启用AVCC格式,rtmp、http-flv协议使用"`                 //启用AVCC格式,rtmp、http-flv协议使用
+	EnableRTP           bool          `default:"true" desc:"启用RTP格式,rtsp、webrtc等协议使用"`                   //启用RTP格式,rtsp、webrtc等协议使用
+	EnableSubEvent      bool          `default:"true" desc:"启用订阅事件,禁用可以提高性能"`                            //启用订阅事件,禁用可以提高性能
+	EnableAuth          bool          `default:"true" desc:"启用鉴权"`                                       //启用鉴权
+	LogLang             string        `default:"zh" desc:"日志语言" enum:"zh:中文,en:英文"`                      //日志语言
+	LogLevel            string        `default:"info" enum:"trace:跟踪,debug:调试,info:信息,warn:警告,error:错误"` //日志级别
+	EventBusSize        int           `default:"10" desc:"事件总线大小"`                                       //事件总线大小
+	PulseInterval       time.Duration `default:"5s" desc:"心跳事件间隔"`                                       //心跳事件间隔
+	DisableAll          bool          `default:"false" desc:"禁用所有插件"`                                    //禁用所有插件
+	RTPReorderBufferLen int           `default:"50" desc:"RTP重排序缓冲区长度"`                                  //RTP重排序缓冲区长度
+	PoolSize            int           `desc:"内存池大小"`                                                     //内存池大小
+	enableReport        bool          `default:"false"`                                                  //启用报告,用于统计和监控
+	reportStream        quic.Stream   // console server connection
+	instanceId          string        // instance id 来自console
+}
+
+func (cfg *Engine) GetEnableReport() bool {
+	return cfg.enableReport
+}
+
+func (cfg *Engine) GetInstanceId() string {
+	return cfg.instanceId
+}
+
+var Global *Engine
+
+func (cfg *Engine) InitDefaultHttp() {
+	Global = cfg
+	cfg.HTTP.mux = http.NewServeMux()
+	cfg.HTTP.ListenAddrTLS = ":8443"
+	cfg.HTTP.ListenAddr = ":8080"
+}
+
+type myResponseWriter struct {
+}
+
+func (*myResponseWriter) Header() http.Header {
+	return make(http.Header)
+}
+func (*myResponseWriter) WriteHeader(statusCode int) {
+}
+func (w *myResponseWriter) Flush() {
+}
+
+type myWsWriter struct {
+	myResponseWriter
+	*websocket.Conn
+}
+
+func (w *myWsWriter) Write(b []byte) (int, error) {
+	return len(b), websocket.Message.Send(w.Conn, b)
+}
+func (cfg *Engine) WsRemote() {
+	for {
+		conn, err := websocket.Dial(cfg.Server, "", "https://console.monibuca.com")
+		wr := &myWsWriter{Conn: conn}
+		if err != nil {
+			log.Error("connect to console server ", cfg.Server, " ", err)
+			time.Sleep(time.Second * 5)
+			continue
+		}
+		if err = websocket.Message.Send(conn, cfg.Secret); err != nil {
+			time.Sleep(time.Second * 5)
+			continue
+		}
+		var rMessage map[string]interface{}
+		if err := websocket.JSON.Receive(conn, &rMessage); err == nil {
+			if rMessage["code"].(float64) != 0 {
+				log.Error("connect to console server ", cfg.Server, " ", rMessage["msg"])
+				return
+			} else {
+				log.Info("connect to console server ", cfg.Server, " success")
+			}
+		}
+		for {
+			var msg string
+			err := websocket.Message.Receive(conn, &msg)
+			if err != nil {
+				log.Error("read console server error:", err)
+				break
+			} else {
+				b, a, f := strings.Cut(msg, "\n")
+				if f {
+					if len(a) > 0 {
+						req, err := http.NewRequest("POST", b, strings.NewReader(a))
+						if err != nil {
+							log.Error("read console server error:", err)
+							break
+						}
+						h, _ := cfg.mux.Handler(req)
+						h.ServeHTTP(wr, req)
+					} else {
+						req, err := http.NewRequest("GET", b, nil)
+						if err != nil {
+							log.Error("read console server error:", err)
+							break
+						}
+						h, _ := cfg.mux.Handler(req)
+						h.ServeHTTP(wr, req)
+					}
+				} else {
+
+				}
+			}
+		}
+	}
+}
+
+func (cfg *Engine) OnEvent(event any) {
+	switch v := event.(type) {
+	case []byte:
+		if cfg.reportStream != nil {
+			cfg.reportStream.Write(v)
+			cfg.reportStream.Write([]byte{0})
+		}
+	case context.Context:
+		util.RTPReorderBufferLen = uint16(cfg.RTPReorderBufferLen)
+		if cfg.Secret != "" && cfg.Server != "" {
+			if strings.HasPrefix(cfg.Console.Server, "wss") {
+				go cfg.WsRemote()
+			} else {
+				go cfg.WtRemote(v)
+			}
+		}
+	}
+}

BIN
m7s.live/engine/v4@v4.14.6/demo.gif


+ 121 - 0
m7s.live/engine/v4@v4.14.6/events.go

@@ -0,0 +1,121 @@
+package engine
+
+import (
+	"reflect"
+	"time"
+
+	"m7s.live/engine/v4/common"
+)
+
+type Event[T any] struct {
+	Time   time.Time
+	Target T `json:"-" yaml:"-"`
+}
+
+func CreateEvent[T any](target T) (event Event[T]) {
+	event.Time = time.Now()
+	event.Target = target
+	return
+}
+
+// PulseEvent 心跳事件
+type PulseEvent struct {
+	Event[struct{}]
+}
+
+type StreamEvent struct {
+	Event[*Stream]
+}
+
+// StateEvent 状态机事件
+type StateEvent struct {
+	StreamEvent
+	Action StreamAction
+	From   StreamState
+}
+
+// ErrorEvent 错误事件
+type ErrorEvent struct {
+	Event[any]
+	Error error
+}
+
+func (se StateEvent) Next() (next StreamState, ok bool) {
+	next, ok = StreamFSM[se.From][se.Action]
+	return
+}
+
+type SEwaitPublish struct {
+	StateEvent
+	Publisher IPublisher
+}
+
+type SEpublish struct {
+	StateEvent
+}
+
+type SEtrackAvaliable struct {
+	StateEvent
+}
+
+type SErepublish struct {
+	StateEvent
+}
+
+type SEwaitClose struct {
+	StateEvent
+}
+type SEclose struct {
+	StateEvent
+}
+type SEcreate struct {
+	StreamEvent
+}
+
+type SEKick struct {
+	Event[struct{}]
+}
+
+type UnsubscribeEvent struct {
+	Event[ISubscriber]
+}
+
+type AddTrackEvent struct {
+	Event[common.Track]
+}
+
+// InvitePublishEvent 邀请推流事件(按需拉流)
+type InvitePublish struct {
+	Event[string]
+}
+
+func TryInvitePublish(streamPath string) {
+	s := Streams.Get(streamPath)
+	if s == nil || s.Publisher == nil {
+		EventBus <- InvitePublish{Event: CreateEvent(streamPath)}
+	}
+}
+
+// InviteTrackEvent 邀请推送指定 Track 事件(转码需要)
+type InviteTrackEvent struct {
+	Event[string]
+	ISubscriber
+}
+
+func InviteTrack(name string, suber ISubscriber) {
+	EventBus <- InviteTrackEvent{Event: CreateEvent(name), ISubscriber: suber}
+}
+
+var handlers = make(map[reflect.Type][]any)
+
+func ListenEvent[T any](handler func(event T)) {
+	t := reflect.TypeOf(handler).In(0)
+	handlers[t] = append(handlers[t], handler)
+}
+
+func EmitEvent[T any](event T) {
+	t := reflect.TypeOf(event)
+	for _, handler := range handlers[t] {
+		handler.(func(event T))(event)
+	}
+}

+ 47 - 0
m7s.live/engine/v4@v4.14.6/go.mod

@@ -0,0 +1,47 @@
+module m7s.live/engine/v4
+
+go 1.20
+
+require (
+	github.com/bluenviron/gortsplib/v4 v4.6.2
+	github.com/bluenviron/mediacommon v1.5.1
+	github.com/cnotch/ipchub v1.1.0
+	github.com/google/uuid v1.4.0
+	github.com/logrusorgru/aurora/v4 v4.0.0
+	github.com/mcuadros/go-defaults v1.2.0
+	github.com/pion/rtp v1.8.3
+	github.com/pion/webrtc/v3 v3.2.20
+	github.com/q191201771/naza v0.30.48
+	github.com/quic-go/quic-go v0.38.1
+	github.com/shirou/gopsutil/v3 v3.23.8
+	go.uber.org/zap v1.26.0
+	golang.org/x/net v0.19.0
+	golang.org/x/sync v0.3.0
+	gopkg.in/yaml.v3 v3.0.1
+)
+
+require (
+	github.com/denisbrodbeck/machineid v1.0.1
+	github.com/go-ole/go-ole v1.2.6 // indirect
+	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
+	github.com/golang/mock v1.6.0 // indirect
+	github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
+	github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a // indirect
+	github.com/onsi/ginkgo/v2 v2.9.5 // indirect
+	github.com/pion/randutil v0.1.0 // indirect
+	github.com/pkg/errors v0.9.1
+	github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
+	github.com/quic-go/qtls-go1-20 v0.3.3 // indirect
+	github.com/shoenig/go-m1cpu v0.1.6 // indirect
+	github.com/tklauser/go-sysconf v0.3.12 // indirect
+	github.com/tklauser/numcpus v0.6.1 // indirect
+	github.com/yapingcat/gomedia v0.0.0-20230905155010-55b9713fcec1
+	github.com/yusufpapurcu/wmi v1.2.3 // indirect
+	go.uber.org/multierr v1.11.0 // indirect
+	golang.org/x/crypto v0.16.0 // indirect
+	golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
+	golang.org/x/mod v0.12.0 // indirect
+	golang.org/x/sys v0.15.0 // indirect
+	golang.org/x/tools v0.13.0 // indirect
+	google.golang.org/protobuf v1.30.0 // indirect
+)

+ 294 - 0
m7s.live/engine/v4@v4.14.6/go.sum

@@ -0,0 +1,294 @@
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/bluenviron/gortsplib/v4 v4.6.2 h1:CGIsxpnUFvSlIxnSFS0oFSSfwsHMmBCmYcrGAtIcwXc=
+github.com/bluenviron/gortsplib/v4 v4.6.2/go.mod h1:dN1YjyPNMfy/NwC17Ga6MiIMiUoQfg5GL7LGsVHa0Jo=
+github.com/bluenviron/mediacommon v1.5.1 h1:yYVF+ebqZOJh8yH+EeuPcAtTmWR66BqbJGmStxkScoI=
+github.com/bluenviron/mediacommon v1.5.1/go.mod h1:Ij/kE1LEucSjryNBVTyPL/gBI0d6/Css3f5PyrM957w=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/cnotch/apirouter v0.0.0-20200731232942-89e243a791f3/go.mod h1:5deJPLON/x/s2dLOQfuKS0lenhOIT4xX0pvtN/OEIuY=
+github.com/cnotch/ipchub v1.1.0 h1:hH0lh2mU3AZXPiqMwA0pdtqrwo7PFIMRGush9OobMUs=
+github.com/cnotch/ipchub v1.1.0/go.mod h1:2PbeBs2q2VxxTVCn1eYCDwpAWuVXbq1+N0FU7GimOH4=
+github.com/cnotch/loader v0.0.0-20200405015128-d9d964d09439/go.mod h1:oWpDagHB6p+Kqqq7RoRZKyC4XAXft50hR8pbTxdbYYs=
+github.com/cnotch/queue v0.0.0-20200326024423-6e88bdbf2ad4/go.mod h1:zOssjAlNusOxvtaqT+EMA+Iyi8rrtKr4/XfzN1Fgoeg=
+github.com/cnotch/queue v0.0.0-20201224060551-4191569ce8f6/go.mod h1:zOssjAlNusOxvtaqT+EMA+Iyi8rrtKr4/XfzN1Fgoeg=
+github.com/cnotch/scheduler v0.0.0-20200522024700-1d2da93eefc5/go.mod h1:F4GE3SZkJZ8an1Y0ZCqvSM3jeozNuKzoC67erG1PhIo=
+github.com/cnotch/xlog v0.0.0-20201208005456-cfda439cd3a0/go.mod h1:RW9oHsR79ffl3sR3yMGgxYupMn2btzdtJUwoxFPUE5E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
+github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
+github.com/emitter-io/address v1.0.0/go.mod h1:GfZb5+S/o8694B1GMGK2imUYQyn2skszMvGNA5D84Ug=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
+github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
+github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
+github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
+github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
+github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/kelindar/process v0.0.0-20170730150328-69a29e249ec3/go.mod h1:+lTCLnZFXOkqwD8sLPl6u4erAc0cP8wFegQHfipz7KE=
+github.com/kelindar/rate v1.0.0/go.mod h1:AjT4G+hTItNwt30lucEGZIz8y7Uk5zPho6vurIZ+1Es=
+github.com/kelindar/tcp v1.0.0/go.mod h1:JB5hj1cshLU60XrLij2BBxW3JQ4hOye8vqbyvuKb52k=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/logrusorgru/aurora/v4 v4.0.0 h1:sRjfPpun/63iADiSvGGjgA1cAYegEWMPCJdUpJYn9JA=
+github.com/logrusorgru/aurora/v4 v4.0.0/go.mod h1:lP0iIa2nrnT/qoFXcOZSrZQpJ1o6n2CUf/hyHi2Q4ZQ=
+github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
+github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a h1:N9zuLhTvBSRt0gWSiJswwQ2HqDmtX/ZCDJURnKUt1Ik=
+github.com/lufia/plan9stats v0.0.0-20230326075908-cb1d2100619a/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
+github.com/mcuadros/go-defaults v1.2.0 h1:FODb8WSf0uGaY8elWJAkoLL0Ri6AlZ1bFlenk56oZtc=
+github.com/mcuadros/go-defaults v1.2.0/go.mod h1:WEZtHEVIGYVDqkKSWBdWKUVdRyKlMfulPaGDWIVeCWY=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
+github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
+github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
+github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
+github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
+github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
+github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
+github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
+github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
+github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
+github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
+github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
+github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
+github.com/pion/ice/v2 v2.3.11/go.mod h1:hPcLC3kxMa+JGRzMHqQzjoSj3xtE9F+eoncmXLlCL4E=
+github.com/pion/interceptor v0.1.18/go.mod h1:tpvvF4cPM6NGxFA1DUMbhabzQBxdWMATDGEUYOR9x6I=
+github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
+github.com/pion/mdns v0.0.8/go.mod h1:hYE72WX8WDveIhg7fmXgMKivD3Puklk0Ymzog0lSyaI=
+github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
+github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
+github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
+github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
+github.com/pion/rtp v1.8.1/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
+github.com/pion/rtp v1.8.3 h1:VEHxqzSVQxCkKDSHro5/4IUUG1ea+MFdqR2R3xSpNU8=
+github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
+github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
+github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs=
+github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
+github.com/pion/srtp/v2 v2.0.17/go.mod h1:y5WSHcJY4YfNB/5r7ca5YjHeIr1H3LM1rKArGGs8jMc=
+github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8=
+github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
+github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g=
+github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc=
+github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
+github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY=
+github.com/pion/webrtc/v3 v3.2.20 h1:BQJiXQsJq9LgLp3op7rLy1y8d2WD+LtiS9cpY0uQ22A=
+github.com/pion/webrtc/v3 v3.2.20/go.mod h1:vVURQTBOG5BpWKOJz3nlr23NfTDeyKVmubRNqzQp+Tg=
+github.com/pixelbender/go-sdp v1.1.0/go.mod h1:6IBlz9+BrUHoFTea7gcp4S54khtOhjCW/nVDLhmZBAs=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
+github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+github.com/q191201771/naza v0.30.48 h1:lbYUaa7A15kJKYwOiU4AbFS1Zo8oQwppl2tLEbJTqnw=
+github.com/q191201771/naza v0.30.48/go.mod h1:n+dpJjQSh90PxBwxBNuifOwQttywvSIN5TkWSSYCeBk=
+github.com/quic-go/qtls-go1-20 v0.3.3 h1:17/glZSLI9P9fDAeyCHBFSWSqJcwx1byhLwP5eUIDCM=
+github.com/quic-go/qtls-go1-20 v0.3.3/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
+github.com/quic-go/quic-go v0.38.1 h1:M36YWA5dEhEeT+slOu/SwMEucbYd0YFidxG3KlGPZaE=
+github.com/quic-go/quic-go v0.38.1/go.mod h1:ijnZM7JsFIkp4cRyjxJNIzdSfCLmUMg9wdyhGmg+SN4=
+github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
+github.com/shirou/gopsutil/v3 v3.23.8 h1:xnATPiybo6GgdRoC4YoGnxXZFRc3dqQTGi73oLvvBrE=
+github.com/shirou/gopsutil/v3 v3.23.8/go.mod h1:7hmCaBn+2ZwaZOr6jmPBZDfawwMGuo1id3C6aM8EDqQ=
+github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
+github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
+github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
+github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
+github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
+github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
+github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
+github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
+github.com/yapingcat/gomedia v0.0.0-20230905155010-55b9713fcec1 h1:ilNIuDBR+UKA3qefiyWRoFufIFn3E4tgEXbBM4ILH28=
+github.com/yapingcat/gomedia v0.0.0-20230905155010-55b9713fcec1/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
+github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
+go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
+golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I=
+golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
+golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
+golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY=
+golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
+golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
+golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
+golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
+golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
+golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
+golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
+golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
+golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
+golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
+gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 379 - 0
m7s.live/engine/v4@v4.14.6/http.go

@@ -0,0 +1,379 @@
+package engine
+
+import (
+	"encoding/json"
+	"io"
+	"net/http"
+	"os"
+	"strconv"
+	"strings"
+
+	"go.uber.org/zap"
+	"gopkg.in/yaml.v3"
+	"m7s.live/engine/v4/codec"
+	"m7s.live/engine/v4/config"
+	"m7s.live/engine/v4/util"
+)
+
+const (
+	NO_SUCH_CONIFG = "no such config"
+	NO_SUCH_STREAM = "no such stream"
+)
+
+type GlobalConfig struct {
+	config.Engine
+}
+
+func (conf *GlobalConfig) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
+	if r.URL.Path == "/favicon.ico" {
+		http.ServeFile(rw, r, "favicon.ico")
+		return
+	}
+	rw.Write([]byte("Monibuca API Server\n"))
+	for _, api := range apiList {
+		rw.Write([]byte(api + "\n"))
+	}
+}
+
+func (conf *GlobalConfig) API_summary(rw http.ResponseWriter, r *http.Request) {
+	util.ReturnValue(&summary, rw, r)
+}
+
+func (conf *GlobalConfig) API_plugins(rw http.ResponseWriter, r *http.Request) {
+	util.ReturnValue(Plugins, rw, r)
+}
+
+func (conf *GlobalConfig) API_stream(rw http.ResponseWriter, r *http.Request) {
+	if streamPath := r.URL.Query().Get("streamPath"); streamPath != "" {
+		if s := Streams.Get(streamPath); s != nil {
+			util.ReturnValue(s, rw, r)
+		} else {
+			util.ReturnError(util.APIErrorNoStream, NO_SUCH_STREAM, rw, r)
+		}
+	} else {
+		util.ReturnError(util.APIErrorNoStream, "no streamPath", rw, r)
+	}
+}
+
+func (conf *GlobalConfig) API_sysInfo(rw http.ResponseWriter, r *http.Request) {
+	util.ReturnValue(&SysInfo, rw, r)
+}
+
+func (conf *GlobalConfig) API_closeStream(w http.ResponseWriter, r *http.Request) {
+	if streamPath := r.URL.Query().Get("streamPath"); streamPath != "" {
+		if s := Streams.Get(streamPath); s != nil {
+			s.Close()
+			util.ReturnOK(w, r)
+		} else {
+			util.ReturnError(util.APIErrorNoStream, NO_SUCH_STREAM, w, r)
+		}
+	} else {
+		util.ReturnError(util.APIErrorNoStream, "no streamPath", w, r)
+	}
+}
+
+// API_getConfig 获取指定的配置信息
+func (conf *GlobalConfig) API_getConfig(w http.ResponseWriter, r *http.Request) {
+	var p *Plugin
+	var q = r.URL.Query()
+	if configName := q.Get("name"); configName != "" {
+		if c, ok := Plugins[configName]; ok {
+			p = c
+		} else {
+			util.ReturnError(util.APIErrorNoConfig, NO_SUCH_CONIFG, w, r)
+			return
+		}
+	} else {
+		p = Engine
+	}
+	var data any
+	if q.Get("yaml") != "" {
+		var tmp struct {
+			File     string
+			Modified string
+			Merged   string
+		}
+		mm, err := yaml.Marshal(p.RawConfig.File)
+		if err == nil {
+			tmp.File = string(mm)
+		}
+		mm, err = yaml.Marshal(p.RawConfig.Modify)
+		if err == nil {
+			tmp.Modified = string(mm)
+		}
+		mm, err = yaml.Marshal(p.RawConfig.GetMap())
+		if err == nil {
+			tmp.Merged = string(mm)
+		}
+		data = &tmp
+	} else if q.Get("formily") != "" {
+		data = p.RawConfig.GetFormily()
+	} else {
+		data = &p.RawConfig
+	}
+	util.ReturnValue(data, w, r)
+}
+
+// API_modifyConfig 修改并保存配置
+func (conf *GlobalConfig) API_modifyConfig(w http.ResponseWriter, r *http.Request) {
+	var p *Plugin
+	var q = r.URL.Query()
+	var err error
+	if configName := q.Get("name"); configName != "" {
+		if c, ok := Plugins[configName]; ok {
+			p = c
+		} else {
+			util.ReturnError(util.APIErrorNoConfig, NO_SUCH_CONIFG, w, r)
+			return
+		}
+	} else {
+		p = Engine
+	}
+	var modified map[string]any
+	if q.Get("yaml") != "" {
+		err = yaml.NewDecoder(r.Body).Decode(&modified)
+	} else {
+		err = json.NewDecoder(r.Body).Decode(&modified)
+	}
+	if err != nil {
+		util.ReturnError(util.APIErrorDecode, err.Error(), w, r)
+		return
+	}
+	p.RawConfig.ParseModifyFile(modified)
+	if err = p.Save(); err != nil {
+		util.ReturnError(util.APIErrorSave, err.Error(), w, r)
+		return
+	}
+	util.ReturnOK(w, r)
+}
+
+// API_updateConfig 热更新配置
+func (conf *GlobalConfig) API_updateConfig(w http.ResponseWriter, r *http.Request) {
+	var p *Plugin
+	var q = r.URL.Query()
+	if configName := q.Get("name"); configName != "" {
+		if c, ok := Plugins[configName]; ok {
+			p = c
+		} else {
+			util.ReturnError(util.APIErrorNoConfig, NO_SUCH_CONIFG, w, r)
+			return
+		}
+	} else {
+		p = Engine
+	}
+	var err error
+	var modified map[string]any
+	if q.Get("yaml") != "" {
+		err = yaml.NewDecoder(r.Body).Decode(&modified)
+	} else {
+		err = json.NewDecoder(r.Body).Decode(&modified)
+	}
+	if err != nil {
+		util.ReturnError(util.APIErrorDecode, err.Error(), w, r)
+		return
+	}
+	p.RawConfig.ParseModifyFile(modified)
+	if err = p.Save(); err != nil {
+		util.ReturnError(util.APIErrorSave, err.Error(), w, r)
+		return
+	}
+	p.Update(&p.RawConfig)
+	util.ReturnOK(w, r)
+}
+
+func (conf *GlobalConfig) API_list_pull(w http.ResponseWriter, r *http.Request) {
+	util.ReturnFetchValue(func() (result []any) {
+		Pullers.Range(func(key, value any) bool {
+			result = append(result, value)
+			return true
+		})
+		return
+	}, w, r)
+}
+
+func (conf *GlobalConfig) API_list_push(w http.ResponseWriter, r *http.Request) {
+	util.ReturnFetchValue(func() (result []any) {
+		Pushers.Range(func(key, value any) bool {
+			result = append(result, value)
+			return true
+		})
+		return
+	}, w, r)
+}
+
+func (conf *GlobalConfig) API_stop_push(w http.ResponseWriter, r *http.Request) {
+	q := r.URL.Query()
+	pusher, ok := Pushers.Load(q.Get("url"))
+	if ok {
+		pusher.(IPusher).Stop()
+		util.ReturnOK(w, r)
+	} else {
+		util.ReturnError(util.APIErrorNoPusher, "no such pusher", w, r)
+	}
+}
+
+func (conf *GlobalConfig) API_stop_subscribe(w http.ResponseWriter, r *http.Request) {
+	q := r.URL.Query()
+	streamPath := q.Get("streamPath")
+	id := q.Get("id")
+	s := Streams.Get(streamPath)
+	if s == nil {
+		util.ReturnError(util.APIErrorNoStream, NO_SUCH_STREAM, w, r)
+		return
+	}
+	suber := s.Subscribers.Find(id)
+	if suber == nil {
+		util.ReturnError(util.APIErrorNoSubscriber, "no such subscriber", w, r)
+		return
+	}
+	suber.Stop(zap.String("reason", "stop by api"))
+	util.ReturnOK(w, r)
+}
+
+func (conf *GlobalConfig) API_replay_rtpdump(w http.ResponseWriter, r *http.Request) {
+	q := r.URL.Query()
+	streamPath := q.Get("streamPath")
+	if streamPath == "" {
+		streamPath = "dump/rtsp"
+	}
+	dumpFile := q.Get("dump")
+	if dumpFile == "" {
+		dumpFile = streamPath + ".rtpdump"
+	}
+	cv := q.Get("vcodec")
+	ca := q.Get("acodec")
+	cvp := q.Get("vpayload")
+	cap := q.Get("apayload")
+	var pub RTPDumpPublisher
+	i, _ := strconv.ParseInt(cvp, 10, 64)
+	pub.VPayloadType = byte(i)
+	i, _ = strconv.ParseInt(cap, 10, 64)
+	pub.APayloadType = byte(i)
+	switch cv {
+	case "h264":
+		pub.VCodec = codec.CodecID_H264
+	case "h265":
+		pub.VCodec = codec.CodecID_H265
+	}
+	switch ca {
+	case "aac":
+		pub.ACodec = codec.CodecID_AAC
+	case "pcma":
+		pub.ACodec = codec.CodecID_PCMA
+	case "pcmu":
+		pub.ACodec = codec.CodecID_PCMU
+	}
+	ss := strings.Split(dumpFile, ",")
+	if len(ss) > 1 {
+		if err := Engine.Publish(streamPath, &pub); err != nil {
+			util.ReturnError(util.APIErrorPublish, err.Error(), w, r)
+		} else {
+			for _, s := range ss {
+				f, err := os.Open(s)
+				if err != nil {
+					util.ReturnError(util.APIErrorOpen, err.Error(), w, r)
+					return
+				}
+				go pub.Feed(f)
+			}
+			util.ReturnOK(w, r)
+		}
+	} else {
+		f, err := os.Open(dumpFile)
+		if err != nil {
+			util.ReturnError(util.APIErrorOpen, err.Error(), w, r)
+			return
+		}
+		if err := Engine.Publish(streamPath, &pub); err != nil {
+			util.ReturnError(util.APIErrorPublish, err.Error(), w, r)
+		} else {
+			pub.SetIO(f)
+			util.ReturnOK(w, r)
+			go pub.Feed(f)
+		}
+	}
+}
+
+func (conf *GlobalConfig) API_replay_ts(w http.ResponseWriter, r *http.Request) {
+	q := r.URL.Query()
+	streamPath := q.Get("streamPath")
+	if streamPath == "" {
+		streamPath = "dump/ts"
+	}
+	dumpFile := q.Get("dump")
+	if dumpFile == "" {
+		dumpFile = streamPath + ".ts"
+	}
+	f, err := os.Open(dumpFile)
+	if err != nil {
+		util.ReturnError(util.APIErrorOpen, err.Error(), w, r)
+		return
+	}
+	var pub TSPublisher
+	if err := Engine.Publish(streamPath, &pub); err != nil {
+		util.ReturnError(util.APIErrorPublish, err.Error(), w, r)
+	} else {
+		tsReader := NewTSReader(&pub)
+		pub.SetIO(f)
+		go func() {
+			tsReader.Feed(f)
+			tsReader.Close()
+		}()
+		util.ReturnOK(w, r)
+	}
+}
+
+func (conf *GlobalConfig) API_replay_mp4(w http.ResponseWriter, r *http.Request) {
+	q := r.URL.Query()
+	streamPath := q.Get("streamPath")
+	if streamPath == "" {
+		streamPath = "dump/mp4"
+	}
+	dumpFile := q.Get("dump")
+	if dumpFile == "" {
+		dumpFile = streamPath + ".mp4"
+	}
+	var pub MP4Publisher
+	f, err := os.Open(dumpFile)
+	if err != nil {
+		util.ReturnError(util.APIErrorOpen, err.Error(), w, r)
+		return
+	}
+	if err := Engine.Publish(streamPath, &pub); err != nil {
+		util.ReturnError(util.APIErrorPublish, err.Error(), w, r)
+	} else {
+		pub.SetIO(f)
+		util.ReturnOK(w, r)
+		go pub.ReadMP4Data(f)
+	}
+}
+
+func (conf *GlobalConfig) API_insertSEI(w http.ResponseWriter, r *http.Request) {
+	q := r.URL.Query()
+	streamPath := q.Get("streamPath")
+	s := Streams.Get(streamPath)
+	if s == nil {
+		util.ReturnError(util.APIErrorNoStream, NO_SUCH_STREAM, w, r)
+		return
+	}
+	t := q.Get("type")
+	tb, err := strconv.ParseInt(t, 10, 8)
+	if err != nil {
+		if t == "" {
+			tb = 5
+		} else {
+			util.ReturnError(util.APIErrorQueryParse, "type must a number", w, r)
+			return
+		}
+	}
+	sei, err := io.ReadAll(r.Body)
+	if err == nil {
+		if s.Tracks.AddSEI(byte(tb), sei) {
+			util.ReturnOK(w, r)
+		} else {
+			util.ReturnError(util.APIErrorNoSEI, "no sei track", w, r)
+		}
+	} else {
+		util.ReturnError(util.APIErrorNoBody, err.Error(), w, r)
+	}
+}

+ 292 - 0
m7s.live/engine/v4@v4.14.6/io.go

@@ -0,0 +1,292 @@
+package engine
+
+import (
+	"context"
+	"crypto/md5"
+	"errors"
+	"fmt"
+	"io"
+	"net/url"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+	"m7s.live/engine/v4/config"
+	"m7s.live/engine/v4/log"
+	"m7s.live/engine/v4/util"
+)
+
+type IOConfig interface {
+	config.Publish | config.Subscribe
+}
+type ClientConfig interface {
+	config.Pull | config.Push
+}
+
+type AuthSub interface {
+	OnAuth(*util.Promise[ISubscriber]) error
+}
+
+type AuthPub interface {
+	OnAuth(*util.Promise[IPublisher]) error
+}
+
+// 发布者或者订阅者的共用结构体
+type IO struct {
+	ID                      string
+	Type                    string
+	RemoteAddr              string
+	context.Context         `json:"-" yaml:"-"` //不要直接设置,应当通过SetParentCtx传入父级Context
+	context.CancelCauseFunc `json:"-" yaml:"-"` //流关闭是关闭发布者或者订阅者
+	*log.Logger             `json:"-" yaml:"-"`
+	StartTime               time.Time //创建时间
+	Stream                  *Stream   `json:"-" yaml:"-"`
+	io.Reader               `json:"-" yaml:"-"`
+	io.Writer               `json:"-" yaml:"-"`
+	io.Closer               `json:"-" yaml:"-"`
+	Args                    url.Values
+	Spesific                IIO `json:"-" yaml:"-"`
+}
+
+func (io *IO) IsClosed() bool {
+	return io.Err() != nil
+}
+
+// SetIO(可选) 设置Writer、Reader、Closer
+func (i *IO) SetIO(conn any) {
+	if v, ok := conn.(io.Closer); ok {
+		i.Closer = v
+	}
+	if v, ok := conn.(io.Reader); ok {
+		i.Reader = v
+	}
+	if v, ok := conn.(io.Writer); ok {
+		i.Writer = v
+	}
+}
+
+// SetParentCtx(可选)
+func (i *IO) SetParentCtx(parent context.Context) {
+	i.Context, i.CancelCauseFunc = context.WithCancelCause(parent)
+}
+
+func (i *IO) SetLogger(logger *log.Logger) {
+	i.Logger = logger
+}
+
+func (i *IO) OnEvent(event any) {
+	switch event.(type) {
+	case SEclose:
+		i.close(StopError{zap.String("event", "close")})
+	case SEKick:
+		i.close(StopError{zap.String("event", "kick")})
+	}
+}
+
+func (io *IO) IsShutdown() bool {
+	if io.Stream == nil {
+		return false
+	}
+	return io.Stream.IsShutdown()
+}
+
+type IIO interface {
+	receive(string, IIO) error
+	IsClosed() bool
+	OnEvent(any)
+	Stop(reason ...zapcore.Field)
+	SetIO(any)
+	SetParentCtx(context.Context)
+	SetLogger(*log.Logger)
+	IsShutdown() bool
+	log.Zap
+}
+
+func (i *IO) close(err StopError) bool {
+	if i.IsClosed() {
+		i.Warn("already closed", err...)
+		return false
+	}
+	i.Info("close", err...)
+	if i.CancelCauseFunc != nil {
+		i.CancelCauseFunc(err)
+	}
+	if i.Closer != nil {
+		i.Closer.Close()
+	}
+	return true
+}
+
+// Stop 停止订阅或者发布,由订阅者或者发布者调用
+func (io *IO) Stop(reason ...zapcore.Field) {
+	io.close(StopError(reason))
+}
+
+type StopError []zapcore.Field
+
+func (s StopError) Error() string {
+	return "stop"
+}
+
+var (
+	ErrDuplicatePublish = errors.New("Duplicate Publish")
+	ErrBadStreamName    = errors.New("StreamPath Illegal")
+	ErrBadTrackName     = errors.New("Track Already Exist")
+	ErrTrackMute        = errors.New("Track Mute")
+	ErrStreamIsClosed   = errors.New("Stream Is Closed")
+	ErrPublisherLost    = errors.New("Publisher Lost")
+	ErrAuth             = errors.New("Auth Failed")
+	OnAuthSub           func(p *util.Promise[ISubscriber]) error
+	OnAuthPub           func(p *util.Promise[IPublisher]) error
+)
+
+func (io *IO) auth(key string, secret string, expire string) bool {
+	if unixTime, err := strconv.ParseInt(expire, 16, 64); err != nil || time.Now().Unix() > unixTime {
+		return false
+	}
+	trueSecret := md5.Sum([]byte(key + io.Stream.Path + expire))
+	for i := 0; i < 16; i++ {
+		hex, err := strconv.ParseInt(secret[i<<1:(i<<1)+2], 16, 16)
+		if trueSecret[i] != byte(hex) || err != nil {
+			return false
+		}
+	}
+	return true
+}
+
+// receive 用于接收发布或者订阅
+func (io *IO) receive(streamPath string, specific IIO) error {
+	streamPath = strings.Trim(streamPath, "/")
+	u, err := url.Parse(streamPath)
+	if err != nil {
+		if EngineConfig.LogLang == "zh" {
+			log.Error("接收流路径(流唯一标识)格式错误,必须形如 live/test ", zap.String("流路径", streamPath), zap.Error(err))
+		} else {
+			log.Error("receive streamPath wrong format", zap.String("streamPath", streamPath), zap.Error(err))
+		}
+		return err
+	}
+	io.Args = u.Query()
+	wt := time.Second * 5
+	var iSub ISubscriber
+	var iPub IPublisher
+	var isSubscribe bool
+	if iSub, isSubscribe = specific.(ISubscriber); isSubscribe {
+		wt = iSub.GetSubscriber().Config.WaitTimeout
+	} else {
+		iPub = specific.(IPublisher)
+	}
+	s, create := findOrCreateStream(u.Path, wt)
+	if s == nil {
+		return ErrBadStreamName
+	}
+	if io.Stream == nil { //初次
+		if io.Type == "" {
+			io.Type = reflect.TypeOf(specific).Elem().Name()
+		}
+		logFeilds := []zapcore.Field{zap.String("type", io.Type)}
+		if io.ID != "" {
+			logFeilds = append(logFeilds, zap.String("ID", io.ID))
+		}
+		if io.Logger == nil {
+			io.Logger = s.With(logFeilds...)
+		} else {
+			io.Logger = io.Logger.With(logFeilds...)
+		}
+	}
+	io.Stream = s
+	io.Spesific = specific
+	io.StartTime = time.Now()
+	if io.Context == nil {
+		io.Debug("create context")
+		io.SetParentCtx(Engine.Context)
+	} else if io.IsClosed() {
+		io.Debug("recreate context")
+		io.SetParentCtx(Engine.Context)
+	} else {
+		io.Debug("warp context")
+		io.SetParentCtx(io.Context)
+	}
+	defer func() {
+		if err == nil {
+			specific.OnEvent(specific)
+		}
+	}()
+	if !isSubscribe {
+		puber := iPub.GetPublisher()
+		conf := puber.Config
+		io.Info("publish", zap.String("ptr", fmt.Sprintf("%p", io.Context)))
+		s.pubLocker.Lock()
+		defer s.pubLocker.Unlock()
+		if config.Global.EnableAuth {
+			onAuthPub := OnAuthPub
+			if auth, ok := specific.(AuthPub); ok {
+				onAuthPub = auth.OnAuth
+			}
+			if onAuthPub != nil {
+				authPromise := util.NewPromise(iPub)
+				if err = onAuthPub(authPromise); err == nil {
+					err = authPromise.Await()
+				}
+				if err != nil {
+					return err
+				}
+			} else if conf.Key != "" {
+				if !io.auth(conf.Key, io.Args.Get(conf.SecretArgName), io.Args.Get(conf.ExpireArgName)) {
+					return ErrAuth
+				}
+			}
+		}
+		if promise := util.NewPromise(iPub); s.Receive(promise) {
+			err = promise.Await()
+			return err
+		}
+	} else {
+		conf := iSub.GetSubscriber().Config
+		io.Info("subscribe")
+		if create {
+			EventBus <- InvitePublish{CreateEvent(s.Path)} // 通知发布者按需拉流
+		}
+		if config.Global.EnableAuth && !conf.Internal {
+			onAuthSub := OnAuthSub
+			if auth, ok := specific.(AuthSub); ok {
+				onAuthSub = auth.OnAuth
+			}
+			if onAuthSub != nil {
+				authPromise := util.NewPromise(iSub)
+				if err = onAuthSub(authPromise); err == nil {
+					err = authPromise.Await()
+				}
+				if err != nil {
+					return err
+				}
+			} else if conf.Key != "" {
+				if !io.auth(conf.Key, io.Args.Get(conf.SecretArgName), io.Args.Get(conf.ExpireArgName)) {
+					return ErrAuth
+				}
+			}
+		}
+		if promise := util.NewPromise(iSub); s.Receive(promise) {
+			err = promise.Await()
+			return err
+		}
+	}
+	return ErrStreamIsClosed
+}
+
+// ClientIO 作为Client角色(Puller,Pusher)的公共结构体
+type ClientIO[C ClientConfig] struct {
+	Config         *C
+	StreamPath     string // 本地流标识
+	RemoteURL      string // 远程服务器地址(用于推拉)
+	ReConnectCount int    //重连次数
+}
+
+func (c *ClientIO[C]) init(streamPath string, url string, conf *C) {
+	c.Config = conf
+	c.StreamPath = streamPath
+	c.RemoteURL = url
+}

+ 66 - 0
m7s.live/engine/v4@v4.14.6/lang/lang.go

@@ -0,0 +1,66 @@
+package lang
+
+import (
+	_ "embed"
+
+	"gopkg.in/yaml.v3"
+	"os"
+	"os/exec"
+	"strings"
+	"runtime"
+)
+
+//go:embed zh.yaml
+var zhYaml []byte
+var zh map[string]string
+
+func init() {
+	yaml.Unmarshal(zhYaml, &zh)
+}
+
+func Get(lang string) map[string]string {
+	if lang == "zh" {
+		if runtime.GOOS == "linux" && !IsTerminalSupportChinese() {
+			return nil
+		}
+		return zh
+	}
+	return nil
+}
+
+func Update(lang string, key string, value string) {
+	if lang == "zh" {
+		zh[key] = value
+	}
+}
+
+func Merge(lang string, data map[string]string) {
+	if lang == "zh" {
+		for k, v := range data {
+			zh[k] = v
+		}
+	}
+}
+
+func IsTerminalSupportChinese() bool {
+	// 获取终端的环境变量
+	env := os.Environ()
+
+	// 查找 LANG 环境变量
+	isSupportUTF8 := false
+	for _, v := range env {
+		if strings.Index(v, "LANG") != -1 && strings.Index(v, "UTF-8") != -1 {
+			isSupportUTF8 = true
+		}
+	}
+	if isSupportUTF8 {
+		// 在终端中打印中文字符
+		cmd := exec.Command("echo", "你好!")
+		_, err := cmd.CombinedOutput()
+		if err == nil {
+			return true
+		}
+		return false
+	}
+	return false
+}

+ 68 - 0
m7s.live/engine/v4@v4.14.6/lang/zh.yaml

@@ -0,0 +1,68 @@
+engine:    引擎
+config:    配置
+stream:    流
+publish:   发布
+subscribe: 订阅
+pull:      拉取
+push:      推送
+action:    动作
+type:      类型
+enable:    启用
+listenaddr: 监听地址
+kick:     踢出
+stop:     停止
+start:    开始
+install:  安装
+version:  版本
+name:     名称
+state:    状态
+initialize: 初始化
+"start read": 开始读取
+"start pull": 开始从远端拉流
+"stop pull": 停止从远端拉流
+"restart pull": 重新拉流
+"pull interrupt": 拉流中断
+"pull publish": 拉流发布
+"wait publisher": 等待发布者发布
+"wait timeout": 等待超时
+created:  已创建
+create:   创建
+timeout:  超时
+"track available": 轨道可用
+track:    轨道
+"track timeout": 轨道超时
+"last writetime": 最后写入时间
+track+1:  轨道+1
+playblock: 阻塞式播放
+"play neither video nor audio": 播放既没有视频也没有音频
+"play before subscribe": 播放之前需要先订阅
+"play stop": 播放停止
+"suber -1": 订阅者-1
+"suber +1": 订阅者+1
+"innersuber +1": 内部订阅者+1
+reamins:  剩余
+"need sequence frame": 需要序列帧
+"video codecID not support": 视频编码不支持
+"http handle added": http处理器已添加
+"http handle added to engine": http处理器已添加到引擎
+"plugin disabled": 插件已禁用
+"audio codec not support yet": 音频编码暂不支持
+"video track attached": 视频轨道已附加
+"audio track attached": 音频轨道已附加
+"data track attached": 数据轨道已附加
+"track back online": 轨道已恢复
+"first frame read": 第一帧已读取
+"fu have no start": rtp的FU起始包丢了
+"disabled by env": 被环境变量禁用
+"event cost too much time": 事件处理耗时过长
+"run timeout": 流事件处理耗时过长
+"cost": 耗时
+firstTs:  第一帧时间戳
+firstSeq: 第一帧序列号
+skipSeq:  跳过序列号
+skipTs:   跳过时间戳
+"nalu type not supported": nalu类型不支持
+"create file": 创建文件
+"duplicate publish": 重复发布
+"republish": 重新发布
+"stream already had a publisher": 流已经有发布者了

+ 129 - 0
m7s.live/engine/v4@v4.14.6/log/log.go

@@ -0,0 +1,129 @@
+package log
+
+import (
+	"io"
+
+	// "github.com/mattn/go-colorable"
+	"gopkg.in/yaml.v3"
+
+	// log "github.com/sirupsen/logrus"
+	. "github.com/logrusorgru/aurora/v4"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+)
+
+var engineConfig = zapcore.EncoderConfig{
+	// Keys can be anything except the empty string.
+	TimeKey:        "T",
+	LevelKey:       "L",
+	NameKey:        "N",
+	CallerKey:      "C",
+	FunctionKey:    zapcore.OmitKey,
+	MessageKey:     "M",
+	StacktraceKey:  "S",
+	LineEnding:     zapcore.DefaultLineEnding,
+	EncodeLevel:    zapcore.CapitalColorLevelEncoder,
+	EncodeTime:     zapcore.TimeEncoderOfLayout("15:04:05"),
+	EncodeDuration: zapcore.StringDurationEncoder,
+	EncodeCaller:   zapcore.ShortCallerEncoder,
+	EncodeName:     NameEncoder,
+	NewReflectedEncoder: func(w io.Writer) zapcore.ReflectedEncoder {
+		return yaml.NewEncoder(w)
+	},
+}
+var LogLevel = zap.NewAtomicLevelAt(zap.DebugLevel)
+var Trace bool
+var logger *zap.Logger = zap.New(
+	zapcore.NewCore(zapcore.NewConsoleEncoder(engineConfig), zapcore.AddSync(multipleWriter), LogLevel),
+)
+var sugaredLogger *zap.SugaredLogger = logger.Sugar()
+var LocaleLogger *Logger
+
+func NameEncoder(loggerName string, enc zapcore.PrimitiveArrayEncoder) {
+	enc.AppendString(Colorize(loggerName, WhiteFg|BlackBg).String())
+}
+
+type Zap interface {
+	Lang(lang map[string]string) *Logger
+	Named(name string) *Logger
+	With(fields ...zap.Field) *Logger
+	Trace(msg string, fields ...zap.Field)
+	Debug(msg string, fields ...zap.Field)
+	Info(msg string, fields ...zap.Field)
+	Warn(msg string, fields ...zap.Field)
+	Error(msg string, fields ...zap.Field)
+}
+
+type Logger struct {
+	*zap.Logger
+	lang map[string]string
+}
+
+func (l Logger) Lang(lang map[string]string) *Logger {
+	l.Logger = logger
+	l.lang = lang
+	return &l
+}
+
+func (l Logger) Named(name string) *Logger {
+	l.Logger = l.Logger.Named(name)
+	return &l
+}
+
+func (l Logger) With(fields ...zap.Field) *Logger {
+	for i, field := range fields {
+		if v, ok := l.lang[field.Key]; ok {
+			fields[i].Key = v
+		}
+	}
+	l.Logger = l.Logger.With(fields...)
+	return &l
+}
+
+func (l *Logger) formatLang(msg *string, fields []zapcore.Field) {
+	if l.lang != nil {
+		if v, ok := l.lang[*msg]; ok {
+			*msg = v
+		}
+		for i, field := range fields {
+			if v, ok := l.lang[field.Key]; ok {
+				fields[i].Key = v
+			}
+		}
+	}
+}
+
+func (l *Logger) Trace(msg string, fields ...zap.Field) {
+	l.formatLang(&msg, fields)
+	l.Logger.Debug(msg, fields...)
+}
+
+func (l *Logger) Debug(msg string, fields ...zap.Field) {
+	l.formatLang(&msg, fields)
+	l.Logger.Debug(msg, fields...)
+}
+
+func (l *Logger) Info(msg string, fields ...zap.Field) {
+	l.formatLang(&msg, fields)
+	l.Logger.Info(msg, fields...)
+}
+
+func (l *Logger) Warn(msg string, fields ...zap.Field) {
+	l.formatLang(&msg, fields)
+	l.Logger.Warn(msg, fields...)
+}
+
+func (l *Logger) Error(msg string, fields ...zap.Field) {
+	l.formatLang(&msg, fields)
+	l.Logger.Error(msg, fields...)
+}
+
+func (l *Logger) Fatal(msg string, fields ...zap.Field) {
+	l.formatLang(&msg, fields)
+	l.Logger.Fatal(msg, fields...)
+}
+
+func (l *Logger) Panic(msg string, fields ...zap.Field) {
+	l.formatLang(&msg, fields)
+	l.Logger.Panic(msg, fields...)
+}

+ 46 - 0
m7s.live/engine/v4@v4.14.6/log/sugar.go

@@ -0,0 +1,46 @@
+package log
+
+func Debug(args ...any) {
+	sugaredLogger.Debug(args...)
+}
+
+func Info(args ...any) {
+	sugaredLogger.Info(args...)
+}
+
+func Warn(args ...any) {
+	sugaredLogger.Warn(args...)
+}
+
+func Error(args ...any) {
+	sugaredLogger.Error(args...)
+}
+
+func Debugf(format string, args ...interface{}) {
+	sugaredLogger.Debugf(format, args...)
+}
+
+// Infof logs a message at level Info on the standard logger.
+func Infof(format string, args ...interface{}) {
+	sugaredLogger.Infof(format, args...)
+}
+
+// Warnf logs a message at level Warn on the standard logger.
+func Warnf(format string, args ...interface{}) {
+	sugaredLogger.Warnf(format, args...)
+}
+
+// Errorf logs a message at level Error on the standard logger.
+func Errorf(format string, args ...interface{}) {
+	sugaredLogger.Errorf(format, args...)
+}
+
+// Panicf logs a message at level Panic on the standard logger.
+func Panicf(format string, args ...interface{}) {
+	sugaredLogger.Panicf(format, args...)
+}
+
+// Fatalf logs a message at level Fatal on the standard logger then the process will exit with status set to 1.
+func Fatalf(format string, args ...interface{}) {
+	sugaredLogger.Fatalf(format, args...)
+}

+ 36 - 0
m7s.live/engine/v4@v4.14.6/log/writer.go

@@ -0,0 +1,36 @@
+package log
+
+import (
+	"io"
+	"os"
+	"sync"
+)
+
+type MultipleWriter struct {
+	io.Writer // 默认输出到标准输出
+	sync.Map  // 用于存储多个输出
+}
+
+func (m *MultipleWriter) Write(p []byte) (n int, err error) {
+	n, err = m.Writer.Write(p)
+	m.Range(func(key, value any) bool {
+		if _, err := key.(io.Writer).Write(p); err != nil {
+			m.Delete(key)
+		}
+		return true
+	})
+	return
+}
+
+func (m *MultipleWriter) Add(writer io.Writer) {
+	m.Map.Store(writer, struct{}{})
+}
+
+var multipleWriter = &MultipleWriter{Writer: os.Stdout}
+
+func AddWriter(writer io.Writer) {
+	multipleWriter.Add(writer)
+}
+func DeleteWriter(writer io.Writer) {
+	multipleWriter.Delete(writer)
+}

+ 256 - 0
m7s.live/engine/v4@v4.14.6/main.go

@@ -0,0 +1,256 @@
+package engine // import "m7s.live/engine/v4"
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"io"
+	"net"
+	"net/http"
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"time"
+
+	"github.com/denisbrodbeck/machineid"
+	"github.com/google/uuid"
+	. "github.com/logrusorgru/aurora/v4"
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+	"gopkg.in/yaml.v3"
+	"m7s.live/engine/v4/lang"
+	"m7s.live/engine/v4/log"
+	"m7s.live/engine/v4/util"
+)
+
+var (
+	SysInfo struct {
+		StartTime time.Time //启动时间
+		LocalIP   string
+		Version   string
+	}
+	ExecPath = os.Args[0]
+	ExecDir  = filepath.Dir(ExecPath)
+	// ConfigRaw 配置信息的原始数据
+	ConfigRaw    []byte
+	Plugins      = make(map[string]*Plugin) // Plugins 所有的插件配置
+	plugins      []*Plugin                  //插件列表
+	EngineConfig = &GlobalConfig{}
+	Engine       = InstallPlugin(EngineConfig)
+	SettingDir   = filepath.Join(ExecDir, ".m7s")           //配置缓存目录,该目录按照插件名称作为文件名存储修改过的配置
+	MergeConfigs = []string{"Publish", "Subscribe", "HTTP"} //需要合并配置的属性项,插件若没有配置则使用全局配置
+	EventBus     chan any
+	apiList      []string //注册到引擎的API接口列表
+)
+
+func init() {
+	if setting_dir := os.Getenv("M7S_SETTING_DIR"); setting_dir != "" {
+		SettingDir = setting_dir
+	}
+	if conn, err := net.Dial("udp", "114.114.114.114:80"); err == nil {
+		SysInfo.LocalIP, _, _ = strings.Cut(conn.LocalAddr().String(), ":")
+	}
+}
+
+// Run 启动Monibuca引擎,传入总的Context,可用于关闭所有
+func Run(ctx context.Context, conf any) (err error) {
+	id, _ := machineid.ProtectedID("monibuca")
+	SysInfo.StartTime = time.Now()
+	SysInfo.Version = Engine.Version
+	Engine.Context = ctx
+	var cg map[string]map[string]any
+	switch v := conf.(type) {
+	case string:
+		if _, err = os.Stat(v); err != nil {
+			v = filepath.Join(ExecDir, v)
+		}
+		if ConfigRaw, err = os.ReadFile(v); err != nil {
+			log.Warn("read config file error:", err.Error())
+		}
+	case []byte:
+		ConfigRaw = v
+	case map[string]map[string]any:
+		cg = v
+	}
+
+	if err = util.CreateShutdownScript(); err != nil {
+		log.Error("create shutdown script error:", err)
+	}
+
+	if err = os.MkdirAll(SettingDir, 0766); err != nil {
+		log.Error("create dir .m7s error:", err)
+		return
+	}
+	log.Info("Ⓜ starting engine:", Blink(Engine.Version))
+	if ConfigRaw != nil {
+		if err = yaml.Unmarshal(ConfigRaw, &cg); err != nil {
+			log.Error("parsing yml error:", err)
+		}
+	}
+	Engine.RawConfig.Parse(&EngineConfig.Engine, "GLOBAL")
+	if cg != nil {
+		Engine.RawConfig.ParseUserFile(cg["global"])
+	}
+	var logger log.Logger
+	log.LocaleLogger = logger.Lang(lang.Get(EngineConfig.LogLang))
+	if EngineConfig.LogLevel == "trace" {
+		log.Trace = true
+		log.LogLevel.SetLevel(zap.DebugLevel)
+	} else {
+		loglevel, err := zapcore.ParseLevel(EngineConfig.LogLevel)
+		if err != nil {
+			logger.Error("parse log level error:", zap.Error(err))
+			loglevel = zapcore.InfoLevel
+		}
+		log.LogLevel.SetLevel(loglevel)
+	}
+
+	Engine.Logger = log.LocaleLogger.Named("engine")
+
+	Engine.assign()
+	Engine.Logger.Debug("", zap.Any("config", EngineConfig))
+	util.PoolSize = EngineConfig.PoolSize
+	EventBus = make(chan any, EngineConfig.EventBusSize)
+	go EngineConfig.Listen(Engine)
+	for _, plugin := range plugins {
+		plugin.Logger = log.LocaleLogger.Named(plugin.Name)
+		if os.Getenv(strings.ToUpper(plugin.Name)+"_ENABLE") == "false" {
+			plugin.Disabled = true
+			plugin.Warn("disabled by env")
+			continue
+		}
+		plugin.Info("initialize", zap.String("version", plugin.Version))
+
+		plugin.RawConfig.Parse(plugin.Config, strings.ToUpper(plugin.Name))
+		for _, fname := range MergeConfigs {
+			if name := strings.ToLower(fname); plugin.RawConfig.Has(name) {
+				plugin.RawConfig.Get(name).ParseGlobal(Engine.RawConfig.Get(name))
+			}
+		}
+		var userConfig map[string]any
+		if plugin.defaultYaml != "" {
+			if err := yaml.Unmarshal([]byte(plugin.defaultYaml), &userConfig); err != nil {
+				log.Error("parsing default config error:", err)
+			} else {
+				plugin.RawConfig.ParseDefaultYaml(userConfig)
+			}
+		}
+		userConfig = cg[strings.ToLower(plugin.Name)]
+		plugin.RawConfig.ParseUserFile(userConfig)
+		if EngineConfig.DisableAll {
+			plugin.Disabled = true
+		}
+		if userConfig["enable"] == false {
+			plugin.Disabled = true
+		} else if userConfig["enable"] == true {
+			plugin.Disabled = false
+		}
+		if plugin.Disabled {
+			plugin.Warn("plugin disabled")
+		} else {
+			plugin.assign()
+		}
+	}
+	UUID := uuid.NewString()
+
+	contentBuf := bytes.NewBuffer(nil)
+	req, _ := http.NewRequestWithContext(ctx, http.MethodPost, "https://console.monibuca.com/report", nil)
+	req.Header.Set("Content-Type", "application/json")
+	version := Engine.Version
+	if ver, ok := ctx.Value("version").(string); ok && ver != "" && ver != "dev" {
+		version = ver
+	}
+	if EngineConfig.LogLang == "zh" {
+		log.Info("monibuca ", version, Green(" 启动成功"))
+	} else {
+		log.Info("monibuca ", version, Green(" start success"))
+	}
+	var enabledPlugins, disabledPlugins []*Plugin
+	for _, plugin := range plugins {
+		if plugin.Disabled {
+			disabledPlugins = append(disabledPlugins, plugin)
+		} else {
+			enabledPlugins = append(enabledPlugins, plugin)
+		}
+	}
+	if EngineConfig.LogLang == "zh" {
+		fmt.Print("已运行的插件:")
+	} else {
+		fmt.Print("enabled plugins:")
+	}
+	for _, plugin := range enabledPlugins {
+		fmt.Print(Colorize(" "+plugin.Name+" ", BlackFg|GreenBg|BoldFm), " ")
+	}
+	fmt.Println()
+	if EngineConfig.LogLang == "zh" {
+		fmt.Print("已禁用的插件:")
+	} else {
+		fmt.Print("disabled plugins:")
+	}
+	for _, plugin := range disabledPlugins {
+		fmt.Print(Colorize(" "+plugin.Name+" ", BlackFg|RedBg|CrossedOutFm), " ")
+	}
+	fmt.Println()
+	if EngineConfig.LogLang == "zh" {
+		fmt.Println(Cyan("🌏 官网地址: ").Bold(), Yellow("https://monibuca.com"))
+		fmt.Println(Cyan("🔥 启动工程: ").Bold(), Yellow("https://github.com/langhuihui/monibuca"))
+		fmt.Println(Cyan("📄 文档地址: ").Bold(), Yellow("https://monibuca.com/docs/index.html"))
+		fmt.Println(Cyan("🎞 视频教程: ").Bold(), Yellow("https://space.bilibili.com/328443019/channel/collectiondetail?sid=514619"))
+		fmt.Println(Cyan("🖥 远程界面: ").Bold(), Yellow("https://console.monibuca.com"))
+		fmt.Println(Yellow("关注公众号:不卡科技,获取更多信息"))
+	} else {
+		fmt.Println(Cyan("🌏 WebSite: ").Bold(), Yellow("https://m7s.live"))
+		fmt.Println(Cyan("🔥 Github: ").Bold(), Yellow("https://github.com/langhuihui/monibuca"))
+		fmt.Println(Cyan("📄 Docs: ").Bold(), Yellow("https://docs.m7s.live"))
+		fmt.Println(Cyan("🎞 Videos: ").Bold(), Yellow("https://space.bilibili.com/328443019/channel/collectiondetail?sid=514619"))
+		fmt.Println(Cyan("🖥 Console: ").Bold(), Yellow("https://console.monibuca.com"))
+	}
+	rp := struct {
+		UUID     string `json:"uuid"`
+		Machine  string `json:"machine"`
+		Instance string `json:"instance"`
+		Version  string `json:"version"`
+		OS       string `json:"os"`
+		Arch     string `json:"arch"`
+	}{UUID, id, EngineConfig.GetInstanceId(), version, runtime.GOOS, runtime.GOARCH}
+	json.NewEncoder(contentBuf).Encode(&rp)
+	req.Body = io.NopCloser(contentBuf)
+	EngineConfig.OnEvent(ctx)
+	go func() {
+		var c http.Client
+		reportTimer := time.NewTimer(time.Minute)
+		c.Do(req)
+		for {
+			<-reportTimer.C
+			contentBuf.Reset()
+			contentBuf.WriteString(fmt.Sprintf(`{"uuid":"`+UUID+`","streams":%d}`, Streams.Len()))
+			req.Body = io.NopCloser(contentBuf)
+			c.Do(req)
+			reportTimer.Reset(time.Minute)
+		}
+	}()
+	for _, plugin := range enabledPlugins {
+		plugin.Config.OnEvent(EngineConfig) //引擎初始化完成后,通知插件
+	}
+	for {
+		select {
+		case event := <-EventBus:
+			ts := time.Now()
+			for _, plugin := range enabledPlugins {
+				ts := time.Now()
+				plugin.Config.OnEvent(event)
+				if cost := time.Since(ts); cost > time.Millisecond*100 {
+					plugin.Warn("event cost too much time", zap.String("event", fmt.Sprintf("%v", event)), zap.Duration("cost", cost))
+				}
+			}
+			EngineConfig.OnEvent(event)
+			if cost := time.Since(ts); cost > time.Millisecond*100 {
+				log.Warn("event cost too much time", zap.String("event", fmt.Sprintf("%v", event)), zap.Duration("cost", cost))
+			}
+		case <-ctx.Done():
+			return
+		}
+	}
+}

+ 192 - 0
m7s.live/engine/v4@v4.14.6/memory-ts.go

@@ -0,0 +1,192 @@
+package engine
+
+import (
+	"errors"
+	"fmt"
+	"io"
+	"net"
+
+	"m7s.live/engine/v4/codec"
+	"m7s.live/engine/v4/codec/mpegts"
+	"m7s.live/engine/v4/util"
+)
+
+type MemoryTs struct {
+	util.BytesPool
+	PMT util.Buffer
+	util.BLL
+}
+
+func (ts *MemoryTs) WritePMTPacket(audio codec.AudioCodecID, video codec.VideoCodecID) {
+	ts.PMT.Reset()
+	mpegts.WritePMTPacket(&ts.PMT, video, audio)
+}
+
+func (ts *MemoryTs) WriteTo(w io.Writer) (int64, error) {
+	w.Write(mpegts.DefaultPATPacket)
+	w.Write(ts.PMT)
+	return ts.BLL.WriteTo(w)
+}
+
+func (ts *MemoryTs) WritePESPacket(frame *mpegts.MpegtsPESFrame, packet mpegts.MpegTsPESPacket) (err error) {
+	if packet.Header.PacketStartCodePrefix != 0x000001 {
+		err = errors.New("packetStartCodePrefix != 0x000001")
+		return
+	}
+	pesHeadItem := ts.Get(32)
+	pesHeadItem.Value.Reset()
+	_, err = mpegts.WritePESHeader(&pesHeadItem.Value, packet.Header)
+	if err != nil {
+		return
+	}
+	pesBuffers := append(net.Buffers{pesHeadItem.Value}, packet.Buffers...)
+	defer pesHeadItem.Recycle()
+	pesPktLength := util.SizeOfBuffers(pesBuffers)
+	buffer := ts.Get((pesPktLength/mpegts.TS_PACKET_SIZE+1)*6 + pesPktLength)
+	bwTsHeader := &buffer.Value
+	bigLen := bwTsHeader.Len()
+	bwTsHeader.Reset()
+	ts.BLL.Push(buffer)
+	var tsHeaderLength int
+	for i := 0; len(pesBuffers) > 0; i++ {
+		if bigLen < mpegts.TS_PACKET_SIZE {
+			if i == 0 {
+				ts.Recycle()
+			}
+			headerItem := ts.Get(mpegts.TS_PACKET_SIZE)
+			ts.BLL.Push(headerItem)
+			bwTsHeader = &headerItem.Value
+			bwTsHeader.Reset()
+		}
+		bigLen -= mpegts.TS_PACKET_SIZE
+		pesPktLength = util.SizeOfBuffers(pesBuffers)
+		tsHeader := mpegts.MpegTsHeader{
+			SyncByte:                   0x47,
+			TransportErrorIndicator:    0,
+			PayloadUnitStartIndicator:  0,
+			TransportPriority:          0,
+			Pid:                        frame.Pid,
+			TransportScramblingControl: 0,
+			AdaptionFieldControl:       1,
+			ContinuityCounter:          frame.ContinuityCounter,
+		}
+
+		frame.ContinuityCounter++
+		frame.ContinuityCounter = frame.ContinuityCounter % 16
+
+		// 每一帧的开头,当含有pcr的时候,包含调整字段
+		if i == 0 {
+			tsHeader.PayloadUnitStartIndicator = 1
+
+			// 当PCRFlag为1的时候,包含调整字段
+			if frame.IsKeyFrame {
+				tsHeader.AdaptionFieldControl = 0x03
+				tsHeader.AdaptationFieldLength = 7
+				tsHeader.PCRFlag = 1
+				tsHeader.RandomAccessIndicator = 1
+				tsHeader.ProgramClockReferenceBase = frame.ProgramClockReferenceBase
+			}
+		}
+
+		// 每一帧的结尾,当不满足188个字节的时候,包含调整字段
+		if pesPktLength < mpegts.TS_PACKET_SIZE-4 {
+			var tsStuffingLength uint8
+
+			tsHeader.AdaptionFieldControl = 0x03
+			tsHeader.AdaptationFieldLength = uint8(mpegts.TS_PACKET_SIZE - 4 - 1 - pesPktLength)
+
+			// TODO:如果第一个TS包也是最后一个TS包,是不是需要考虑这个情况?
+			// MpegTsHeader最少占6个字节.(前4个走字节 + AdaptationFieldLength(1 byte) + 3个指示符5个标志位(1 byte))
+			if tsHeader.AdaptationFieldLength >= 1 {
+				tsStuffingLength = tsHeader.AdaptationFieldLength - 1
+			} else {
+				tsStuffingLength = 0
+			}
+			// error
+			tsHeaderLength, err = mpegts.WriteTsHeader(bwTsHeader, tsHeader)
+			if err != nil {
+				return
+			}
+			if tsStuffingLength > 0 {
+				if _, err = bwTsHeader.Write(mpegts.Stuffing[:tsStuffingLength]); err != nil {
+					return
+				}
+			}
+			tsHeaderLength += int(tsStuffingLength)
+		} else {
+
+			tsHeaderLength, err = mpegts.WriteTsHeader(bwTsHeader, tsHeader)
+			if err != nil {
+				return
+			}
+		}
+
+		tsPayloadLength := mpegts.TS_PACKET_SIZE - tsHeaderLength
+
+		//fmt.Println("tsPayloadLength :", tsPayloadLength)
+
+		// 这里不断的减少PES包
+		io.CopyN(bwTsHeader, &pesBuffers, int64(tsPayloadLength))
+		// tmp := tsHeaderByte[3] << 2
+		// tmp = tmp >> 6
+		// if tmp == 2 {
+		// 	fmt.Println("fuck you mother.")
+		// }
+		tsPktByteLen := bwTsHeader.Len()
+
+		if tsPktByteLen != (i+1)*mpegts.TS_PACKET_SIZE && tsPktByteLen != mpegts.TS_PACKET_SIZE {
+			err = errors.New(fmt.Sprintf("%s, packet size=%d", "TS_PACKET_SIZE != 188,", tsPktByteLen))
+			return
+		}
+	}
+
+	return nil
+}
+
+func (ts *MemoryTs) WriteAudioFrame(frame AudioFrame, pes *mpegts.MpegtsPESFrame) (err error) {
+	// packetLength = 原始音频流长度 + adts(7) + MpegTsOptionalPESHeader长度(8 bytes, 因为只含有pts)
+	var packet mpegts.MpegTsPESPacket
+	if frame.CodecID == codec.CodecID_AAC {
+		packet.Header.PesPacketLength = uint16(7 + frame.AUList.ByteLength + 8)
+		packet.Buffers = frame.GetADTS()
+	} else {
+		packet.Header.PesPacketLength = uint16(frame.AUList.ByteLength + 8)
+		packet.Buffers = frame.AUList.ToBuffers()
+	}
+	packet.Header.PacketStartCodePrefix = 0x000001
+	packet.Header.ConstTen = 0x80
+	packet.Header.StreamID = mpegts.STREAM_ID_AUDIO
+	packet.Header.Pts = uint64(frame.PTS)
+	pes.ProgramClockReferenceBase = packet.Header.Pts
+	packet.Header.PtsDtsFlags = 0x80
+	packet.Header.PesHeaderDataLength = 5
+	return ts.WritePESPacket(pes, packet)
+}
+
+func (ts *MemoryTs) WriteVideoFrame(frame VideoFrame, pes *mpegts.MpegtsPESFrame) (err error) {
+	var buffer net.Buffers
+	//需要对原始数据(ES),进行一些预处理,视频需要分割nalu(H264编码),并且打上sps,pps,nalu_aud信息.
+	if len(frame.ParamaterSets) == 2 {
+		buffer = append(buffer, codec.NALU_AUD_BYTE)
+	} else {
+		buffer = append(buffer, codec.AudNalu)
+	}
+	buffer = append(buffer, frame.GetAnnexB()...)
+	pktLength := util.SizeOfBuffers(buffer) + 10 + 3
+	if pktLength > 0xffff {
+		pktLength = 0
+	}
+
+	var packet mpegts.MpegTsPESPacket
+	packet.Header.PacketStartCodePrefix = 0x000001
+	packet.Header.ConstTen = 0x80
+	packet.Header.StreamID = mpegts.STREAM_ID_VIDEO
+	packet.Header.PesPacketLength = uint16(pktLength)
+	packet.Header.Pts = uint64(frame.PTS)
+	pes.ProgramClockReferenceBase = packet.Header.Pts
+	packet.Header.Dts = uint64(frame.DTS)
+	packet.Header.PtsDtsFlags = 0xC0
+	packet.Header.PesHeaderDataLength = 10
+	packet.Buffers = buffer
+	return ts.WritePESPacket(pes, packet)
+}

+ 342 - 0
m7s.live/engine/v4@v4.14.6/plugin.go

@@ -0,0 +1,342 @@
+package engine
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"net/http"
+	"os"
+	"path/filepath"
+	"reflect"
+	"runtime"
+	"strings"
+	"sync"
+	"time"
+	"unsafe"
+
+	"github.com/mcuadros/go-defaults"
+	"go.uber.org/zap"
+	"gopkg.in/yaml.v3"
+	"m7s.live/engine/v4/config"
+	"m7s.live/engine/v4/log"
+	"m7s.live/engine/v4/util"
+)
+
+// InstallPlugin 安装插件,传入插件配置生成插件信息对象
+func InstallPlugin(config config.Plugin, options ...any) *Plugin {
+	defaults.SetDefaults(config)
+	t := reflect.TypeOf(config).Elem()
+	name := strings.TrimSuffix(t.Name(), "Config")
+	plugin := &Plugin{
+		Name:   name,
+		Config: config,
+	}
+	for _, v := range options {
+		switch v := v.(type) {
+		case DefaultYaml:
+			plugin.defaultYaml = v
+		case string:
+			name = v
+			plugin.Name = name
+		}
+	}
+	_, pluginFilePath, _, _ := runtime.Caller(1)
+	configDir := filepath.Dir(pluginFilePath)
+	if parts := strings.Split(configDir, "@"); len(parts) > 1 {
+		plugin.Version = util.LastElement(parts)
+	} else {
+		plugin.Version = pluginFilePath
+	}
+	if _, ok := Plugins[name]; ok {
+		return nil
+	}
+	switch v := config.(type) {
+	case *GlobalConfig:
+		v.InitDefaultHttp()
+	default:
+		Plugins[name] = plugin
+		plugins = append(plugins, plugin)
+	}
+	return plugin
+}
+
+type FirstConfig *config.Config
+type UpdateConfig *config.Config
+type DefaultYaml string
+
+// Plugin 插件信息
+type Plugin struct {
+	context.Context    `json:"-" yaml:"-"`
+	context.CancelFunc `json:"-" yaml:"-"`
+	Name               string        //插件名称
+	Config             config.Plugin `json:"-" yaml:"-"` //类型化的插件配置
+	Version            string        //插件版本
+	RawConfig          config.Config //最终合并后的配置的map形式方便查询
+	defaultYaml        DefaultYaml   //默认配置
+	*log.Logger        `json:"-" yaml:"-"`
+	saveTimer          *time.Timer //用于保存的时候的延迟,防抖
+	Disabled           bool
+}
+
+func (opt *Plugin) logHandler(pattern string, handler http.Handler) http.Handler {
+	return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+		opt.Debug("visit", zap.String("path", r.URL.String()), zap.String("remote", r.RemoteAddr))
+		name := strings.ToLower(opt.Name)
+		r.URL.Path = strings.TrimPrefix(r.URL.Path, "/"+name)
+		handler.ServeHTTP(rw, r)
+	})
+}
+func (opt *Plugin) handle(pattern string, handler http.Handler) {
+	if opt == nil {
+		return
+	}
+	conf, ok := opt.Config.(config.HTTPConfig)
+	if !strings.HasPrefix(pattern, "/") {
+		pattern = "/" + pattern
+	}
+	if ok {
+		opt.Debug("http handle added", zap.String("pattern", pattern))
+		conf.Handle(pattern, opt.logHandler(pattern, handler))
+	}
+	if opt != Engine {
+		pattern = "/" + strings.ToLower(opt.Name) + pattern
+		opt.Debug("http handle added to engine", zap.String("pattern", pattern))
+		EngineConfig.Handle(pattern, opt.logHandler(pattern, handler))
+	}
+	apiList = append(apiList, pattern)
+}
+
+// 读取独立配置合并入总配置中
+func (opt *Plugin) assign() {
+	f, err := os.Open(opt.settingPath())
+	defer f.Close()
+	if err == nil {
+		var modifyConfig map[string]any
+		err = yaml.NewDecoder(f).Decode(&modifyConfig)
+		if err != nil {
+			panic(err)
+		}
+		opt.RawConfig.ParseModifyFile(modifyConfig)
+	}
+	opt.registerHandler()
+	if opt != Engine {
+		opt.run()
+	}
+}
+
+func (opt *Plugin) run() {
+	opt.Context, opt.CancelFunc = context.WithCancel(Engine)
+	opt.Config.OnEvent(FirstConfig(&opt.RawConfig))
+	opt.Debug("config", zap.Any("config", opt.Config))
+	if conf, ok := opt.Config.(config.HTTPConfig); ok {
+		go conf.Listen(opt)
+	}
+	if conf, ok := opt.Config.(config.TCPConfig); ok {
+		go conf.ListenTCP(opt, opt.Config.(config.TCPPlugin))
+	}
+}
+
+// Update 热更新配置
+func (opt *Plugin) Update(conf *config.Config) {
+	opt.Config.OnEvent(UpdateConfig(conf))
+}
+
+func (opt *Plugin) registerHandler() {
+	t := reflect.TypeOf(opt.Config)
+	v := reflect.ValueOf(opt.Config)
+	// 注册http响应
+	for i, j := 0, t.NumMethod(); i < j; i++ {
+		name := t.Method(i).Name
+		if name == "ServeHTTP" {
+			continue
+		}
+		switch handler := v.Method(i).Interface().(type) {
+		case func(http.ResponseWriter, *http.Request):
+			patten := strings.ToLower(strings.ReplaceAll(name, "_", "/"))
+			opt.handle(patten, http.HandlerFunc(handler))
+		}
+	}
+	if rootHandler, ok := opt.Config.(http.Handler); ok {
+		opt.handle("/", rootHandler)
+	}
+}
+
+func (opt *Plugin) settingPath() string {
+	return filepath.Join(SettingDir, strings.ToLower(opt.Name)+".yaml")
+}
+
+func (opt *Plugin) Save() error {
+	if opt.saveTimer == nil {
+		var lock sync.Mutex
+		opt.saveTimer = time.AfterFunc(time.Second, func() {
+			lock.Lock()
+			defer lock.Unlock()
+			if opt.RawConfig.Modify == nil {
+				os.Remove(opt.settingPath())
+				return
+			}
+			file, err := os.OpenFile(opt.settingPath(), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
+			if err == nil {
+				defer file.Close()
+				err = yaml.NewEncoder(file).Encode(opt.RawConfig.Modify)
+			}
+			if err == nil {
+				opt.Info("config saved")
+			}
+		})
+	} else {
+		opt.saveTimer.Reset(time.Second)
+	}
+	return nil
+}
+
+func (opt *Plugin) AssignPubConfig(puber *Publisher) {
+	if puber.Config == nil {
+		conf, ok := opt.Config.(config.PublishConfig)
+		if !ok {
+			conf = EngineConfig
+		}
+		copyConfig := conf.GetPublishConfig()
+		puber.Config = &copyConfig
+	}
+}
+
+func (opt *Plugin) Publish(streamPath string, pub IPublisher) error {
+	puber := pub.GetPublisher()
+	if puber == nil {
+		if EngineConfig.LogLang == "zh" {
+			return errors.New("不是发布者")
+		} else {
+			return errors.New("not publisher")
+		}
+	}
+	opt.AssignPubConfig(puber)
+	return pub.Publish(streamPath, pub)
+}
+
+var ErrStreamNotExist = errors.New("stream not exist")
+
+// SubscribeExist 订阅已经存在的流
+func (opt *Plugin) SubscribeExist(streamPath string, sub ISubscriber) error {
+	opt.Info("subscribe exsit", zap.String("path", streamPath))
+	path, _, _ := strings.Cut(streamPath, "?")
+	if !Streams.Has(path) {
+		opt.Warn("stream not exist", zap.String("path", streamPath))
+		return ErrStreamNotExist
+	}
+	return opt.Subscribe(streamPath, sub)
+}
+func (opt *Plugin) AssignSubConfig(suber *Subscriber) {
+	if suber.Config == nil {
+		conf, ok := opt.Config.(config.SubscribeConfig)
+		if !ok {
+			conf = EngineConfig
+		}
+		copyConfig := *conf.GetSubscribeConfig()
+		suber.Config = &copyConfig
+	}
+	if suber.ID == "" {
+		suber.ID = fmt.Sprintf("%d", uintptr(unsafe.Pointer(suber)))
+	}
+}
+
+// Subscribe 订阅一个流,如果流不存在则创建一个等待流
+func (opt *Plugin) Subscribe(streamPath string, sub ISubscriber) error {
+	suber := sub.GetSubscriber()
+	if suber == nil {
+		if EngineConfig.LogLang == "zh" {
+			return errors.New("不是订阅者")
+		} else {
+			return errors.New("not subscriber")
+		}
+	}
+	opt.AssignSubConfig(suber)
+	return sub.Subscribe(streamPath, sub)
+}
+
+// SubscribeBlock 阻塞订阅一个流,直到订阅结束
+func (opt *Plugin) SubscribeBlock(streamPath string, sub ISubscriber, t byte) (err error) {
+	if err = opt.Subscribe(streamPath, sub); err == nil {
+		sub.PlayBlock(t)
+	}
+	return
+}
+
+var ErrNoPullConfig = errors.New("no pull config")
+var Pullers sync.Map
+
+func (opt *Plugin) Pull(streamPath string, url string, puller IPuller, save int) (err error) {
+	conf, ok := opt.Config.(config.PullConfig)
+	if !ok {
+		return ErrNoPullConfig
+	}
+	pullConf := conf.GetPullConfig()
+	if save < 2 {
+		zurl := zap.String("url", url)
+		zpath := zap.String("stream", streamPath)
+		opt.Info("pull", zpath, zurl)
+		puller.init(streamPath, url, pullConf)
+		opt.AssignPubConfig(puller.GetPublisher())
+		puller.SetLogger(opt.Logger.With(zpath, zurl))
+		go puller.startPull(puller)
+	}
+	switch save {
+	case 1:
+		pullConf.PullOnStartLocker.Lock()
+		defer pullConf.PullOnStartLocker.Unlock()
+		m := map[string]string{streamPath: url}
+		opt.RawConfig.ParseModifyFile(map[string]any{
+			"pull": map[string]any{
+				"pullonstart": m,
+			},
+		})
+	case 2:
+		pullConf.PullOnSubLocker.Lock()
+		defer pullConf.PullOnSubLocker.Unlock()
+		m := map[string]string{streamPath: url}
+		for id := range pullConf.PullOnSub {
+			m[id] = pullConf.PullOnSub[id]
+		}
+		opt.RawConfig.ParseModifyFile(map[string]any{
+			"pull": map[string]any{
+				"pullonsub": m,
+			},
+		})
+	}
+	if save > 0 {
+		if err = opt.Save(); err != nil {
+			opt.Error("save faild", zap.Error(err))
+		}
+	}
+	return
+}
+
+var ErrNoPushConfig = errors.New("no push config")
+var Pushers sync.Map
+
+func (opt *Plugin) Push(streamPath string, url string, pusher IPusher, save bool) (err error) {
+	zp, zu := zap.String("stream", streamPath), zap.String("url", url)
+	opt.Info("push", zp, zu)
+	defer func() {
+		if err != nil {
+			opt.Error("push faild", zap.Error(err))
+		}
+	}()
+	conf, ok := opt.Config.(config.PushConfig)
+	if !ok {
+		return ErrNoPushConfig
+	}
+	pushConfig := conf.GetPushConfig()
+	pusher.init(streamPath, url, pushConfig)
+	pusher.SetLogger(opt.Logger.With(zp, zu))
+	opt.AssignSubConfig(pusher.GetSubscriber())
+	go pusher.startPush(pusher)
+	if save {
+		pushConfig.AddPush(url, streamPath)
+		opt.RawConfig.Get("push").Get("pushlist").Modify = pushConfig.PushList
+		if err = opt.Save(); err != nil {
+			opt.Error("save faild", zap.Error(err))
+		}
+	}
+	return
+}

+ 62 - 0
m7s.live/engine/v4@v4.14.6/publisher-mp4.go

@@ -0,0 +1,62 @@
+package engine
+
+import (
+	"io"
+
+	"github.com/yapingcat/gomedia/go-mp4"
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/track"
+	"m7s.live/engine/v4/util"
+)
+
+type MP4Publisher struct {
+	Publisher
+	*mp4.MovDemuxer `json:"-" yaml:"-"`
+}
+
+// Start reading the MP4 file
+func (p *MP4Publisher) ReadMP4Data(source io.ReadSeeker) error {
+	defer p.Stop()
+	p.MovDemuxer = mp4.CreateMp4Demuxer(source)
+	if tracks, err := p.ReadHead(); err != nil {
+		if err == io.EOF {
+			p.Info("Reached end of MP4 file")
+			return nil
+		}
+		p.Error("Error reading MP4 header", zap.Error(err))
+		return err
+	} else {
+		info := p.GetMp4Info()
+		p.Info("MP4 info", zap.Any("info", info))
+		for _, t := range tracks {
+			p.Info("MP4 track", zap.Any("track", t))
+			switch t.Cid {
+			case mp4.MP4_CODEC_H264:
+				p.VideoTrack = track.NewH264(p.Stream)
+			case mp4.MP4_CODEC_H265:
+				p.VideoTrack = track.NewH265(p.Stream)
+			case mp4.MP4_CODEC_AAC:
+				p.AudioTrack = track.NewAAC(p.Stream)
+			case mp4.MP4_CODEC_G711A:
+				p.AudioTrack = track.NewG711(p.Stream, true)
+			case mp4.MP4_CODEC_G711U:
+				p.AudioTrack = track.NewG711(p.Stream, false)
+			}
+		}
+		for {
+			pkg, err := p.ReadPacket()
+			if err != nil {
+				p.Error("Error reading MP4 packet", zap.Error(err))
+				return err
+			}
+			switch pkg.Cid {
+			case mp4.MP4_CODEC_H264, mp4.MP4_CODEC_H265:
+				p.VideoTrack.WriteAnnexB(uint32(pkg.Pts*90), uint32(pkg.Dts*90), pkg.Data)
+			case mp4.MP4_CODEC_AAC:
+				p.AudioTrack.WriteADTS(uint32(pkg.Pts*90), util.Buffer(pkg.Data))
+			case mp4.MP4_CODEC_G711A, mp4.MP4_CODEC_G711U:
+				p.AudioTrack.WriteRawBytes(uint32(pkg.Pts*90), util.Buffer(pkg.Data))
+			}
+		}
+	}
+}

+ 108 - 0
m7s.live/engine/v4@v4.14.6/publisher-rtpdump.go

@@ -0,0 +1,108 @@
+package engine
+
+import (
+	"os"
+	"sync"
+	"time"
+
+	"github.com/bluenviron/mediacommon/pkg/codecs/mpeg4audio"
+	"github.com/pion/webrtc/v3/pkg/media/rtpdump"
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/codec"
+	"m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/track"
+	"m7s.live/engine/v4/util"
+)
+
+type RTPDumpPublisher struct {
+	Publisher
+	VCodec       codec.VideoCodecID
+	ACodec       codec.AudioCodecID
+	VPayloadType uint8
+	APayloadType uint8
+	other        rtpdump.Packet
+	sync.Mutex
+}
+
+func (t *RTPDumpPublisher) Feed(file *os.File) {
+
+	r, h, err := rtpdump.NewReader(file)
+	if err != nil {
+		t.Stream.Error("RTPDumpPublisher open file error", zap.Error(err))
+		return
+	}
+	t.Lock()
+	t.Stream.Info("RTPDumpPublisher open file success", zap.String("file", file.Name()), zap.String("start", h.Start.String()), zap.String("source", h.Source.String()), zap.Uint16("port", h.Port))
+	if t.VideoTrack == nil {
+		switch t.VCodec {
+		case codec.CodecID_H264:
+			t.VideoTrack = track.NewH264(t.Publisher.Stream, t.VPayloadType)
+		case codec.CodecID_H265:
+			t.VideoTrack = track.NewH265(t.Publisher.Stream, t.VPayloadType)
+		}
+		if t.VideoTrack != nil {
+			t.VideoTrack.SetSpeedLimit(500 * time.Millisecond)
+		}
+	}
+	if t.AudioTrack == nil {
+		switch t.ACodec {
+		case codec.CodecID_AAC:
+			at := track.NewAAC(t.Publisher.Stream, t.APayloadType)
+			t.AudioTrack = at
+			var c mpeg4audio.Config
+			c.ChannelCount = 2
+			c.SampleRate = 48000
+			asc, _ := c.Marshal()
+			at.WriteSequenceHead(append([]byte{0xAF, 0x00}, asc...))
+		case codec.CodecID_PCMA:
+			t.AudioTrack = track.NewG711(t.Publisher.Stream, true, t.APayloadType)
+		case codec.CodecID_PCMU:
+			t.AudioTrack = track.NewG711(t.Publisher.Stream, false, t.APayloadType)
+		}
+		if t.AudioTrack != nil {
+			t.AudioTrack.SetSpeedLimit(500 * time.Millisecond)
+		}
+	}
+	t.Unlock()
+	needLock := true
+	for {
+		packet, err := r.Next()
+		if err != nil {
+			t.Stream.Error("RTPDumpPublisher read file error", zap.Error(err))
+			return
+		}
+		if packet.IsRTCP {
+			continue
+		}
+		if needLock {
+			t.Lock()
+		}
+		if t.other.Payload == nil {
+			t.other = packet
+			t.Unlock()
+			needLock = true
+			continue
+		}
+		if packet.Offset >= t.other.Offset {
+			t.WriteRTP(t.other.Payload)
+			t.other = packet
+			t.Unlock()
+			needLock = true
+			continue
+		}
+		needLock = false
+		t.WriteRTP(packet.Payload)
+	}
+}
+func (t *RTPDumpPublisher) WriteRTP(raw []byte) {
+	var frame common.RTPFrame
+	frame.Unmarshal(raw)
+	switch frame.PayloadType {
+	case t.VPayloadType:
+		t.VideoTrack.WriteRTP(&util.ListItem[common.RTPFrame]{Value: frame})
+	case t.APayloadType:
+		t.AudioTrack.WriteRTP(&util.ListItem[common.RTPFrame]{Value: frame})
+	default:
+		t.Stream.Warn("RTPDumpPublisher unknown payload type", zap.Uint8("payloadType", frame.PayloadType))
+	}
+}

+ 111 - 0
m7s.live/engine/v4@v4.14.6/publisher-ts.go

@@ -0,0 +1,111 @@
+package engine
+
+import (
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/codec/mpegts"
+	"m7s.live/engine/v4/track"
+	"m7s.live/engine/v4/util"
+)
+
+type TSReader struct {
+	*TSPublisher
+	mpegts.MpegTsStream
+}
+
+func NewTSReader(pub *TSPublisher) (r *TSReader) {
+	r = &TSReader{
+		TSPublisher: pub,
+	}
+	r.PESChan = make(chan *mpegts.MpegTsPESPacket, 50)
+	r.PESBuffer = make(map[uint16]*mpegts.MpegTsPESPacket)
+	go r.ReadPES()
+	return
+}
+
+type TSPublisher struct {
+	Publisher
+	pool util.BytesPool
+}
+
+func (t *TSPublisher) OnEvent(event any) {
+	switch v := event.(type) {
+	case IPublisher:
+		t.pool = make(util.BytesPool, 17)
+		if !t.Equal(v) {
+			t.AudioTrack = v.getAudioTrack()
+			t.VideoTrack = v.getVideoTrack()
+		}
+	case SEKick, SEclose:
+		// close(t.PESChan)
+		t.Publisher.OnEvent(event)
+	default:
+		t.Publisher.OnEvent(event)
+	}
+}
+
+func (t *TSPublisher) OnPmtStream(s mpegts.MpegTsPmtStream) {
+	switch s.StreamType {
+	case mpegts.STREAM_TYPE_H264:
+		if t.VideoTrack == nil {
+			t.VideoTrack = track.NewH264(t.Publisher.Stream, t.pool)
+		}
+	case mpegts.STREAM_TYPE_H265:
+		if t.VideoTrack == nil {
+			t.VideoTrack = track.NewH265(t.Publisher.Stream, t.pool)
+		}
+	case mpegts.STREAM_TYPE_AAC:
+		if t.AudioTrack == nil {
+			t.AudioTrack = track.NewAAC(t.Publisher.Stream, t.pool)
+		}
+	case mpegts.STREAM_TYPE_G711A:
+		if t.AudioTrack == nil {
+			t.AudioTrack = track.NewG711(t.Publisher.Stream, true, t.pool)
+		}
+	case mpegts.STREAM_TYPE_G711U:
+		if t.AudioTrack == nil {
+			t.AudioTrack = track.NewG711(t.Publisher.Stream, false, t.pool)
+		}
+	default:
+		t.Warn("unsupport stream type:", zap.Uint8("type", s.StreamType))
+	}
+}
+
+func (t *TSReader) Close() {
+	close(t.PESChan)
+}
+
+func (t *TSReader) ReadPES() {
+	for pes := range t.PESChan {
+		if t.Err() != nil {
+			continue
+		}
+		if pes.Header.Dts == 0 {
+			pes.Header.Dts = pes.Header.Pts
+		}
+		switch pes.Header.StreamID & 0xF0 {
+		case mpegts.STREAM_ID_VIDEO:
+			if t.VideoTrack == nil {
+				for _, s := range t.PMT.Stream {
+					t.OnPmtStream(s)
+				}
+			}
+			if t.VideoTrack != nil {
+				t.WriteAnnexB(uint32(pes.Header.Pts), uint32(pes.Header.Dts), pes.Payload)
+			}
+		default:
+			if t.AudioTrack == nil {
+				for _, s := range t.PMT.Stream {
+					t.OnPmtStream(s)
+				}
+			}
+			if t.AudioTrack != nil {
+				switch t.AudioTrack.(type) {
+				case *track.AAC:
+					t.AudioTrack.WriteADTS(uint32(pes.Header.Pts), pes.Payload)
+				case *track.G711:
+					t.AudioTrack.WriteRawBytes(uint32(pes.Header.Pts), pes.Payload)
+				}
+			}
+		}
+	}
+}

+ 136 - 0
m7s.live/engine/v4@v4.14.6/publisher.go

@@ -0,0 +1,136 @@
+package engine
+
+import (
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/codec"
+	"m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/config"
+	"m7s.live/engine/v4/track"
+	"m7s.live/engine/v4/util"
+)
+
+type IPublisher interface {
+	IIO
+	GetPublisher() *Publisher
+	getAudioTrack() common.AudioTrack
+	getVideoTrack() common.VideoTrack
+	Publish(streamPath string, pub IPublisher) error
+}
+
+var _ IPublisher = (*Publisher)(nil)
+
+type Publisher struct {
+	IO
+	Config            *config.Publish
+	common.AudioTrack `json:"-" yaml:"-"`
+	common.VideoTrack `json:"-" yaml:"-"`
+}
+
+func (p *Publisher) Publish(streamPath string, pub IPublisher) error {
+	return p.receive(streamPath, pub)
+}
+
+func (p *Publisher) GetPublisher() *Publisher {
+	return p
+}
+
+// func (p *Publisher) Stop(reason ...zapcore.Field) {
+// 	p.IO.Stop(reason...)
+// 	p.Stream.Receive(ACTION_PUBLISHCLOSE)
+// }
+
+func (p *Publisher) getAudioTrack() common.AudioTrack {
+	return p.AudioTrack
+}
+func (p *Publisher) getVideoTrack() common.VideoTrack {
+	return p.VideoTrack
+}
+func (p *Publisher) Equal(p2 IPublisher) bool {
+	return p == p2.GetPublisher()
+}
+
+// func (p *Publisher) OnEvent(event any) {
+// 	p.IO.OnEvent(event)
+// 	switch event.(type) {
+// 	case SEclose, SEKick:
+// 		p.AudioTrack = nil
+// 		p.VideoTrack = nil
+// 	}
+// }
+
+func (p *Publisher) WriteAVCCVideo(ts uint32, frame *util.BLL, pool util.BytesPool) {
+	if frame.ByteLength < 6 {
+		return
+	}
+	if p.VideoTrack == nil {
+		b0 := frame.GetByte(0)
+		// https://github.com/veovera/enhanced-rtmp/blob/main/enhanced-rtmp-v1.pdf
+		if isExtHeader := b0 & 0b1000_0000; isExtHeader != 0 {
+			fourCC := frame.GetUintN(1, 4)
+			switch fourCC {
+			case codec.FourCC_H265_32:
+				p.VideoTrack = track.NewH265(p.Stream, pool)
+				p.VideoTrack.WriteAVCC(ts, frame)
+			case codec.FourCC_AV1_32:
+				p.VideoTrack = track.NewAV1(p.Stream, pool)
+				p.VideoTrack.WriteAVCC(ts, frame)
+			}
+		} else {
+			if frame.GetByte(1) == 0 {
+				ts = 0
+				switch codecID := codec.VideoCodecID(b0 & 0x0F); codecID {
+				case codec.CodecID_H264:
+					p.VideoTrack = track.NewH264(p.Stream, pool)
+				case codec.CodecID_H265:
+					p.VideoTrack = track.NewH265(p.Stream, pool)
+				default:
+					p.Stream.Error("video codecID not support", zap.Uint8("codeId", uint8(codecID)))
+					return
+				}
+				p.VideoTrack.WriteAVCC(ts, frame)
+			} else {
+				p.Stream.Warn("need sequence frame")
+			}
+		}
+	} else {
+		p.VideoTrack.WriteAVCC(ts, frame)
+	}
+}
+
+func (p *Publisher) WriteAVCCAudio(ts uint32, frame *util.BLL, pool util.BytesPool) {
+	if frame.ByteLength < 4 {
+		return
+	}
+	if p.AudioTrack == nil {
+		b0 := frame.GetByte(0)
+		switch codecID := codec.AudioCodecID(b0 >> 4); codecID {
+		case codec.CodecID_AAC:
+			if frame.GetByte(1) != 0 {
+				return
+			}
+			a := track.NewAAC(p.Stream, pool)
+			p.AudioTrack = a
+			a.AVCCHead = []byte{frame.GetByte(0), 1}
+			a.WriteAVCC(0, frame)
+		case codec.CodecID_PCMA,
+			codec.CodecID_PCMU:
+			alaw := true
+			if codecID == codec.CodecID_PCMU {
+				alaw = false
+			}
+			a := track.NewG711(p.Stream, alaw, pool)
+			p.AudioTrack = a
+			a.Audio.SampleRate = uint32(codec.SoundRate[(b0&0x0c)>>2])
+			if b0&0x02 == 0 {
+				a.Audio.SampleSize = 8
+			}
+			a.Channels = b0&0x01 + 1
+			a.AVCCHead = []byte{b0}
+			a.WriteAVCC(ts, frame)
+		default:
+			p.Stream.Error("audio codec not support yet", zap.Uint8("codecId", uint8(codecID)))
+		}
+	} else {
+		p.AudioTrack.WriteAVCC(ts, frame)
+	}
+}

+ 109 - 0
m7s.live/engine/v4@v4.14.6/puller.go

@@ -0,0 +1,109 @@
+package engine
+
+import (
+	"io"
+	"strings"
+	"time"
+
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/config"
+)
+
+var zshutdown = zap.String("reason", "shutdown")
+var znomorereconnect = zap.String("reason", "no more reconnect")
+
+type IPuller interface {
+	IPublisher
+	Connect() error
+	OnConnected()
+	Disconnect()
+	Pull() error
+	Reconnect() bool
+	init(streamPath string, url string, conf *config.Pull)
+	startPull(IPuller)
+}
+
+// 用于远程拉流的发布者
+type Puller struct {
+	ClientIO[config.Pull]
+}
+
+func (pub *Puller) OnConnected() {
+	pub.ReConnectCount = 0 // 重置重连次数
+}
+
+// 是否需要重连
+func (pub *Puller) Reconnect() (ok bool) {
+	ok = pub.Config.RePull == -1 || pub.ReConnectCount <= pub.Config.RePull
+	pub.ReConnectCount++
+	return
+}
+
+func (pub *Puller) startPull(puller IPuller) {
+	badPuller := true
+	var stream *Stream
+	var err error
+	streamPath := pub.StreamPath
+	if i := strings.Index(streamPath, "?"); i >= 0 {
+		streamPath = streamPath[:i]
+	}
+	if oldPuller, loaded := Pullers.LoadOrStore(streamPath, puller); loaded {
+		pub := oldPuller.(IPuller).GetPublisher()
+		stream = pub.Stream
+		if stream != nil {
+			puller.Error("puller already exists", zap.Int8("streamState", int8(stream.State)))
+			if stream.State == STATE_CLOSED {
+				oldPuller.(IPuller).Stop(zap.String("reason", "dead puller"))
+			}
+		} else {
+			puller.Error("puller already exists", zap.Time("createAt", pub.StartTime))
+		}
+		return
+	}
+	defer func() {
+		Pullers.Delete(streamPath)
+		puller.Disconnect()
+		if stream != nil {
+			stream.Close()
+		}
+	}()
+	puber := puller.GetPublisher()
+	var startTime time.Time
+	for puller.Info("start pull"); puller.Reconnect(); puller.Warn("restart pull") {
+		if time.Since(startTime) < 5*time.Second {
+			time.Sleep(5 * time.Second)
+		}
+		startTime = time.Now()
+		if err = puller.Connect(); err != nil {
+			if err == io.EOF {
+				puller.Info("pull complete")
+				return
+			}
+			puller.Error("pull connect", zap.Error(err))
+			if badPuller {
+				return
+			}
+		} else {
+			if err = puller.Publish(pub.StreamPath, puller); err != nil {
+				puller.Error("pull publish", zap.Error(err))
+				return
+			}
+			if stream != puber.Stream {
+				// 老流中的音视频轨道不可再使用
+				puber.AudioTrack = nil
+				puber.VideoTrack = nil
+			}
+			stream = puber.Stream
+			badPuller = false
+			if err = puller.Pull(); err != nil && !puller.IsShutdown() {
+				puller.Error("pull interrupt", zap.Error(err))
+			}
+		}
+		if puller.IsShutdown() {
+			puller.Info("stop pull", zshutdown)
+			return
+		}
+		puller.Disconnect()
+	}
+	puller.Warn("stop pull", znomorereconnect)
+}

+ 79 - 0
m7s.live/engine/v4@v4.14.6/pusher.go

@@ -0,0 +1,79 @@
+package engine
+
+import (
+	"io"
+	"time"
+
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/config"
+)
+
+type IPusher interface {
+	ISubscriber
+	Push() error
+	Connect() error
+	Disconnect()
+	init(string, string, *config.Push)
+	Reconnect() bool
+	startPush(IPusher)
+}
+
+type Pusher struct {
+	ClientIO[config.Push]
+}
+
+// 是否需要重连
+func (pub *Pusher) Reconnect() (result bool) {
+	result = pub.Config.RePush == -1 || pub.ReConnectCount <= pub.Config.RePush
+	pub.ReConnectCount++
+	return
+}
+
+func (pub *Pusher) startPush(pusher IPusher) {
+	badPusher := true
+	var err error
+	key := pub.RemoteURL
+
+	if oldPusher, loaded := Pushers.LoadOrStore(key, pusher); loaded {
+		sub := oldPusher.(IPusher).GetSubscriber()
+		pusher.Error("pusher already exists", zap.Time("createAt", sub.StartTime))
+		return
+	}
+
+	defer Pushers.Delete(key)
+	defer pusher.Disconnect()
+	var startTime time.Time
+	for pusher.Info("start push"); pusher.Reconnect(); pusher.Warn("restart push") {
+		if time.Since(startTime) < 5*time.Second {
+			time.Sleep(5 * time.Second)
+		}
+		startTime = time.Now()
+		if err = pusher.Subscribe(pub.StreamPath, pusher); err != nil {
+			pusher.Error("push subscribe", zap.Error(err))
+		} else {
+			stream := pusher.GetSubscriber().Stream
+			if err = pusher.Connect(); err != nil {
+				if err == io.EOF {
+					pusher.Info("push complete")
+					return
+				}
+				pusher.Error("push connect", zap.Error(err))
+				time.Sleep(time.Second * 5)
+				stream.Receive(Unsubscribe(pusher)) // 通知stream移除订阅者
+				if badPusher {
+					return
+				}
+			} else if err = pusher.Push(); err != nil && !stream.IsClosed() {
+				pusher.Error("push", zap.Error(err))
+				pusher.Stop()
+			}
+			badPusher = false
+			if stream.IsClosed() {
+				pusher.Info("stop push closed")
+				return
+			}
+		}
+		pusher.Disconnect()
+	}
+	pusher.Warn("stop push stop reconnect")
+}

+ 720 - 0
m7s.live/engine/v4@v4.14.6/stream.go

@@ -0,0 +1,720 @@
+package engine
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"runtime"
+	"strings"
+	"sync"
+	"sync/atomic"
+	"time"
+	"unsafe"
+
+	. "github.com/logrusorgru/aurora/v4"
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/common"
+	. "m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/config"
+	"m7s.live/engine/v4/log"
+	"m7s.live/engine/v4/track"
+	"m7s.live/engine/v4/util"
+)
+
+type StreamState byte
+type StreamAction byte
+
+func (s StreamState) String() string {
+	return StateNames[s]
+}
+func (s StreamAction) String() string {
+	return ActionNames[s]
+}
+
+// 四状态机
+const (
+	STATE_WAITPUBLISH StreamState = iota // 等待发布者状态
+	STATE_WAITTRACK                      // 等待音视频轨道激活
+	STATE_PUBLISHING                     // 正在发布流状态
+	STATE_WAITCLOSE                      // 等待关闭状态(自动关闭延时开启)
+	STATE_CLOSED                         // 流已关闭,不可使用
+)
+
+const (
+	ACTION_PUBLISH        StreamAction = iota
+	ACTION_TRACKAVAILABLE              // 音视频轨道激活
+	ACTION_TIMEOUT                     // 发布流长时间没有数据/长时间没有发布者发布流/等待关闭时间到
+	ACTION_PUBLISHCLOSE                // 发布者关闭
+	ACTION_CLOSE                       // 主动关闭流
+	ACTION_LASTLEAVE                   // 最后一个订阅者离开
+	ACTION_FIRSTENTER                  // 第一个订阅者进入
+	ACTION_NOTRACK                     // 没有音视频轨道
+)
+
+var StateNames = [...]string{"⌛", "🟡", "🟢", "🟠", "🔴"}
+var ActionNames = [...]string{"publish", "track available", "timeout", "publish close", "close", "last leave", "first enter", "no tracks"}
+
+/*
+stateDiagram-v2
+    [*] --> ⌛等待发布者 : 创建
+    ⌛等待发布者 --> 🟡等待轨道 :发布
+    ⌛等待发布者 --> 🔴已关闭 :关闭
+    ⌛等待发布者 --> 🔴已关闭  :超时
+    ⌛等待发布者 --> 🔴已关闭  :最后订阅者离开
+		🟡等待轨道 --> 🟢正在发布 :轨道激活
+		🟡等待轨道 --> 🔴已关闭 :关闭
+		🟡等待轨道 --> 🔴已关闭 :超时
+		🟡等待轨道 --> 🔴已关闭 :最后订阅者离开
+    🟢正在发布 --> ⌛等待发布者: 发布者断开
+    🟢正在发布 --> 🟠等待关闭: 最后订阅者离开
+    🟢正在发布 --> 🔴已关闭  :关闭
+    🟠等待关闭 --> 🟢正在发布 :第一个订阅者进入
+    🟠等待关闭 --> 🔴已关闭  :关闭
+    🟠等待关闭 --> 🔴已关闭  :超时
+    🟠等待关闭 --> 🔴已关闭  :发布者断开
+*/
+
+var StreamFSM = [len(StateNames)]map[StreamAction]StreamState{
+	{
+		ACTION_PUBLISH:   STATE_WAITTRACK,
+		ACTION_TIMEOUT:   STATE_CLOSED,
+		ACTION_LASTLEAVE: STATE_CLOSED,
+		ACTION_CLOSE:     STATE_CLOSED,
+	},
+	{
+		ACTION_TRACKAVAILABLE: STATE_PUBLISHING,
+		ACTION_TIMEOUT:        STATE_CLOSED,
+		ACTION_LASTLEAVE:      STATE_WAITCLOSE,
+		ACTION_CLOSE:          STATE_CLOSED,
+	},
+	{
+		// ACTION_PUBLISHCLOSE: STATE_WAITPUBLISH,
+		ACTION_TIMEOUT:   STATE_WAITPUBLISH,
+		ACTION_LASTLEAVE: STATE_WAITCLOSE,
+		ACTION_CLOSE:     STATE_CLOSED,
+	},
+	{
+		// ACTION_PUBLISHCLOSE: STATE_CLOSED,
+		ACTION_TIMEOUT:    STATE_CLOSED,
+		ACTION_FIRSTENTER: STATE_PUBLISHING,
+		ACTION_CLOSE:      STATE_CLOSED,
+	},
+	{},
+}
+
+// Streams 所有的流集合
+var Streams util.Map[string, *Stream]
+
+func FilterStreams[T IPublisher]() (ss []*Stream) {
+	Streams.Range(func(_ string, s *Stream) {
+		if _, ok := s.Publisher.(T); ok {
+			ss = append(ss, s)
+		}
+	})
+	return
+}
+
+type StreamTimeoutConfig struct {
+	PublishTimeout    time.Duration //发布者无数据后超时
+	DelayCloseTimeout time.Duration //无订阅者后超时,必须先有一次订阅才会激活
+	IdleTimeout       time.Duration //无订阅者后超时,不需要订阅即可激活
+	PauseTimeout      time.Duration //暂停后超时
+	NeverTimeout      bool          // 永不超时
+}
+type Tracks struct {
+	sync.Map
+	MainVideo   *track.Video
+	MainAudio   *track.Audio
+	SEI         *track.Data[[]byte]
+	marshalLock sync.Mutex
+}
+
+func (tracks *Tracks) Range(f func(name string, t Track)) {
+	tracks.Map.Range(func(k, v any) bool {
+		f(k.(string), v.(Track))
+		return true
+	})
+}
+
+func (tracks *Tracks) Add(name string, t Track) bool {
+	switch v := t.(type) {
+	case *track.Video:
+		if tracks.MainVideo == nil {
+			tracks.MainVideo = v
+			tracks.SetIDR(v)
+		}
+		if tracks.SEI != nil {
+			v.SEIReader = &track.DataReader[[]byte]{}
+			v.SEIReader.Ring = tracks.SEI.Ring
+		}
+	case *track.Audio:
+		if tracks.MainAudio == nil {
+			tracks.MainAudio = v
+		}
+		if tracks.MainVideo != nil {
+			v.Narrow()
+		}
+	}
+	_, loaded := tracks.LoadOrStore(name, t)
+	return !loaded
+}
+
+func (tracks *Tracks) SetIDR(video Track) {
+	if video == tracks.MainVideo {
+		tracks.Range(func(_ string, t Track) {
+			if v, ok := t.(*track.Audio); ok {
+				v.Narrow()
+			}
+		})
+	}
+}
+
+func (tracks *Tracks) AddSEI(t byte, data []byte) bool {
+	if tracks.SEI != nil {
+		l := len(data)
+		var buffer util.Buffer
+		buffer.WriteByte(t)
+		for l >= 255 {
+			buffer.WriteByte(255)
+			l -= 255
+		}
+		buffer.WriteByte(byte(l))
+		buffer.Write(data)
+		buffer.WriteByte(0x80)
+		tracks.SEI.Push(buffer)
+		return true
+	}
+	return false
+}
+
+func (tracks *Tracks) MarshalJSON() ([]byte, error) {
+	var trackList []Track
+	tracks.marshalLock.Lock()
+	defer tracks.marshalLock.Unlock()
+	tracks.Range(func(_ string, t Track) {
+		t.SnapForJson()
+		trackList = append(trackList, t)
+	})
+	return json.Marshal(trackList)
+}
+
+var streamIdGen atomic.Uint32
+
+// Stream 流定义
+type Stream struct {
+	timeout    *time.Timer //当前状态的超时定时器
+	actionChan util.SafeChan[any]
+	ID         uint32 // 流ID
+	*log.Logger
+	StartTime time.Time //创建时间
+	StreamTimeoutConfig
+	Path        string
+	Publisher   IPublisher
+	State       StreamState
+	SEHistory   []StateEvent // 事件历史
+	Subscribers Subscribers  // 订阅者
+	Tracks      Tracks
+	AppName     string
+	StreamName  string
+	IsPause     bool // 是否处于暂停状态
+	pubLocker   sync.Mutex
+}
+type StreamSummay struct {
+	Path        string
+	State       StreamState
+	Subscribers int
+	Tracks      []string
+	StartTime   time.Time
+	Type        string
+	BPS         int
+}
+
+func (s *Stream) GetType() string {
+	if s.Publisher == nil {
+		return ""
+	}
+	return s.Publisher.GetPublisher().Type
+}
+
+func (s *Stream) GetStartTime() time.Time {
+	return s.StartTime
+}
+
+func (s *Stream) GetPublisherConfig() *config.Publish {
+	if s.Publisher == nil {
+		s.Error("GetPublisherConfig: Publisher is nil")
+		return nil
+	}
+	return s.Publisher.GetPublisher().Config
+}
+
+// Summary 返回流的简要信息
+func (s *Stream) Summary() (r StreamSummay) {
+	if s.Publisher != nil {
+		r.Type = s.Publisher.GetPublisher().Type
+	}
+	s.Tracks.Range(func(name string, t Track) {
+		r.BPS += t.GetBPS()
+		r.Tracks = append(r.Tracks, name)
+	})
+	r.Path = s.Path
+	r.State = s.State
+	r.Subscribers = s.Subscribers.Len()
+	r.StartTime = s.StartTime
+	return
+}
+
+func (s *Stream) SSRC() uint32 {
+	return uint32(uintptr(unsafe.Pointer(s)))
+}
+func (s *Stream) SetIDR(video Track) {
+	s.Tracks.SetIDR(video)
+}
+func findOrCreateStream(streamPath string, waitTimeout time.Duration) (s *Stream, created bool) {
+	p := strings.Split(streamPath, "/")
+	if len(p) < 2 {
+		log.Warn(Red("Stream Path Format Error:"), streamPath)
+		return nil, false
+	}
+	actual, loaded := Streams.LoadOrStore(streamPath, &Stream{
+		Path:       streamPath,
+		AppName:    p[0],
+		StreamName: strings.Join(p[1:], "/"),
+		StartTime:  time.Now(),
+		timeout:    time.NewTimer(waitTimeout),
+	})
+	if s := actual.(*Stream); loaded {
+		for s.Logger == nil {
+			runtime.Gosched()
+		}
+		s.Debug("found")
+		return s, false
+	} else {
+		s.ID = streamIdGen.Add(1)
+		s.Subscribers.Init()
+		s.actionChan.Init(10)
+		s.Logger = log.LocaleLogger.With(zap.String("stream", streamPath), zap.Uint32("id", s.ID))
+		s.Info("created")
+		go s.run()
+		return s, true
+	}
+}
+
+func (r *Stream) action(action StreamAction) (ok bool) {
+	var event StateEvent
+	event.Target = r
+	event.Action = action
+	event.From = r.State
+	event.Time = time.Now()
+	var next StreamState
+	if next, ok = event.Next(); ok {
+		r.State = next
+		r.SEHistory = append(r.SEHistory, event)
+		// 给Publisher状态变更的回调,方便进行远程拉流等操作
+		var stateEvent any
+		r.Info(Sprintf("%s%s%s", event.From.String(), Yellow("->"), next.String()), zap.String("action", action.String()))
+		switch next {
+		case STATE_WAITPUBLISH:
+			stateEvent = SEwaitPublish{event, r.Publisher}
+			waitTime := time.Duration(0)
+			if r.Publisher != nil {
+				waitTime = r.Publisher.GetPublisher().Config.WaitCloseTimeout
+				r.Tracks.Range(func(name string, t Track) {
+					t.SetStuff(TrackStateOffline)
+				})
+			}
+			r.Subscribers.OnPublisherLost(event)
+			if suber := r.Subscribers.Pick(); suber != nil {
+				r.Subscribers.Broadcast(stateEvent)
+				if waitTime == 0 {
+					waitTime = suber.GetSubscriber().Config.WaitTimeout
+				}
+			} else if waitTime == 0 {
+				waitTime = time.Millisecond * 10 //没有订阅者也没有配置发布者等待重连时间,默认10ms后关闭流
+			}
+			r.timeout.Reset(waitTime)
+			r.Debug("wait publisher", zap.Duration("wait timeout", waitTime))
+		case STATE_WAITTRACK:
+			if len(r.SEHistory) > 1 {
+				stateEvent = SErepublish{event}
+			} else {
+				stateEvent = SEpublish{event}
+			}
+			r.timeout.Reset(time.Second * 5) // 5秒心跳,检测track的存活度
+		case STATE_PUBLISHING:
+			stateEvent = SEtrackAvaliable{event}
+			r.Subscribers.SendInviteTrack(r)
+			r.Subscribers.Broadcast(stateEvent)
+			if puller, ok := r.Publisher.(IPuller); ok {
+				puller.OnConnected()
+			}
+			r.timeout.Reset(time.Second * 5) // 5秒心跳,检测track的存活度
+		case STATE_WAITCLOSE:
+			stateEvent = SEwaitClose{event}
+			if r.IdleTimeout > 0 {
+				r.timeout.Reset(r.IdleTimeout)
+			} else {
+				r.timeout.Reset(r.DelayCloseTimeout)
+			}
+		case STATE_CLOSED:
+			Streams.Delete(r.Path)
+			r.timeout.Stop()
+			stateEvent = SEclose{event}
+			r.Subscribers.Broadcast(stateEvent)
+			r.Tracks.Range(func(_ string, t Track) {
+				t.Dispose()
+			})
+			r.Subscribers.Dispose()
+			r.actionChan.Close()
+		}
+		if actionCoust := time.Since(event.Time); actionCoust > 100*time.Millisecond {
+			r.Warn("action timeout", zap.String("action", action.String()), zap.Duration("cost", actionCoust))
+		}
+		EventBus <- stateEvent
+		if actionCoust := time.Since(event.Time); actionCoust > 100*time.Millisecond {
+			r.Warn("action timeout after eventbus", zap.String("action", action.String()), zap.Duration("cost", actionCoust))
+		}
+		if r.Publisher != nil {
+			r.Publisher.OnEvent(stateEvent)
+			if actionCoust := time.Since(event.Time); actionCoust > 100*time.Millisecond {
+				r.Warn("action timeout after send to publisher", zap.String("action", action.String()), zap.Duration("cost", actionCoust))
+			}
+		}
+	} else {
+		r.Debug("wrong action", zap.String("action", action.String()))
+	}
+	return
+}
+
+func (r *Stream) IsShutdown() bool {
+	switch l := len(r.SEHistory); l {
+	case 0:
+		return false
+	case 1:
+		return r.SEHistory[0].Action == ACTION_CLOSE
+	default:
+		switch r.SEHistory[l-1].Action {
+		case ACTION_CLOSE:
+			return true
+		case ACTION_TIMEOUT:
+			return r.SEHistory[l-1].From == STATE_WAITCLOSE
+		}
+	}
+	return false
+}
+
+func (r *Stream) IsClosed() bool {
+	if r == nil {
+		return true
+	}
+	return r.State == STATE_CLOSED
+}
+
+func (r *Stream) Close() {
+	r.Receive(ACTION_CLOSE)
+}
+
+func (s *Stream) Receive(event any) bool {
+	if s.IsClosed() {
+		return false
+	}
+	return s.actionChan.Send(event)
+}
+
+func (s *Stream) onSuberClose(sub ISubscriber) {
+	s.Subscribers.Delete(sub)
+	if s.Publisher != nil {
+		s.Publisher.OnEvent(sub) // 通知Publisher有订阅者离开,在回调中可以去获取订阅者数量
+	}
+	if (s.DelayCloseTimeout > 0 || s.IdleTimeout > 0) && s.Subscribers.Len() == 0 {
+		s.action(ACTION_LASTLEAVE)
+	}
+}
+
+func (s *Stream) checkRunCost(timeStart time.Time, timeOutInfo zap.Field) {
+	if cost := time.Since(timeStart); cost > 100*time.Millisecond {
+		s.Warn("run timeout", timeOutInfo, zap.Duration("cost", cost))
+	}
+}
+
+// 流状态处理中枢,包括接收订阅发布指令等
+func (s *Stream) run() {
+	EventBus <- SEcreate{StreamEvent{Event[*Stream]{Target: s, Time: time.Now()}}}
+	pulseTicker := time.NewTicker(EngineConfig.PulseInterval)
+	defer pulseTicker.Stop()
+	var timeOutInfo zap.Field
+	var timeStart time.Time
+	for pulseSuber := make(map[ISubscriber]struct{}); ; s.checkRunCost(timeStart, timeOutInfo) {
+		select {
+		case <-pulseTicker.C:
+			timeStart = time.Now()
+			timeOutInfo = zap.String("type", "pulse")
+			for sub := range pulseSuber {
+				sub.OnEvent(PulseEvent{CreateEvent(struct{}{})})
+			}
+		case <-s.timeout.C:
+			timeStart = time.Now()
+			timeOutInfo = zap.String("state", s.State.String())
+			if s.State == STATE_PUBLISHING || s.State == STATE_WAITTRACK {
+				for sub := range s.Subscribers.internal {
+					if sub.IsClosed() {
+						delete(s.Subscribers.internal, sub)
+						s.Info("innersuber -1", zap.Int("remains", len(s.Subscribers.internal)))
+					}
+				}
+				for sub := range s.Subscribers.public {
+					if sub.IsClosed() {
+						s.onSuberClose(sub)
+					}
+				}
+				if !s.NeverTimeout {
+					lost := false
+					trackCount := 0
+					timeout := s.PublishTimeout
+					if s.IsPause {
+						timeout = s.PauseTimeout
+					}
+					s.Tracks.Range(func(name string, t Track) {
+						trackCount++
+						switch t.(type) {
+						case *track.Video, *track.Audio:
+							// track 超过一定时间没有更新数据了
+							if lastWriteTime := t.LastWriteTime(); !lastWriteTime.IsZero() && time.Since(lastWriteTime) > timeout {
+								s.Warn("track timeout", zap.String("name", name), zap.Time("last writetime", lastWriteTime), zap.Duration("timeout", timeout))
+								lost = true
+							}
+						}
+					})
+					if !lost {
+						if trackCount == 0 {
+							s.Warn("no tracks")
+							lost = true
+						} else if s.Publisher != nil && s.Publisher.IsClosed() {
+							s.Warn("publish is closed", zap.Error(context.Cause(s.Publisher.GetPublisher())), zap.String("ptr", fmt.Sprintf("%p", s.Publisher.GetPublisher().Context)))
+							lost = true
+						}
+					}
+					if lost {
+						s.action(ACTION_TIMEOUT)
+						continue
+					}
+					if s.IdleTimeout > 0 && s.Subscribers.Len() == 0 && time.Since(s.StartTime) > s.IdleTimeout {
+						s.action(ACTION_LASTLEAVE)
+						continue
+					}
+				}
+				if s.State == STATE_WAITTRACK {
+					s.action(ACTION_TRACKAVAILABLE)
+				} else {
+					s.Subscribers.AbortWait()
+					s.timeout.Reset(time.Second * 5)
+				}
+			} else {
+				s.Debug("timeout", timeOutInfo)
+				s.action(ACTION_TIMEOUT)
+			}
+		case action, ok := <-s.actionChan.C:
+			if !ok {
+				return
+			}
+			timeStart = time.Now()
+			switch v := action.(type) {
+			case SubPulse:
+				timeOutInfo = zap.String("action", "SubPulse")
+				pulseSuber[v] = struct{}{}
+			case *util.Promise[IPublisher]:
+				timeOutInfo = zap.String("action", "Publish")
+				if s.IsClosed() {
+					v.Reject(ErrStreamIsClosed)
+					break
+				}
+				puber := v.Value.GetPublisher()
+				conf := puber.Config
+				republish := s.Publisher == v.Value // 重复发布
+				if republish {
+					s.Info("republish")
+					s.Tracks.Range(func(name string, t Track) {
+						t.SetStuff(TrackStateOffline)
+					})
+				}
+				needKick := !republish && s.Publisher != nil && conf.KickExist // 需要踢掉老的发布者
+				if needKick {
+					oldPuber := s.Publisher.GetPublisher()
+					s.Warn("kick", zap.String("old type", oldPuber.Type))
+					// 接管老的发布者的音视频轨道
+					puber.AudioTrack = oldPuber.AudioTrack
+					puber.VideoTrack = oldPuber.VideoTrack
+					s.Publisher.OnEvent(SEKick{CreateEvent[struct{}](util.Null)})
+				}
+				s.Publisher = v.Value
+				s.PublishTimeout = conf.PublishTimeout
+				s.DelayCloseTimeout = conf.DelayCloseTimeout
+				s.IdleTimeout = conf.IdleTimeout
+				s.PauseTimeout = conf.PauseTimeout
+				if s.action(ACTION_PUBLISH) || republish || needKick {
+					if conf.InsertSEI {
+						if s.Tracks.SEI == nil {
+							s.Tracks.SEI = track.NewDataTrack[[]byte]("sei")
+							s.Tracks.SEI.Locker = &sync.Mutex{}
+							s.Tracks.SEI.SetStuff(s)
+							if s.Tracks.Add("sei", s.Tracks.SEI) {
+								s.Info("sei track added")
+							}
+						}
+					}
+					v.Resolve()
+				} else {
+					s.Warn("duplicate publish")
+					v.Reject(ErrDuplicatePublish)
+				}
+			case *util.Promise[ISubscriber]:
+				timeOutInfo = zap.String("action", "Subscribe")
+				if s.IsClosed() {
+					v.Reject(ErrStreamIsClosed)
+					break
+				}
+				suber := v.Value
+				io := suber.GetSubscriber()
+				sbConfig := io.Config
+				waits := &waitTracks{
+					Promise: v,
+				}
+				if ats := io.Args.Get(sbConfig.SubAudioArgName); ats != "" {
+					waits.audio.Wait(strings.Split(ats, ",")...)
+				} else if len(sbConfig.SubAudioTracks) > 0 {
+					waits.audio.Wait(sbConfig.SubAudioTracks...)
+				} else if sbConfig.SubAudio {
+					waits.audio.Wait()
+				}
+				if vts := io.Args.Get(sbConfig.SubVideoArgName); vts != "" {
+					waits.video.Wait(strings.Split(vts, ",")...)
+				} else if len(sbConfig.SubVideoTracks) > 0 {
+					waits.video.Wait(sbConfig.SubVideoTracks...)
+				} else if sbConfig.SubVideo {
+					waits.video.Wait()
+				}
+				if dts := io.Args.Get(sbConfig.SubDataArgName); dts != "" {
+					waits.data.Wait(strings.Split(dts, ",")...)
+				} else {
+					// waits.data.Wait()
+				}
+				if s.Publisher != nil {
+					s.Publisher.OnEvent(v) // 通知Publisher有新的订阅者加入,在回调中可以去获取订阅者数量
+					pubConfig := s.Publisher.GetPublisher().Config
+					s.Tracks.Range(func(name string, t Track) {
+						waits.Accept(t)
+					})
+					if !pubConfig.PubAudio {
+						waits.audio.StopWait()
+					} else if s.State == STATE_PUBLISHING && len(waits.audio) > 0 {
+						waits.audio.InviteTrack(suber)
+					} else if s.Subscribers.waitAborted {
+						waits.audio.StopWait()
+					}
+					if !pubConfig.PubVideo {
+						waits.video.StopWait()
+					} else if s.State == STATE_PUBLISHING && len(waits.video) > 0 {
+						waits.video.InviteTrack(suber)
+					} else if s.Subscribers.waitAborted {
+						waits.video.StopWait()
+					}
+				}
+				s.Subscribers.Add(suber, waits)
+				if s.Subscribers.Len() == 1 && s.State == STATE_WAITCLOSE {
+					s.action(ACTION_FIRSTENTER)
+				}
+			case Unsubscribe:
+				timeOutInfo = zap.String("action", "Unsubscribe")
+				delete(pulseSuber, v)
+				s.onSuberClose(v)
+			case TrackRemoved:
+				timeOutInfo = zap.String("action", "TrackRemoved")
+				if s.IsClosed() {
+					break
+				}
+				name := v.GetName()
+				if t, ok := s.Tracks.LoadAndDelete(name); ok {
+					s.Info("track -1", zap.String("name", name))
+					s.Subscribers.Broadcast(t)
+					t.(common.Track).Dispose()
+				}
+			case *util.Promise[Track]:
+				timeOutInfo = zap.String("action", "Track")
+				if s.IsClosed() {
+					v.Reject(ErrStreamIsClosed)
+					break
+				}
+				if s.State == STATE_WAITPUBLISH {
+					s.action(ACTION_PUBLISH)
+				}
+				pubConfig := s.GetPublisherConfig()
+				name := v.Value.GetName()
+				if _, ok := v.Value.(*track.Video); ok && !pubConfig.PubVideo {
+					v.Reject(ErrTrackMute)
+					continue
+				}
+				if _, ok := v.Value.(*track.Audio); ok && !pubConfig.PubAudio {
+					v.Reject(ErrTrackMute)
+					continue
+				}
+				if s.Tracks.Add(name, v.Value) {
+					v.Resolve()
+					s.Subscribers.OnTrack(v.Value)
+					if _, ok := v.Value.(*track.Video); ok && !pubConfig.PubAudio {
+						s.Subscribers.AbortWait()
+					}
+					if _, ok := v.Value.(*track.Audio); ok && !pubConfig.PubVideo {
+						s.Subscribers.AbortWait()
+					}
+					if (s.Tracks.MainVideo != nil || !pubConfig.PubVideo) && (!pubConfig.PubAudio || s.Tracks.MainAudio != nil) {
+						s.action(ACTION_TRACKAVAILABLE)
+					}
+				} else {
+					v.Reject(ErrBadTrackName)
+				}
+			case NoMoreTrack:
+				s.Subscribers.AbortWait()
+			case StreamAction:
+				timeOutInfo = zap.String("action", "StreamAction"+v.String())
+				s.action(v)
+			default:
+				timeOutInfo = zap.String("action", "unknown")
+				s.Error("unknown action", timeOutInfo)
+			}
+			if s.IsClosed() && s.actionChan.Close() { //再次尝试关闭
+				return
+			}
+		}
+	}
+}
+
+func (s *Stream) AddTrack(t Track) (promise *util.Promise[Track]) {
+	promise = util.NewPromise(t)
+	if !s.Receive(promise) {
+		promise.Reject(ErrStreamIsClosed)
+	}
+	return
+}
+
+func (s *Stream) RemoveTrack(t Track) {
+	s.Receive(TrackRemoved{t})
+}
+
+func (s *Stream) Pause() {
+	s.IsPause = true
+}
+
+func (s *Stream) Resume() {
+	s.IsPause = false
+}
+
+type TrackRemoved struct {
+	Track
+}
+
+type SubPulse struct {
+	ISubscriber
+}
+
+type Unsubscribe ISubscriber
+type NoMoreTrack struct{}

+ 422 - 0
m7s.live/engine/v4@v4.14.6/subscriber.go

@@ -0,0 +1,422 @@
+package engine
+
+import (
+	"bufio"
+	"context"
+	"io"
+	"net"
+	"strconv"
+	"time"
+
+	"go.uber.org/zap"
+	"go.uber.org/zap/zapcore"
+	"m7s.live/engine/v4/codec"
+	. "m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/config"
+	"m7s.live/engine/v4/track"
+	"m7s.live/engine/v4/util"
+)
+
+const (
+	SUBTYPE_RAW = iota
+	SUBTYPE_RTP
+	SUBTYPE_FLV
+)
+const (
+	SUBSTATE_INIT = iota
+	SUBSTATE_FIRST
+	SUBSTATE_NORMAL
+)
+
+// AVCC 格式的序列帧
+type VideoDeConf []byte
+
+// AVCC 格式的序列帧
+type AudioDeConf []byte
+type AudioFrame struct {
+	*AVFrame
+	*track.Audio
+	AbsTime uint32
+	PTS     uint32
+	DTS     uint32
+}
+type VideoFrame struct {
+	*AVFrame
+	*track.Video
+	AbsTime uint32
+	PTS     uint32
+	DTS     uint32
+}
+type FLVFrame net.Buffers
+type AudioRTP RTPFrame
+type VideoRTP RTPFrame
+type HasAnnexB interface {
+	GetAnnexB() (r net.Buffers)
+}
+
+func (a AudioDeConf) WithOutRTMP() []byte {
+	return a[2:]
+}
+
+func (v VideoDeConf) WithOutRTMP() []byte {
+	return v[5:]
+}
+func (f FLVFrame) IsAudio() bool {
+	return f[0][0] == codec.FLV_TAG_TYPE_AUDIO
+}
+func (f FLVFrame) IsVideo() bool {
+	return f[0][0] == codec.FLV_TAG_TYPE_VIDEO
+}
+func (f FLVFrame) WriteTo(w io.Writer) (int64, error) {
+	t := (net.Buffers)(f)
+	return t.WriteTo(w)
+}
+
+func (a AudioFrame) GetADTS() (r net.Buffers) {
+	r = append(append(r, a.ADTS.Value), a.AUList.ToBuffers()...)
+	return
+}
+
+func (a AudioFrame) WriteRawTo(w io.Writer) (n int64, err error) {
+	aulist := a.AUList.ToBuffers()
+	return aulist.WriteTo(w)
+}
+
+func (v VideoFrame) GetAnnexB() (r net.Buffers) {
+	if v.IFrame {
+		r = v.ParamaterSets.GetAnnexB()
+	}
+	v.AUList.Range(func(au *util.BLL) bool {
+		r = append(append(r, codec.NALU_Delimiter2), au.ToBuffers()...)
+		return true
+	})
+	return
+}
+
+func (v VideoFrame) WriteAnnexBTo(w io.Writer) (n int64, err error) {
+	annexB := v.GetAnnexB()
+	return annexB.WriteTo(w)
+}
+
+type ISubscriber interface {
+	IIO
+	GetSubscriber() *Subscriber
+	IsPlaying() bool
+	PlayRaw()
+	PlayBlock(byte)
+	PlayFLV()
+	Stop(reason ...zapcore.Field)
+	Subscribe(streamPath string, sub ISubscriber) error
+}
+
+type TrackPlayer struct {
+	context.Context
+	context.CancelFunc
+	AudioReader, VideoReader *track.AVRingReader
+	Audio                    *track.Audio
+	Video                    *track.Video
+}
+
+// Subscriber 订阅者实体定义
+type Subscriber struct {
+	IO
+	Config      *config.Subscribe
+	readers     []*track.AVRingReader
+	TrackPlayer `json:"-" yaml:"-"`
+}
+
+func (s *Subscriber) Subscribe(streamPath string, sub ISubscriber) error {
+	return s.receive(streamPath, sub)
+}
+
+func (s *Subscriber) GetSubscriber() *Subscriber {
+	return s
+}
+
+func (s *Subscriber) SetIO(i any) {
+	s.IO.SetIO(i)
+	if s.Writer != nil && s.Config != nil && s.Config.WriteBufferSize > 0 {
+		s.Writer = bufio.NewWriterSize(s.Writer, s.Config.WriteBufferSize)
+	}
+}
+func (s *Subscriber) OnEvent(event any) {
+	switch v := event.(type) {
+	case Track: //默认接受所有track
+		s.AddTrack(v)
+	default:
+		s.IO.OnEvent(event)
+	}
+}
+
+func (s *Subscriber) CreateTrackReader(t *track.Media) (result *track.AVRingReader) {
+	result = track.NewAVRingReader(t)
+	s.readers = append(s.readers, result)
+	result.Logger = s.With(zap.String("track", t.Name))
+	return
+}
+
+func (s *Subscriber) AddTrack(t Track) bool {
+	switch v := t.(type) {
+	case *track.Video:
+		if s.VideoReader != nil || !s.Config.SubVideo {
+			return false
+		}
+		s.VideoReader = s.CreateTrackReader(&v.Media)
+		s.Video = v
+	case *track.Audio:
+		if s.AudioReader != nil || !s.Config.SubAudio {
+			return false
+		}
+		s.AudioReader = s.CreateTrackReader(&v.Media)
+		s.Audio = v
+	default:
+		return false
+	}
+	s.Info("track+1", zap.String("name", t.GetName()))
+	return true
+}
+
+func (s *Subscriber) IsPlaying() bool {
+	return s.TrackPlayer.Context != nil && s.TrackPlayer.Err() == nil
+}
+
+func (s *Subscriber) SubPulse() {
+	s.Stream.Receive(SubPulse{s.Spesific.(ISubscriber)})
+}
+
+func (s *Subscriber) PlayRaw() {
+	s.PlayBlock(SUBTYPE_RAW)
+}
+
+func (s *Subscriber) PlayFLV() {
+	s.PlayBlock(SUBTYPE_FLV)
+}
+
+func (s *Subscriber) PlayRTP() {
+	s.PlayBlock(SUBTYPE_RTP)
+}
+
+// PlayBlock 阻塞式读取数据
+func (s *Subscriber) PlayBlock(subType byte) {
+	spesic := s.Spesific
+	if spesic == nil {
+		s.Error("play before subscribe")
+		return
+	}
+	if s.IO.Err() != nil {
+		s.Error("play", zap.Error(s.IO.Err()))
+		return
+	}
+	s.Info("playblock", zap.Uint8("subType", subType))
+	s.TrackPlayer.Context, s.TrackPlayer.CancelFunc = context.WithCancel(s.IO)
+	defer s.TrackPlayer.CancelFunc()
+	ctx := s.TrackPlayer.Context
+	conf := s.Config
+	hasVideo, hasAudio := s.Video != nil && conf.SubVideo, s.Audio != nil && conf.SubAudio
+	stopReason := zap.String("reason", "stop")
+	defer s.onStop(&stopReason)
+	if !hasAudio && !hasVideo {
+		stopReason = zap.String("reason", "play neither video nor audio")
+		return
+	}
+	sendVideoDecConf := func() {
+		// s.Debug("sendVideoDecConf")
+		spesic.OnEvent(s.Video.ParamaterSets)
+		spesic.OnEvent(VideoDeConf(s.VideoReader.Track.SequenceHead))
+	}
+	sendAudioDecConf := func() {
+		// s.Debug("sendAudioDecConf")
+		spesic.OnEvent(AudioDeConf(s.AudioReader.Track.SequenceHead))
+	}
+	var sendAudioFrame, sendVideoFrame func(*AVFrame)
+	switch subType {
+	case SUBTYPE_RAW:
+		sendVideoFrame = func(frame *AVFrame) {
+			// fmt.Println("v", frame.Sequence, s.VideoReader.AbsTime, s.VideoReader.Delay)
+			if frame.AUList.ByteLength == 0 {
+				return
+			}
+			spesic.OnEvent(VideoFrame{frame, s.Video, s.VideoReader.AbsTime, s.VideoReader.GetPTS32(), s.VideoReader.GetDTS32()})
+		}
+		sendAudioFrame = func(frame *AVFrame) {
+			if frame.AUList.ByteLength == 0 {
+				return
+			}
+			// fmt.Println("a", s.AudioReader.Delay)
+			// fmt.Println("a", frame.Sequence, s.AudioReader.AbsTime)
+			spesic.OnEvent(AudioFrame{frame, s.Audio, s.AudioReader.AbsTime, s.AudioReader.GetPTS32(), s.AudioReader.GetDTS32()})
+		}
+	case SUBTYPE_RTP:
+		var videoSeq, audioSeq uint16
+		sendVideoFrame = func(frame *AVFrame) {
+			// fmt.Println("v", frame.Sequence, frame.AbsTime, s.VideoReader.AbsTime, frame.IFrame)
+			delta := uint32(s.VideoReader.SkipTs * 90 / time.Millisecond)
+			frame.RTP.Range(func(vp RTPFrame) bool {
+				videoSeq++
+				copy := *vp.Packet
+				vp.Packet = &copy
+				vp.Header.Timestamp = vp.Header.Timestamp - delta
+				vp.Header.SequenceNumber = videoSeq
+				spesic.OnEvent((VideoRTP)(vp))
+				return true
+			})
+		}
+
+		sendAudioFrame = func(frame *AVFrame) {
+			// fmt.Println("a", frame.Sequence, frame.Timestamp, s.AudioReader.AbsTime)
+			delta := uint32(s.AudioReader.SkipTs / time.Millisecond * time.Duration(s.AudioReader.Track.SampleRate) / 1000)
+			frame.RTP.Range(func(ap RTPFrame) bool {
+				audioSeq++
+				copy := *ap.Packet
+				ap.Packet = &copy
+				ap.Header.SequenceNumber = audioSeq
+				ap.Header.Timestamp = ap.Header.Timestamp - delta
+				spesic.OnEvent((AudioRTP)(ap))
+				return true
+			})
+		}
+	case SUBTYPE_FLV:
+		flvHeadCache := make([]byte, 15) //内存复用
+		sendFlvFrame := func(t byte, ts uint32, avcc ...[]byte) {
+			// println(t, ts)
+			// fmt.Printf("%d %X %X %d\n", t, avcc[0][0], avcc[0][1], ts)
+			flvHeadCache[0] = t
+			result := append(FLVFrame{flvHeadCache[:11]}, avcc...)
+			dataSize := uint32(util.SizeOfBuffers(avcc))
+			if dataSize == 0 {
+				return
+			}
+			util.PutBE(flvHeadCache[1:4], dataSize)
+			util.PutBE(flvHeadCache[4:7], ts)
+			flvHeadCache[7] = byte(ts >> 24)
+			spesic.OnEvent(append(result, util.PutBE(flvHeadCache[11:15], dataSize+11)))
+		}
+		sendVideoDecConf = func() {
+			sendFlvFrame(codec.FLV_TAG_TYPE_VIDEO, s.VideoReader.AbsTime, s.VideoReader.Track.SequenceHead)
+		}
+		sendAudioDecConf = func() {
+			sendFlvFrame(codec.FLV_TAG_TYPE_AUDIO, s.AudioReader.AbsTime, s.AudioReader.Track.SequenceHead)
+		}
+		sendVideoFrame = func(frame *AVFrame) {
+			// fmt.Println(frame.Sequence, s.VideoReader.AbsTime, s.VideoReader.Delay, frame.IFrame)
+			// b := util.Buffer(frame.AVCC.ToBytes()[5:])
+			// for b.CanRead() {
+			// 	nalulen := int(b.ReadUint32())
+			// 	if b.CanReadN(nalulen) {
+			// 		bb := b.ReadN(int(nalulen))
+			// 		println(nalulen, codec.ParseH264NALUType(bb[0]))
+			// 	} else {
+			// 		println("error")
+			// 	}
+			// }
+			sendFlvFrame(codec.FLV_TAG_TYPE_VIDEO, s.VideoReader.AbsTime, frame.AVCC.ToBuffers()...)
+		}
+		sendAudioFrame = func(frame *AVFrame) {
+			// fmt.Println(frame.Sequence, s.AudioReader.AbsTime, s.AudioReader.Delay)
+			sendFlvFrame(codec.FLV_TAG_TYPE_AUDIO, s.AudioReader.AbsTime, frame.AVCC.ToBuffers()...)
+		}
+	}
+
+	var subMode = conf.SubMode //订阅模式
+	if s.Args.Has(conf.SubModeArgName) {
+		subMode, _ = strconv.Atoi(s.Args.Get(conf.SubModeArgName))
+	}
+	var initState = 0
+	var videoFrame, audioFrame *AVFrame
+	for ctx.Err() == nil {
+		if hasVideo {
+			for ctx.Err() == nil {
+				err := s.VideoReader.ReadFrame(subMode)
+				if err == nil {
+					err = ctx.Err()
+				}
+				if err != nil {
+					stopReason = zap.Error(err)
+					return
+				}
+				videoFrame = s.VideoReader.Value
+				// fmt.Println("video", s.VideoReader.Track.PreFrame().Sequence-frame.Sequence)
+				if videoFrame.IFrame && s.VideoReader.DecConfChanged() {
+					s.VideoReader.ConfSeq = s.VideoReader.Track.SequenceHeadSeq
+					sendVideoDecConf()
+				}
+				if hasAudio {
+					if audioFrame != nil {
+						if util.Conditoinal(conf.SyncMode == 0, videoFrame.Timestamp > audioFrame.Timestamp, videoFrame.WriteTime.After(audioFrame.WriteTime)) {
+							// fmt.Println("switch audio", audioFrame.CanRead)
+							sendAudioFrame(audioFrame)
+							audioFrame = nil
+							break
+						}
+					} else if initState++; initState >= 2 {
+						break
+					}
+				}
+
+				if !conf.IFrameOnly || videoFrame.IFrame {
+					sendVideoFrame(videoFrame)
+				} else {
+					// fmt.Println("skip video", frame.Sequence)
+				}
+			}
+		}
+		// 正常模式下或者纯音频模式下,音频开始播放
+		if hasAudio {
+			for ctx.Err() == nil {
+				switch s.AudioReader.State {
+				case track.READSTATE_INIT:
+					if s.Video != nil {
+						s.AudioReader.FirstTs = s.VideoReader.FirstTs
+
+					}
+				case track.READSTATE_NORMAL:
+					if s.Video != nil {
+						s.AudioReader.SkipTs = s.VideoReader.SkipTs
+					}
+				}
+				err := s.AudioReader.ReadFrame(subMode)
+				if err == nil {
+					err = ctx.Err()
+				}
+				if err != nil {
+					stopReason = zap.Error(err)
+					return
+				}
+				audioFrame = s.AudioReader.Value
+				// fmt.Println("audio", s.AudioReader.Track.PreFrame().Sequence-frame.Sequence)
+				if s.AudioReader.DecConfChanged() {
+					s.AudioReader.ConfSeq = s.AudioReader.Track.SequenceHeadSeq
+					sendAudioDecConf()
+				}
+				if hasVideo && videoFrame != nil {
+					if util.Conditoinal(conf.SyncMode == 0, audioFrame.Timestamp > videoFrame.Timestamp, audioFrame.WriteTime.After(videoFrame.WriteTime)) {
+						sendVideoFrame(videoFrame)
+						videoFrame = nil
+						break
+					}
+				}
+				if audioFrame.Timestamp >= s.AudioReader.SkipTs {
+					sendAudioFrame(audioFrame)
+				} else {
+					// fmt.Println("skip audio", frame.AbsTime, s.AudioReader.SkipTs)
+				}
+			}
+		}
+	}
+	if videoFrame != nil {
+		videoFrame.ReaderLeave()
+	}
+	if audioFrame != nil {
+		audioFrame.ReaderLeave()
+	}
+	stopReason = zap.Error(ctx.Err())
+}
+
+func (s *Subscriber) onStop(reason *zapcore.Field) {
+	if !s.Stream.IsClosed() {
+		s.Info("play stop", *reason)
+		if !s.Config.Internal {
+			s.Stream.Receive(s.Spesific)
+		}
+	}
+}

+ 170 - 0
m7s.live/engine/v4@v4.14.6/subscribers.go

@@ -0,0 +1,170 @@
+package engine
+
+import (
+	"encoding/json"
+
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/config"
+)
+
+type Subscribers struct {
+	public      map[ISubscriber]*waitTracks
+	internal    map[ISubscriber]*waitTracks
+	waits       map[*waitTracks]ISubscriber
+	waitAborted bool // 不再等待了
+}
+
+func (s *Subscribers) Init() {
+	s.public = make(map[ISubscriber]*waitTracks)
+	s.internal = make(map[ISubscriber]*waitTracks)
+	s.waits = make(map[*waitTracks]ISubscriber)
+}
+
+func (s *Subscribers) MarshalJSON() ([]byte, error) {
+	var subers []ISubscriber
+	for suber := range s.public {
+		subers = append(subers, suber)
+	}
+	return json.Marshal(subers)
+}
+
+func (s *Subscribers) Broadcast(event any) {
+	for sub := range s.internal {
+		sub.OnEvent(event)
+	}
+	for sub := range s.public {
+		sub.OnEvent(event)
+	}
+}
+
+func (s *Subscribers) Pick() ISubscriber {
+	for sub := range s.public {
+		return sub
+	}
+	return nil
+}
+
+func (s *Subscribers) Len() int {
+	return len(s.public)
+}
+
+func (s *Subscribers) RangeAll(f func(sub ISubscriber)) {
+	s.rangeAll(func(sub ISubscriber, wait *waitTracks) {
+		f(sub)
+	})
+}
+
+func (s *Subscribers) rangeAll(f func(sub ISubscriber, wait *waitTracks)) {
+	for sub, wait := range s.internal {
+		f(sub, wait)
+	}
+	for sub, wait := range s.public {
+		f(sub, wait)
+	}
+}
+
+func (s *Subscribers) OnTrack(track common.Track) {
+	s.rangeAll(func(sub ISubscriber, wait *waitTracks) {
+		if _, ok := s.waits[wait]; ok {
+			if wait.Accept(track) {
+				delete(s.waits, wait)
+			}
+		} else {
+			sub.OnEvent(track)
+		}
+	})
+}
+
+func (s *Subscribers) OnPublisherLost(event StateEvent) {
+	s.rangeAll(func(sub ISubscriber, wait *waitTracks) {
+		if _, ok := s.waits[wait]; ok {
+			wait.Reject(ErrPublisherLost)
+			delete(s.waits, wait)
+		}
+		sub.OnEvent(event)
+	})
+}
+
+// SendInviteTrack 广播需要的 Track(转码插件可以用到)
+func (s *Subscribers) SendInviteTrack(stream *Stream) {
+	var video = map[string]ISubscriber{}
+	var audio = map[string]ISubscriber{}
+	for wait, suber := range s.waits {
+		for _, name := range wait.video {
+			video[name] = suber
+		}
+		for _, name := range wait.audio {
+			audio[name] = suber
+		}
+	}
+	for v, suber := range video {
+		InviteTrack(v, suber)
+	}
+	for a, suber := range audio {
+		InviteTrack(a, suber)
+	}
+}
+
+func (s *Subscribers) AbortWait() {
+	s.waitAborted = true
+	for wait := range s.waits {
+		wait.Resolve()
+		delete(s.waits, wait)
+	}
+}
+
+func (s *Subscribers) Find(id string) ISubscriber {
+	for sub := range s.public {
+		if sub.GetSubscriber().ID == id {
+			return sub
+		}
+	}
+	return nil
+}
+
+func (s *Subscribers) Delete(suber ISubscriber) {
+	io := suber.GetSubscriber()
+	for _, reader := range io.readers {
+		reader.Track.Debug("reader -1", zap.Int32("count", reader.Track.ReaderCount.Add(-1)))
+	}
+	if _, ok := s.public[suber]; ok {
+		delete(s.public, suber)
+		io.Info("suber -1", zap.Int("remains", s.Len()))
+	}
+	if _, ok := s.internal[suber]; ok {
+		delete(s.internal, suber)
+		io.Info("innersuber -1", zap.Int("remains", len(s.internal)))
+	}
+	if config.Global.EnableSubEvent {
+		EventBus <- UnsubscribeEvent{CreateEvent(suber)}
+	}
+}
+
+func (s *Subscribers) Add(suber ISubscriber, wait *waitTracks) {
+	io := suber.GetSubscriber()
+	if io.Config.Internal {
+		s.internal[suber] = wait
+		io.Info("innersuber +1", zap.Int("remains", len(s.internal)))
+	} else {
+		s.public[suber] = wait
+		io.Info("suber +1", zap.Int("remains", s.Len()))
+		if config.Global.EnableSubEvent {
+			EventBus <- suber
+		}
+	}
+	if wait.NeedWait() {
+		s.waits[wait] = suber
+	} else {
+		wait.Resolve()
+	}
+}
+
+func (s *Subscribers) Dispose() {
+	for w := range s.waits {
+		w.Reject(ErrStreamIsClosed)
+	}
+	s.waits = nil
+	s.public = nil
+	s.internal = nil
+}

+ 112 - 0
m7s.live/engine/v4@v4.14.6/summary.go

@@ -0,0 +1,112 @@
+package engine
+
+import (
+	"encoding/json"
+	"sync"
+	"time"
+
+	"github.com/shirou/gopsutil/v3/cpu"
+	"github.com/shirou/gopsutil/v3/disk"
+	"github.com/shirou/gopsutil/v3/mem"
+	"github.com/shirou/gopsutil/v3/net"
+	"m7s.live/engine/v4/util"
+)
+
+var (
+	summary SummaryUtil
+	lastSummary Summary
+	children util.Map[string, *Summary]
+	collectLock sync.RWMutex
+)
+// ServerSummary 系统摘要定义
+type Summary struct {
+	Address string
+	Memory  struct {
+		Total uint64
+		Free  uint64
+		Used  uint64
+		Usage float64
+	}
+	CPUUsage float64
+	HardDisk struct {
+		Total uint64
+		Free  uint64
+		Used  uint64
+		Usage float64
+	}
+	NetWork []NetWorkInfo
+	Streams []StreamSummay
+	ts      time.Time //上次更新时间
+}
+
+// NetWorkInfo 网速信息
+type NetWorkInfo struct {
+	Name         string
+	Receive      uint64
+	Sent         uint64
+	ReceiveSpeed uint64
+	SentSpeed    uint64
+}
+type SummaryUtil Summary
+// Report 上报数据
+func (s *Summary) Report(slave *Summary) {
+	children.Set(slave.Address, slave)
+}
+
+func (s *SummaryUtil) MarshalJSON() ([]byte, error) {
+	return json.Marshal(s.collect())
+}
+
+func (s *SummaryUtil) MarshalYAML() (any, error) {
+	return s.collect(), nil
+}
+
+func (s *SummaryUtil) collect() *Summary {
+	if collectLock.TryLock() {
+		defer collectLock.Unlock()
+	} else {
+		collectLock.RLock()
+		defer collectLock.RUnlock()
+		return &lastSummary
+	}
+	dur := time.Since(s.ts)
+	if dur < time.Second {
+		return &lastSummary
+	}
+	s.ts = time.Now()
+	v, _ := mem.VirtualMemory()
+	d, _ := disk.Usage("/")
+	nv, _ := net.IOCounters(true)
+
+	s.Memory.Total = v.Total >> 20
+	s.Memory.Free = v.Available >> 20
+	s.Memory.Used = v.Used >> 20
+	s.Memory.Usage = v.UsedPercent
+
+	if cc, _ := cpu.Percent(time.Second, false); len(cc) > 0 {
+		s.CPUUsage = cc[0]
+	}
+	s.HardDisk.Free = d.Free >> 30
+	s.HardDisk.Total = d.Total >> 30
+	s.HardDisk.Used = d.Used >> 30
+	s.HardDisk.Usage = d.UsedPercent
+	netWorks := []NetWorkInfo{}
+	for i, n := range nv {
+		info := NetWorkInfo{
+			Name:    n.Name,
+			Receive: n.BytesRecv,
+			Sent:    n.BytesSent,
+		}
+		if len(lastSummary.NetWork) > i {
+			info.ReceiveSpeed = (n.BytesRecv - lastSummary.NetWork[i].Receive) / uint64(dur.Seconds())
+			info.SentSpeed = (n.BytesSent - lastSummary.NetWork[i].Sent) / uint64(dur.Seconds())
+		}
+		netWorks = append(netWorks, info)
+	}
+	s.NetWork = netWorks
+	s.Streams = util.MapList(&Streams, func(name string, ss *Stream) StreamSummay {
+		return ss.Summary()
+	})
+	lastSummary = Summary(*s)
+	return &lastSummary
+}

+ 265 - 0
m7s.live/engine/v4@v4.14.6/track/aac.go

@@ -0,0 +1,265 @@
+package track
+
+import (
+	"fmt"
+	"io"
+	"net"
+
+	"github.com/bluenviron/mediacommon/pkg/bits"
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/codec"
+	. "m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/util"
+)
+
+var _ SpesificTrack = (*AAC)(nil)
+
+func NewAAC(stream IStream, stuff ...any) (aac *AAC) {
+	aac = &AAC{
+		Mode: 2,
+	}
+	aac.SizeLength = 13
+	aac.IndexLength = 3
+	aac.IndexDeltaLength = 3
+	aac.CodecID = codec.CodecID_AAC
+	aac.Channels = 2
+	aac.SampleSize = 16
+	aac.SetStuff("aac", byte(97), aac, stuff, stream)
+	if aac.BytesPool == nil {
+		aac.BytesPool = make(util.BytesPool, 17)
+	}
+	aac.AVCCHead = []byte{0xAF, 1}
+	return
+}
+
+type AAC struct {
+	Audio
+
+	Mode      int       // 1为lbr,2为hbr
+	fragments *util.BLL // 用于处理不完整的AU,缺少的字节数
+}
+
+func (aac *AAC) WriteADTS(ts uint32, b util.IBytes) {
+	adts := b.Bytes()
+	if aac.SequenceHead == nil {
+		profile := ((adts[2] & 0xc0) >> 6) + 1
+		sampleRate := (adts[2] & 0x3c) >> 2
+		channel := ((adts[2] & 0x1) << 2) | ((adts[3] & 0xc0) >> 6)
+		config1 := (profile << 3) | ((sampleRate & 0xe) >> 1)
+		config2 := ((sampleRate & 0x1) << 7) | (channel << 3)
+		aac.Media.WriteSequenceHead([]byte{0xAF, 0x00, config1, config2})
+		aac.SampleRate = uint32(codec.SamplingFrequencies[sampleRate])
+		aac.Channels = channel
+		aac.Parse(aac.SequenceHead[2:])
+		aac.Attach()
+	}
+	aac.generateTimestamp(ts)
+	frameLen := (int(adts[3]&3) << 11) | (int(adts[4]) << 3) | (int(adts[5]) >> 5)
+	for len(adts) >= frameLen {
+		aac.Value.AUList.Push(aac.BytesPool.GetShell(adts[7:frameLen]))
+		adts = adts[frameLen:]
+		if len(adts) < 7 {
+			break
+		}
+		frameLen = (int(adts[3]&3) << 11) | (int(adts[4]) << 3) | (int(adts[5]) >> 5)
+	}
+	aac.Value.ADTS = aac.GetFromPool(b)
+	aac.Flush()
+}
+
+// https://datatracker.ietf.org/doc/html/rfc3640#section-3.2.1
+func (aac *AAC) WriteRTPFrame(rtpItem *util.ListItem[RTPFrame]) {
+	aac.Value.RTP.Push(rtpItem)
+	frame := &rtpItem.Value
+	if len(frame.Payload) < 2 {
+		// aac.fragments = aac.fragments[:0]
+		return
+	}
+	if aac.SampleRate != 90000 {
+		aac.generateTimestamp(uint32(uint64(frame.Timestamp) * 90000 / uint64(aac.SampleRate)))
+	}
+	auHeaderLen := util.ReadBE[int](frame.Payload[:2]) //通常为16,即一个AU Header的长度
+	if auHeaderLen == 0 {
+		aac.Value.AUList.Push(aac.BytesPool.GetShell(frame.Payload[:2]))
+		aac.Flush()
+	} else {
+		payload := frame.Payload[2:]
+		// AU-headers
+		dataLens, err := aac.readAUHeaders(payload, auHeaderLen)
+		if err != nil {
+			// discard pending fragmented packets
+			return
+		}
+
+		pos := (auHeaderLen >> 3)
+		if (auHeaderLen % 8) != 0 {
+			pos++
+		}
+		payload = payload[pos:]
+
+		if aac.fragments == nil {
+			if frame.Header.Marker {
+				// AUs
+				for _, dataLen := range dataLens {
+					if len(payload) < int(dataLen) {
+						aac.fragments = &util.BLL{}
+						aac.fragments.Push(aac.BytesPool.GetShell(payload))
+						// aac.fragments = aac.fragments[:0]
+						// aac.Error("payload is too short 1", zap.Int("dataLen", int(dataLen)), zap.Int("len", len(payload)))
+						return
+					}
+					aac.AppendAuBytes(payload[:dataLen])
+					payload = payload[dataLen:]
+				}
+			} else {
+				if len(dataLens) != 1 {
+					// aac.fragments = aac.fragments[:0]
+					aac.Error("a fragmented packet can only contain one AU")
+					return
+				}
+				aac.fragments = &util.BLL{}
+				// if len(payload) < int(dataLens[0]) {
+				// 	aac.fragments = aac.fragments[:0]
+				// 	aac.Error("payload is too short 2", zap.Int("dataLen", int(dataLens[0])), zap.Int("len", len(payload)))
+				// 	return
+				// }
+				aac.fragments.Push(aac.BytesPool.GetShell(payload))
+				// aac.fragments = append(aac.fragments, payload[:dataLens[0]])
+				return
+			}
+		} else {
+			// we are decoding a fragmented AU
+			if len(dataLens) != 1 {
+				aac.fragments.Recycle()
+				aac.fragments = nil
+				// aac.fragments = aac.fragments[:0]
+				aac.Error("a fragmented packet can only contain one AU")
+				return
+			}
+
+			// if len(payload) < int(dataLens[0]) {
+			// 	aac.fragments = aac.fragments[:0]
+			// 	aac.Error("payload is too short 3", zap.Int("dataLen", int(dataLens[0])), zap.Int("len", len(payload)))
+			// 	return
+			// }
+
+			// if fragmentedSize := util.SizeOfBuffers(aac.fragments) + int(dataLens[0]); fragmentedSize > 5*1024 {
+			// 	aac.fragments = aac.fragments[:0] // discard pending fragmented packets
+			// 	aac.Error(fmt.Sprintf("AU size (%d) is too big (maximum is %d)", fragmentedSize, 5*1024))
+			// 	return
+			// }
+
+			// aac.fragments = append(aac.fragments, payload[:dataLens[0]])
+			aac.fragments.Push(aac.BytesPool.GetShell(payload))
+			if !frame.Header.Marker {
+				return
+			}
+			if uint64(aac.fragments.ByteLength) != dataLens[0] {
+				aac.Error("fragmented AU size is not correct", zap.Uint64("dataLen", dataLens[0]), zap.Int("len", aac.fragments.ByteLength))
+			}
+			aac.Value.AUList.PushValue(aac.fragments)
+			// aac.AppendAuBytes(aac.fragments...)
+
+			aac.fragments = nil
+		}
+		aac.Flush()
+	}
+}
+
+func (aac *AAC) WriteSequenceHead(sh []byte) error {
+	aac.Media.WriteSequenceHead(sh)
+	config1, config2 := aac.SequenceHead[2], aac.SequenceHead[3]
+	aac.Channels = ((config2 >> 3) & 0x0F) //声道
+	aac.SampleRate = uint32(codec.SamplingFrequencies[((config1&0x7)<<1)|(config2>>7)])
+	aac.Parse(aac.SequenceHead[2:])
+	go aac.Attach()
+	return nil
+}
+
+func (aac *AAC) WriteAVCC(ts uint32, frame *util.BLL) error {
+	if l := frame.ByteLength; l < 4 {
+		aac.Error("AVCC data too short", zap.Int("len", l))
+		return io.ErrShortWrite
+	}
+	if frame.GetByte(1) == 0 {
+		aac.WriteSequenceHead(frame.ToBytes())
+		frame.Recycle()
+	} else {
+		au := frame.ToBuffers()
+		au[0] = au[0][2:]
+		aac.AppendAuBytes(au...)
+		aac.Audio.WriteAVCC(ts, frame)
+	}
+	return nil
+}
+
+func (aac *AAC) CompleteRTP(value *AVFrame) {
+	l := value.AUList.ByteLength
+	//AU_HEADER_LENGTH,因为单位是bit, 除以8就是auHeader的字节长度;又因为单个auheader字节长度2字节,所以再除以2就是auheader的个数。
+	auHeaderLen := []byte{0x00, 0x10, (byte)((l & 0x1fe0) >> 5), (byte)((l & 0x1f) << 3)} // 3 = 16-13, 5 = 8-3
+	var packets [][][]byte
+	r := value.AUList.Next.Value.NewReader()
+	for bufs := r.ReadN(RTPMTU); len(bufs) > 0; bufs = r.ReadN(RTPMTU) {
+		packets = append(packets, append(net.Buffers{auHeaderLen}, bufs...))
+	}
+	aac.PacketizeRTP(packets...)
+}
+
+func (aac *AAC) readAUHeaders(buf []byte, headersLen int) ([]uint64, error) {
+	firstRead := false
+
+	count := 0
+	for i := 0; i < headersLen; {
+		if i == 0 {
+			i += aac.SizeLength
+			i += aac.IndexLength
+		} else {
+			i += aac.SizeLength
+			i += aac.IndexDeltaLength
+		}
+		count++
+	}
+
+	dataLens := make([]uint64, count)
+
+	pos := 0
+	i := 0
+
+	for headersLen > 0 {
+		dataLen, err := bits.ReadBits(buf, &pos, aac.SizeLength)
+		if err != nil {
+			return nil, err
+		}
+		headersLen -= aac.SizeLength
+
+		if !firstRead {
+			firstRead = true
+			if aac.IndexLength > 0 {
+				auIndex, err := bits.ReadBits(buf, &pos, aac.IndexLength)
+				if err != nil {
+					return nil, err
+				}
+				headersLen -= aac.IndexLength
+
+				if auIndex != 0 {
+					return nil, fmt.Errorf("AU-index different than zero is not supported")
+				}
+			}
+		} else if aac.IndexDeltaLength > 0 {
+			auIndexDelta, err := bits.ReadBits(buf, &pos, aac.IndexDeltaLength)
+			if err != nil {
+				return nil, err
+			}
+			headersLen -= aac.IndexDeltaLength
+
+			if auIndexDelta != 0 {
+				return nil, fmt.Errorf("AU-index-delta different than zero is not supported")
+			}
+		}
+
+		dataLens[i] = dataLen
+		i++
+	}
+
+	return dataLens, nil
+}

+ 98 - 0
m7s.live/engine/v4@v4.14.6/track/audio.go

@@ -0,0 +1,98 @@
+package track
+
+import (
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/codec"
+	. "m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/util"
+)
+
+type Audio struct {
+	Media
+	CodecID          codec.AudioCodecID
+	Channels         byte
+	SampleSize       byte
+	SizeLength       int // 通常为13
+	IndexLength      int
+	IndexDeltaLength int
+	AVCCHead         []byte // 音频包在AVCC格式中,AAC会有两个字节,其他的只有一个字节
+	codec.AudioSpecificConfig
+}
+
+func (a *Audio) Attach() {
+	if a.Attached.CompareAndSwap(false, true) {
+		if err := a.Stream.AddTrack(a).Await(); err != nil {
+			a.Error("attach audio track failed", zap.Error(err))
+			a.Attached.Store(false)
+		} else {
+			a.Info("audio track attached", zap.Uint32("sample rate", a.SampleRate))
+		}
+	}
+}
+
+func (a *Audio) Detach() {
+	if a.Attached.CompareAndSwap(true, false) {
+		a.Stream.RemoveTrack(a)
+	}
+}
+
+func (a *Audio) GetName() string {
+	if a.Name == "" {
+		return a.CodecID.String()
+	}
+	return a.Name
+}
+
+func (av *Audio) WriteADTS(pts uint32, adts util.IBytes) {
+
+}
+
+func (av *Audio) WriteSequenceHead(sh []byte) error {
+	av.Media.WriteSequenceHead(sh)
+	return nil
+}
+
+func (av *Audio) Flush() {
+	if av.CodecID == codec.CodecID_AAC && av.Value.ADTS == nil {
+		item := av.BytesPool.Get(7)
+		av.ToADTS(av.Value.AUList.ByteLength, item.Value)
+		av.Value.ADTS = item
+	}
+	av.Media.Flush()
+}
+
+func (av *Audio) WriteRawBytes(pts uint32, raw util.IBytes) {
+	curValue := av.Value
+	curValue.BytesIn += raw.Len()
+	av.Value.AUList.Push(av.GetFromPool(raw))
+	av.generateTimestamp(pts)
+	av.Flush()
+}
+
+func (av *Audio) WriteAVCC(ts uint32, frame *util.BLL) {
+	av.Value.WriteAVCC(ts, frame)
+	av.generateTimestamp(ts * 90)
+	av.Flush()
+}
+
+func (a *Audio) CompleteAVCC(value *AVFrame) {
+	value.AVCC.Push(a.BytesPool.GetShell(a.AVCCHead))
+	value.AUList.Range(func(v *util.BLL) bool {
+		v.Range(func(v util.Buffer) bool {
+			value.AVCC.Push(a.BytesPool.GetShell(v))
+			return true
+		})
+		return true
+	})
+}
+
+func (a *Audio) CompleteRTP(value *AVFrame) {
+	a.PacketizeRTP(value.AUList.ToList()...)
+}
+
+func (a *Audio) Narrow() {
+	// if a.HistoryRing == nil && a.IDRing != nil {
+	// 	a.narrow(int(a.Value.Sequence - a.IDRing.Value.Sequence))
+	// }
+	a.AddIDR()
+}

+ 172 - 0
m7s.live/engine/v4@v4.14.6/track/av1.go

@@ -0,0 +1,172 @@
+package track
+
+import (
+	"time"
+
+	"github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1"
+	"github.com/bluenviron/mediacommon/pkg/codecs/av1"
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/codec"
+	. "m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/log"
+	"m7s.live/engine/v4/util"
+)
+
+var _ SpesificTrack = (*AV1)(nil)
+
+type AV1 struct {
+	Video
+	decoder         rtpav1.Decoder
+	encoder         rtpav1.Encoder
+	seqHeader       av1.SequenceHeader
+	seenFrameHeader bool
+	refFrameType    map[byte]byte
+}
+
+func NewAV1(stream IStream, stuff ...any) (vt *AV1) {
+	vt = &AV1{}
+	vt.Video.CodecID = codec.CodecID_AV1
+	vt.SetStuff("av1", byte(96), uint32(90000), vt, stuff, stream)
+	if vt.BytesPool == nil {
+		vt.BytesPool = make(util.BytesPool, 17)
+	}
+	vt.nalulenSize = 0
+	vt.dtsEst = NewDTSEstimator()
+	vt.decoder.Init()
+	vt.encoder.Init()
+	vt.encoder.PayloadType = vt.PayloadType
+	vt.ParamaterSets = [][]byte{nil, {0, 0, 0}}
+	return
+}
+
+func (vt *AV1) WriteSequenceHead(head []byte) (err error) {
+	vt.Video.WriteSequenceHead(head)
+	var info codec.AV1CodecConfigurationRecord
+	info.Unmarshal(head[5:])
+	vt.seqHeader.Unmarshal(info.ConfigOBUs)
+	vt.ParamaterSets = [][]byte{info.ConfigOBUs, {info.SeqLevelIdx0, info.SeqProfile, info.SeqTier0}}
+	return
+}
+
+func (vt *AV1) WriteRTPFrame(rtpItem *util.ListItem[RTPFrame]) {
+	defer func() {
+		err := recover()
+		if err != nil {
+			vt.Error("WriteRTPFrame panic", zap.Any("err", err))
+			vt.Stream.Close()
+		}
+	}()
+	if vt.lastSeq != vt.lastSeq2+1 && vt.lastSeq2 != 0 {
+		vt.lostFlag = true
+		vt.Warn("lost rtp packet", zap.Uint16("lastSeq", vt.lastSeq), zap.Uint16("lastSeq2", vt.lastSeq2))
+	}
+	frame := &rtpItem.Value
+	rv := vt.Value
+	rv.RTP.Push(rtpItem)
+	obus, err := vt.decoder.Decode(frame.Packet)
+	for _, obu := range obus {
+		var obuHeader av1.OBUHeader
+		obuHeader.Unmarshal(obu)
+		switch obuHeader.Type {
+		case av1.OBUTypeSequenceHeader:
+			rtmpHead := []byte{0b1001_0000 | byte(codec.PacketTypeMPEG2TSSequenceStart), 0, 0, 0, 0}
+			util.BigEndian.PutUint32(rtmpHead[1:], codec.FourCC_AV1_32)
+			// TODO: 生成 head
+			rtmpHead = append(rtmpHead, obu...)
+			vt.Video.WriteSequenceHead(rtmpHead)
+			vt.ParamaterSets[0] = obu
+		default:
+			rv.AUList.Push(vt.BytesPool.GetShell(obu))
+		}
+	}
+	if err == nil {
+		vt.generateTimestamp(frame.Timestamp)
+		vt.Flush()
+	}
+}
+
+func (vt *AV1) writeAVCCFrame(ts uint32, r *util.BLLReader, frame *util.BLL) (err error) {
+	vt.Value.PTS = time.Duration(ts) * 90
+	vt.Value.DTS = time.Duration(ts) * 90
+	var obuHeader av1.OBUHeader
+	for r.CanRead() {
+		offset := r.GetOffset()
+		b, _ := r.ReadByte()
+		obuHeader.Unmarshal([]byte{b})
+		if log.Trace {
+			vt.Trace("obu", zap.Any("type", obuHeader.Type), zap.Bool("iframe", vt.Value.IFrame))
+		}
+		obuSize, _, _ := r.LEB128Unmarshal()
+		end := r.GetOffset()
+		size := end - offset + int(obuSize)
+		r = frame.NewReader()
+		r.Skip(offset)
+		obu := r.ReadN(size)
+		switch obuHeader.Type {
+		case codec.AV1_OBU_SEQUENCE_HEADER:
+		// 	vt.seqHeader.Unmarshal(util.ConcatBuffers(obu))
+		// 	vt.seenFrameHeader = false
+		// 	vt.AppendAuBytes(obu...)
+		case codec.AV1_OBU_FRAME:
+			// 	if !vt.seenFrameHeader {
+			// 		if vt.seqHeader.ReducedStillPictureHeader {
+			// 			vt.Value.IFrame = true
+			// 			vt.seenFrameHeader = true
+			// 		} else {
+			// 			showframe := obu[0][0] >> 7
+			// 			if showframe != 0 {
+			// 				frame_to_show_map_idx := (obu[0][0] >> 4) & 0b0111
+			// 				vt.Value.IFrame = vt.refFrameType[frame_to_show_map_idx] == 0
+			// 			} else {
+			// 				vt.Value.IFrame = (obu[0][0])&0b0110_0000 == 0
+			// 			}
+			// 			vt.seenFrameHeader = showframe == 0
+			// 		}
+			// 	}
+			// 	vt.AppendAuBytes(obu...)
+		case codec.AV1_OBU_TEMPORAL_DELIMITER:
+		case codec.AV1_OBU_FRAME_HEADER:
+		}
+		vt.AppendAuBytes(obu...)
+	}
+	return
+}
+
+func (vt *AV1) CompleteAVCC(rv *AVFrame) {
+	mem := vt.BytesPool.Get(5)
+	b := mem.Value
+	if rv.IFrame {
+		b[0] = 0b1001_0000 | byte(codec.PacketTypeCodedFrames)
+	} else {
+		b[0] = 0b1010_0000 | byte(codec.PacketTypeCodedFrames)
+	}
+	util.BigEndian.PutUint32(b[1:], codec.FourCC_AV1_32)
+	// println(rv.PTS < rv.DTS, "\t", rv.PTS, "\t", rv.DTS, "\t", rv.PTS-rv.DTS)
+	// 写入CTS
+	rv.AVCC.Push(mem)
+
+	rv.AUList.Range(func(au *util.BLL) bool {
+		au.Range(func(slice util.Buffer) bool {
+			rv.AVCC.Push(vt.BytesPool.GetShell(slice))
+			return true
+		})
+		return true
+	})
+}
+
+// RTP格式补完
+func (vt *AV1) CompleteRTP(value *AVFrame) {
+	obus := vt.Value.AUList.ToBuffers()
+	// if vt.Value.IFrame {
+	// 	obus = append(net.Buffers{vt.ParamaterSets[0]}, obus...)
+	// }
+	rtps, err := vt.encoder.Encode(obus)
+	if err != nil {
+		vt.Error("AV1 encoder encode error", zap.Error(err))
+		return
+	}
+
+	for _, rtp := range rtps {
+		vt.Value.RTP.PushValue(RTPFrame{Packet: rtp})
+	}
+}

+ 318 - 0
m7s.live/engine/v4@v4.14.6/track/base.go

@@ -0,0 +1,318 @@
+package track
+
+import (
+	"time"
+	"unsafe"
+
+	"github.com/pion/rtp"
+	"go.uber.org/zap"
+	. "m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/config"
+	"m7s.live/engine/v4/log"
+	"m7s.live/engine/v4/util"
+)
+
+type 流速控制 struct {
+	起始时间戳 time.Duration
+	起始dts time.Duration
+	等待上限  time.Duration
+	起始时间  time.Time
+}
+
+func (p *流速控制) 重置(绝对时间戳 time.Duration, dts time.Duration) {
+	p.起始时间 = time.Now()
+	p.起始时间戳 = 绝对时间戳
+	p.起始dts = dts
+	// println("重置", p.起始时间.Format("2006-01-02 15:04:05"), p.起始时间戳)
+}
+func (p *流速控制) 根据起始DTS计算绝对时间戳(dts time.Duration) time.Duration {
+	if dts < p.起始dts {
+		dts += (1 << 32)
+	}
+	return ((dts-p.起始dts)*time.Millisecond + p.起始时间戳*90) / 90
+}
+
+func (p *流速控制) 控制流速(绝对时间戳 time.Duration, dts time.Duration) (等待了 time.Duration) {
+	数据时间差, 实际时间差 := 绝对时间戳-p.起始时间戳, time.Since(p.起始时间)
+	// println("数据时间差", 数据时间差, "实际时间差", 实际时间差, "绝对时间戳", 绝对时间戳, "起始时间戳", p.起始时间戳, "起始时间", p.起始时间.Format("2006-01-02 15:04:05"))
+	// if 实际时间差 > 数据时间差 {
+	// 	p.重置(绝对时间戳)
+	// 	return
+	// }
+	// 如果收到的帧的时间戳超过实际消耗的时间100ms就休息一下,100ms作为一个弹性区间防止频繁调用sleep
+	if 过快 := (数据时间差 - 实际时间差); 过快 > 100*time.Millisecond {
+		// fmt.Println("过快毫秒", 过快.Milliseconds())
+		// println("过快毫秒", p.name, 过快.Milliseconds())
+		if 过快 > p.等待上限 {
+			等待了 = p.等待上限
+		} else {
+			等待了 = 过快
+		}
+		time.Sleep(等待了)
+	} else if 过快 < -100*time.Millisecond {
+		// fmt.Println("过慢毫秒", 过快.Milliseconds())
+		// p.重置(绝对时间戳, dts)
+		// println("过慢毫秒", p.name, 过快.Milliseconds())
+	}
+	return
+}
+
+type SpesificTrack interface {
+	CompleteRTP(*AVFrame)
+	CompleteAVCC(*AVFrame)
+	WriteSliceBytes([]byte)
+	WriteRTPFrame(*util.ListItem[RTPFrame])
+	generateTimestamp(uint32)
+	WriteSequenceHead([]byte) error
+	writeAVCCFrame(uint32, *util.BLLReader, *util.BLL) error
+	GetNALU_SEI() *util.ListItem[util.Buffer]
+	Flush()
+}
+
+type IDRingList struct {
+	IDRList     util.List[*util.Ring[*AVFrame]]
+	IDRing      *util.Ring[*AVFrame]
+	HistoryRing *util.Ring[*AVFrame]
+}
+
+func (p *IDRingList) AddIDR(IDRing *util.Ring[*AVFrame]) {
+	p.IDRList.PushValue(IDRing)
+	p.IDRing = IDRing
+}
+
+func (p *IDRingList) ShiftIDR() {
+	p.IDRList.Shift()
+	p.HistoryRing = p.IDRList.Next.Value
+}
+
+// Media 基础媒体Track类
+type Media struct {
+	Base[any, *AVFrame]
+	PayloadType     byte
+	IDRingList      `json:"-" yaml:"-"` //最近的关键帧位置,首屏渲染
+	SSRC            uint32
+	SampleRate      uint32
+	BytesPool       util.BytesPool      `json:"-" yaml:"-"`
+	RtpPool         util.Pool[RTPFrame] `json:"-" yaml:"-"`
+	SequenceHead    []byte              `json:"-" yaml:"-"` //H264(SPS、PPS) H265(VPS、SPS、PPS) AAC(config)
+	SequenceHeadSeq int
+	RTPDemuxer
+	SpesificTrack `json:"-" yaml:"-"`
+	deltaTs       time.Duration //用于接续发布后时间戳连续
+	deltaDTSRange time.Duration //DTS差的范围
+	流速控制
+}
+
+func (av *Media) GetFromPool(b util.IBytes) (item *util.ListItem[util.Buffer]) {
+	if b.Reuse() {
+		item = av.BytesPool.Get(b.Len())
+		copy(item.Value, b.Bytes())
+	} else {
+		return av.BytesPool.GetShell(b.Bytes())
+	}
+	return
+}
+
+func (av *Media) GetRTPFromPool() (result *util.ListItem[RTPFrame]) {
+	result = av.RtpPool.Get()
+	if result.Value.Packet == nil {
+		result.Value.Packet = &rtp.Packet{}
+		result.Value.PayloadType = av.PayloadType
+		result.Value.SSRC = av.SSRC
+		result.Value.Version = 2
+		result.Value.Raw = make([]byte, 1460)
+	}
+	result.Value.Raw = result.Value.Raw[:1460]
+	result.Value.Payload = result.Value.Raw[:0]
+	return
+}
+
+// 为json序列化而计算的数据
+func (av *Media) SnapForJson() {
+	v := av.LastValue
+	if av.RawPart != nil {
+		av.RawPart = av.RawPart[:0]
+	} else {
+		av.RawPart = make([]int, 0, 10)
+	}
+	if av.RawSize = v.AUList.ByteLength; av.RawSize > 0 {
+		r := v.AUList.NewReader()
+		for b, err := r.ReadByte(); err == nil && len(av.RawPart) < 10; b, err = r.ReadByte() {
+			av.RawPart = append(av.RawPart, int(b))
+		}
+	}
+}
+
+func (av *Media) SetSpeedLimit(value time.Duration) {
+	av.等待上限 = value
+}
+
+func (av *Media) SetStuff(stuff ...any) {
+	// 代表发布者已经离线,该Track成为遗留Track,等待下一任发布者接续发布
+	for _, s := range stuff {
+		switch v := s.(type) {
+		case IStream:
+			pubConf := v.GetPublisherConfig()
+			av.Base.SetStuff(v)
+			av.Init(256, NewAVFrame)
+			av.SSRC = uint32(uintptr(unsafe.Pointer(av)))
+			av.等待上限 = pubConf.SpeedLimit
+		case uint32:
+			av.SampleRate = v
+		case byte:
+			av.PayloadType = v
+		case util.BytesPool:
+			av.BytesPool = v
+		case SpesificTrack:
+			av.SpesificTrack = v
+		case []any:
+			av.SetStuff(v...)
+		default:
+			av.Base.SetStuff(v)
+		}
+	}
+}
+
+func (av *Media) LastWriteTime() time.Time {
+	return av.LastValue.WriteTime
+}
+
+func (av *Media) CurrentFrame() *AVFrame {
+	return av.Value
+}
+func (av *Media) PreFrame() *AVFrame {
+	return av.LastValue
+}
+
+func (av *Media) generateTimestamp(ts uint32) {
+	av.Value.PTS = time.Duration(ts)
+	av.Value.DTS = time.Duration(ts)
+}
+
+func (av *Media) WriteSequenceHead(sh []byte) {
+	av.SequenceHead = sh
+	av.SequenceHeadSeq++
+}
+func (av *Media) AppendAuBytes(b ...[]byte) {
+	var au util.BLL
+	for _, bb := range b {
+		au.Push(av.BytesPool.GetShell(bb))
+	}
+	av.Value.AUList.PushValue(&au)
+}
+
+func (av *Media) narrow(gop int) {
+	if l := av.Size - gop; l > 12 {
+		if log.Trace {
+			av.Trace("resize", zap.Int("before", av.Size), zap.Int("after", av.Size-5))
+		}
+		//缩小缓冲环节省内存
+		av.Reduce(5)
+	}
+}
+
+func (av *Media) AddIDR() {
+	if av.Stream.GetPublisherConfig().BufferTime > 0 {
+		av.IDRingList.AddIDR(av.Ring)
+		if av.HistoryRing == nil {
+			av.HistoryRing = av.IDRing
+		}
+	} else {
+		av.IDRing = av.Ring
+	}
+}
+
+func (av *Media) Flush() {
+	curValue, preValue, nextValue := av.Value, av.LastValue, av.Next()
+	useDts := curValue.Timestamp == 0
+	if av.State == TrackStateOffline {
+		av.State = TrackStateOnline
+		if useDts {
+			av.deltaTs = curValue.DTS - preValue.DTS
+		} else {
+			av.deltaTs = curValue.Timestamp - preValue.Timestamp
+		}
+		curValue.DTS = preValue.DTS + 90
+		curValue.PTS = preValue.PTS + 90
+		curValue.Timestamp = preValue.Timestamp + time.Millisecond
+		av.Info("track back online", zap.Duration("delta", av.deltaTs))
+	} else if av.deltaTs != 0 {
+		if useDts {
+			curValue.DTS -= av.deltaTs
+			curValue.PTS -= av.deltaTs
+		} else {
+			rtpts := av.deltaTs * 90 / time.Millisecond
+			curValue.DTS -= rtpts
+			curValue.PTS -= rtpts
+			curValue.Timestamp -= av.deltaTs
+		}
+	}
+	if av.起始时间.IsZero() {
+		curValue.DeltaTime = 0
+		if useDts {
+			curValue.Timestamp = time.Since(av.Stream.GetStartTime())
+		}
+		av.重置(curValue.Timestamp, curValue.DTS)
+	} else {
+		if useDts {
+			deltaDts := curValue.DTS - preValue.DTS
+			if deltaDts <= 0 && deltaDts > -(1<<15) {
+				// 生成一个无奈的deltaDts
+				deltaDts = 90
+				// 必须保证DTS递增
+				curValue.DTS = preValue.DTS + deltaDts
+			} else if deltaDts != 90 {
+				// 正常情况下生成容错范围
+				av.deltaDTSRange = deltaDts * 2
+			}
+			curValue.Timestamp = av.根据起始DTS计算绝对时间戳(curValue.DTS)
+		}
+
+		curValue.DeltaTime = uint32(deltaTS(curValue.Timestamp, preValue.Timestamp) / time.Millisecond)
+	}
+	if log.Trace {
+		av.Trace("write", zap.Uint32("seq", curValue.Sequence), zap.Duration("dts", curValue.DTS), zap.Duration("dts delta", curValue.DTS-preValue.DTS), zap.Uint32("delta", curValue.DeltaTime), zap.Duration("timestamp", curValue.Timestamp), zap.Int("au", curValue.AUList.Length), zap.Int("rtp", curValue.RTP.Length), zap.Int("avcc", curValue.AVCC.ByteLength), zap.Int("raw", curValue.AUList.ByteLength), zap.Int("bps", av.BPS))
+	}
+	bufferTime := av.Stream.GetPublisherConfig().BufferTime
+	if bufferTime > 0 && av.IDRingList.IDRList.Length > 1 && deltaTS(curValue.Timestamp, av.IDRingList.IDRList.Next.Next.Value.Value.Timestamp) > bufferTime {
+		av.ShiftIDR()
+		av.narrow(int(curValue.Sequence - av.HistoryRing.Value.Sequence))
+	}
+	// 下一帧为订阅起始帧,即将覆盖,需要扩环
+	if nextValue == av.IDRing || nextValue == av.HistoryRing {
+		// if av.AVRing.Size < 512 {
+		if log.Trace {
+			av.Stream.Trace("resize", zap.Int("before", av.Size), zap.Int("after", av.Size+5), zap.String("name", av.Name))
+		}
+		av.Glow(5)
+		// } else {
+		// 	av.Stream.Error("sub ring overflow", zap.Int("size", av.AVRing.Size), zap.String("name", av.Name))
+		// }
+	}
+
+	if curValue.AUList.Length > 0 {
+		// 补完RTP
+		if config.Global.EnableRTP && curValue.RTP.Length == 0 {
+			av.CompleteRTP(curValue)
+		}
+		// 补完AVCC
+		if config.Global.EnableAVCC && curValue.AVCC.ByteLength == 0 {
+			av.CompleteAVCC(curValue)
+		}
+	}
+	av.ComputeBPS(curValue.BytesIn)
+	av.Step()
+	if av.等待上限 > 0 {
+		等待了 := av.控制流速(curValue.Timestamp, curValue.DTS)
+		if log.Trace && 等待了 > 0 {
+			av.Trace("speed control", zap.Duration("sleep", 等待了))
+		}
+	}
+}
+
+func deltaTS(curTs time.Duration, preTs time.Duration) time.Duration {
+	if curTs < preTs {
+		return curTs + (1<<32)*time.Millisecond - preTs
+	}
+	return curTs - preTs
+}

+ 113 - 0
m7s.live/engine/v4@v4.14.6/track/data.go

@@ -0,0 +1,113 @@
+package track
+
+import (
+	"context"
+	"sync"
+	"time"
+
+	"go.uber.org/zap"
+	. "m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/log"
+	"m7s.live/engine/v4/util"
+)
+
+type Data[T any] struct {
+	Base[T, *DataFrame[T]]
+	sync.Locker `json:"-" yaml:"-"` // 写入锁,可选,单一协程写入可以不加锁
+}
+
+func (dt *Data[T]) Init(n int) {
+	dt.Base.Init(n, NewDataFrame[T])
+}
+
+func (dt *Data[T]) Push(data T) {
+	if dt.Locker != nil {
+		dt.Lock()
+		defer dt.Unlock()
+	}
+	curValue := dt.Value
+	if log.Trace {
+		dt.Trace("push data", zap.Uint32("sequence", curValue.Sequence))
+	}
+	curValue.Data = data
+	dt.Step()
+}
+
+func (d *Data[T]) Play(ctx context.Context, onData func(*DataFrame[T]) error) (err error) {
+	d.Debug("play data track")
+	reader := DataReader[T]{}
+	for err = reader.StartRead(d.Ring); err == nil; err = reader.ReadNext() {
+		if log.Trace {
+			d.Trace("read data", zap.Uint32("sequence", reader.Value.Sequence))
+		}
+		if err = onData(reader.Value); err == nil {
+			err = ctx.Err()
+		}
+		if err != nil {
+			reader.Value.ReaderLeave()
+			return
+		}
+	}
+	return
+}
+
+func (d *Data[T]) Attach(s IStream) {
+	d.SetStuff(s)
+	if err := s.AddTrack(d).Await(); err != nil {
+		d.Error("attach data track failed", zap.Error(err))
+	} else {
+		d.Info("data track attached")
+	}
+}
+
+func (d *Data[T]) LastWriteTime() time.Time {
+	return d.LastValue.WriteTime
+}
+
+func NewDataTrack[T any](name string) (dt *Data[T]) {
+	dt = &Data[T]{}
+	dt.Init(10)
+	dt.SetStuff(name)
+	return
+}
+
+type RecycleData[T util.Recyclable] struct {
+	Data[T]
+}
+
+func (dt *RecycleData[T]) Push(data T) {
+	if dt.Locker != nil {
+		dt.Lock()
+		defer dt.Unlock()
+	}
+	curValue := dt.Value
+	if log.Trace {
+		dt.Trace("push data", zap.Uint32("sequence", curValue.Sequence))
+	}
+	curValue.Data = data
+	dt.Step()
+	if !dt.Value.WriteTime.IsZero() {
+		dt.Value.Data.Recycle()
+	}
+}
+
+func NewRecycleDataTrack[T util.Recyclable](name string) (dt *RecycleData[T]) {
+	dt = &RecycleData[T]{}
+	dt.Init(10)
+	dt.SetStuff(name)
+	return
+}
+
+type BytesData struct {
+	RecycleData[*util.ListItem[util.Buffer]]
+	Pool util.BytesPool
+}
+
+func NewBytesDataTrack(name string) (dt *BytesData) {
+	dt = &BytesData{
+		Pool: make(util.BytesPool, 17),
+	}
+	dt.Init(10)
+	dt.SetStuff(name)
+	return
+}

+ 82 - 0
m7s.live/engine/v4@v4.14.6/track/g711.go

@@ -0,0 +1,82 @@
+package track
+
+import (
+	"io"
+
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/codec"
+	. "m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/util"
+)
+
+var _ SpesificTrack = (*G711)(nil)
+
+func NewG711(stream IStream, alaw bool, stuff ...any) (g711 *G711) {
+	g711 = &G711{}
+	if alaw {
+		g711.Name = "pcma"
+		g711.PayloadType = 8
+	} else {
+		g711.Name = "pcmu"
+		g711.PayloadType = 0
+	}
+	if alaw {
+		g711.CodecID = codec.CodecID_PCMA
+	} else {
+		g711.CodecID = codec.CodecID_PCMU
+	}
+	g711.SampleSize = 8
+	g711.Channels = 1
+	g711.AVCCHead = []byte{(byte(g711.CodecID) << 4) | (1 << 1)}
+	g711.SetStuff(uint32(8000), g711, stuff, stream)
+	if g711.BytesPool == nil {
+		g711.BytesPool = make(util.BytesPool, 17)
+	}
+	go g711.Attach()
+	return
+}
+
+type G711 struct {
+	Audio
+}
+
+func (g711 *G711) WriteAVCC(ts uint32, frame *util.BLL) error {
+	if l := frame.ByteLength; l < 2 {
+		g711.Error("AVCC data too short", zap.Int("len", l))
+		return io.ErrShortWrite
+	}
+	i := 0
+	frame.Range(func(v util.Buffer) bool {
+		if i == 0 {
+			v = v.SubBuf(1, v.Len()-1)
+		}
+		g711.Value.AUList.Push(g711.BytesPool.GetShell(v))
+		i++
+		return true
+	})
+	g711.Audio.WriteAVCC(ts, frame)
+	return nil
+}
+
+func (g711 *G711) WriteRTPFrame(rtpItem *util.ListItem[RTPFrame]) {
+	frame := &rtpItem.Value
+	g711.Value.RTP.Push(rtpItem)
+	if g711.SampleRate != 90000 {
+		g711.generateTimestamp(uint32(uint64(frame.Timestamp) * 90000 / uint64(g711.SampleRate)))
+	}
+	g711.AppendAuBytes(frame.Payload)
+	g711.Flush()
+}
+
+func (g711 *G711) CompleteRTP(value *AVFrame) {
+	if value.AUList.ByteLength > RTPMTU {
+		var packets [][][]byte
+		r := value.AUList.Next.Value.NewReader()
+		for bufs := r.ReadN(RTPMTU); len(bufs) > 0; bufs = r.ReadN(RTPMTU) {
+			packets = append(packets, bufs)
+		}
+		g711.PacketizeRTP(packets...)
+	} else {
+		g711.Audio.CompleteRTP(value)
+	}
+}

+ 229 - 0
m7s.live/engine/v4@v4.14.6/track/h264.go

@@ -0,0 +1,229 @@
+package track
+
+import (
+	"bytes"
+	"time"
+
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/codec"
+	. "m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/log"
+	"m7s.live/engine/v4/util"
+)
+
+var _ SpesificTrack = (*H264)(nil)
+
+type H264 struct {
+	Video
+	buf util.Buffer // rtp 包临时缓存,对于不规范的 rtp 包(sps 放到了 fua 中导致)需要缓存
+}
+
+func NewH264(stream IStream, stuff ...any) (vt *H264) {
+	vt = &H264{}
+	vt.Video.CodecID = codec.CodecID_H264
+	vt.SetStuff("h264", byte(96), uint32(90000), vt, stuff, stream)
+	if vt.BytesPool == nil {
+		vt.BytesPool = make(util.BytesPool, 17)
+	}
+	vt.ParamaterSets = make(ParamaterSets, 2)
+	vt.nalulenSize = 4
+	vt.dtsEst = NewDTSEstimator()
+	return
+}
+
+func (vt *H264) WriteSliceBytes(slice []byte) {
+	if len(slice) > 4 && bytes.Equal(slice[:4], codec.NALU_Delimiter2) {
+		slice = slice[4:] // 有些设备厂商不规范,所以需要移除前导的 00 00 00 01
+	}
+	if len(slice) == 0 {
+		vt.Error("H264 WriteSliceBytes got empty slice")
+		return
+	}
+	naluType := codec.ParseH264NALUType(slice[0])
+	if log.Trace {
+		vt.Trace("naluType", zap.Uint8("naluType", naluType.Byte()))
+	}
+	switch naluType {
+	case codec.NALU_SPS:
+		spsInfo, _ := codec.ParseSPS(slice)
+		if spsInfo.Width != vt.SPSInfo.Width || spsInfo.Height != vt.SPSInfo.Height {
+			vt.Debug("SPS", zap.Any("SPSInfo", spsInfo))
+		}
+		vt.SPSInfo = spsInfo
+		vt.Video.SPS = slice
+		vt.ParamaterSets[0] = slice
+	case codec.NALU_PPS:
+		vt.Video.PPS = slice
+		vt.ParamaterSets[1] = slice
+		lenSPS := len(vt.Video.SPS)
+		lenPPS := len(vt.Video.PPS)
+		var b util.Buffer
+		if lenSPS > 3 {
+			b.Write(codec.RTMP_AVC_HEAD[:6])
+			b.Write(vt.Video.SPS[1:4])
+			b.Write(codec.RTMP_AVC_HEAD[9:10])
+		} else {
+			b.Write(codec.RTMP_AVC_HEAD)
+		}
+		b.WriteByte(0xE1)
+		b.WriteUint16(uint16(lenSPS))
+		b.Write(vt.Video.SPS)
+		b.WriteByte(0x01)
+		b.WriteUint16(uint16(lenPPS))
+		b.Write(vt.Video.PPS)
+		vt.WriteSequenceHead(b)
+	case codec.NALU_IDR_Picture:
+		vt.Value.IFrame = true
+		vt.AppendAuBytes(slice)
+	case codec.NALU_Non_IDR_Picture,
+		codec.NALU_Data_Partition_A,
+		codec.NALU_Data_Partition_B,
+		codec.NALU_Data_Partition_C:
+		vt.Value.IFrame = false
+		vt.AppendAuBytes(slice)
+	case codec.NALU_SEI:
+		vt.AppendAuBytes(slice)
+	case codec.NALU_Access_Unit_Delimiter:
+	case codec.NALU_Filler_Data:
+	default:
+		if vt.Value.IFrame {
+			vt.AppendAuBytes(slice)
+			return
+		}
+		vt.Error("nalu type not support", zap.Int("type", int(naluType)))
+	}
+}
+func (vt *H264) WriteSequenceHead(head []byte) (err error) {
+	var info codec.AVCDecoderConfigurationRecord
+	if _, err = info.Unmarshal(head[5:]); err == nil {
+		vt.SPSInfo, _ = codec.ParseSPS(info.SequenceParameterSetNALUnit)
+		vt.nalulenSize = int(info.LengthSizeMinusOne&3 + 1)
+		vt.SPS = info.SequenceParameterSetNALUnit
+		vt.PPS = info.PictureParameterSetNALUnit
+		vt.ParamaterSets[0] = vt.SPS
+		vt.ParamaterSets[1] = vt.PPS
+		vt.Video.WriteSequenceHead(head)
+	} else {
+		vt.Stream.Error("H264 ParseSpsPps Error")
+		vt.Stream.Close()
+	}
+	return
+}
+
+func (vt *H264) WriteRTPFrame(rtpItem *util.ListItem[RTPFrame]) {
+	defer func() {
+		err := recover()
+		if err != nil {
+			vt.Error("WriteRTPFrame panic", zap.Any("err", err))
+			vt.Stream.Close()
+		}
+	}()
+	if vt.lastSeq != vt.lastSeq2+1 && vt.lastSeq2 != 0 {
+		vt.lostFlag = true
+		vt.Warn("lost rtp packet", zap.Uint16("lastSeq", vt.lastSeq), zap.Uint16("lastSeq2", vt.lastSeq2))
+	}
+	frame := &rtpItem.Value
+	pts := frame.Timestamp
+	rv := vt.Value
+	// 有些流的 rtp 包中没有设置 marker 导致无法判断是否是最后一个包,此时通过时间戳变化判断,先 flush 之前的帧
+	if rv.PTS != time.Duration(pts) {
+		if rv.AUList.ByteLength > 0 {
+			if !vt.dcChanged && rv.IFrame {
+				vt.insertDCRtp()
+			}
+			vt.Flush()
+			rv = vt.Value
+		}
+		vt.generateTimestamp(pts)
+	}
+	rv.RTP.Push(rtpItem)
+	if naluType := frame.H264Type(); naluType < 24 {
+		vt.WriteSliceBytes(frame.Payload)
+	} else {
+		offset := naluType.Offset()
+		switch naluType {
+		case codec.NALU_STAPA, codec.NALU_STAPB:
+			if len(frame.Payload) <= offset {
+				vt.Error("invalid nalu size", zap.Int("naluType", int(naluType)))
+				return
+			}
+			for buffer := util.Buffer(frame.Payload[offset:]); buffer.CanRead(); {
+				nextSize := int(buffer.ReadUint16())
+				if buffer.Len() >= nextSize {
+					vt.WriteSliceBytes(buffer.ReadN(nextSize))
+				} else {
+					vt.Error("invalid nalu size", zap.Int("naluType", int(naluType)))
+					return
+				}
+			}
+		case codec.NALU_FUA, codec.NALU_FUB:
+			b1 := frame.Payload[1]
+			if util.Bit1(b1, 0) {
+				naluType = naluType.Parse(b1)
+				firstByte := naluType.Or(frame.Payload[0] & 0x60)
+				switch naluType {
+				case codec.NALU_SPS, codec.NALU_PPS:
+					vt.buf.WriteByte(firstByte)
+				default:
+					vt.WriteSliceByte(firstByte)
+				}
+			}
+			if vt.buf.Len() > 0 {
+				vt.buf.Write(frame.Payload[offset:])
+			} else {
+				if rv.AUList.Pre != nil && rv.AUList.Pre.Value != nil {
+					rv.AUList.Pre.Value.Push(vt.BytesPool.GetShell(frame.Payload[offset:]))
+				} else {
+					vt.Error("fu have no start")
+					return
+				}
+			}
+			if !util.Bit1(b1, 1) {
+				// fua 还没结束
+				return
+			} else if vt.buf.Len() > 0 {
+				vt.WriteAnnexB(uint32(rv.PTS), uint32(rv.DTS), vt.buf)
+				vt.buf = nil
+			}
+		}
+	}
+	if frame.Marker && rv.AUList.ByteLength > 0 {
+		if !vt.dcChanged && rv.IFrame {
+			vt.insertDCRtp()
+		}
+		vt.Flush()
+	}
+}
+
+// RTP格式补完
+func (vt *H264) CompleteRTP(value *AVFrame) {
+	var out [][][]byte
+	if value.IFrame {
+		out = append(out, [][]byte{vt.SPS}, [][]byte{vt.PPS})
+	}
+	vt.Value.AUList.Range(func(au *util.BLL) bool {
+		if au.ByteLength < RTPMTU {
+			out = append(out, au.ToBuffers())
+		} else {
+			startIndex := len(out)
+			var naluType codec.H264NALUType
+			r := au.NewReader()
+			b0, _ := r.ReadByte()
+			naluType = naluType.Parse(b0)
+			b0 = codec.NALU_FUA.Or(b0 & 0x60)
+			for bufs := r.ReadN(RTPMTU); len(bufs) > 0; bufs = r.ReadN(RTPMTU) {
+				out = append(out, append([][]byte{{b0, naluType.Byte()}}, bufs...))
+			}
+			out[startIndex][0][1] |= 1 << 7 // set start bit
+			out[len(out)-1][0][1] |= 1 << 6 // set end bit
+		}
+		return true
+	})
+	vt.PacketizeRTP(out...)
+}
+
+func (vt *H264) GetNALU_SEI() (item *util.ListItem[util.Buffer]) {
+	item = vt.BytesPool.Get(1)
+	item.Value[0] = byte(codec.NALU_SEI)
+	return
+}

+ 225 - 0
m7s.live/engine/v4@v4.14.6/track/h265.go

@@ -0,0 +1,225 @@
+package track
+
+import (
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/codec"
+	. "m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/log"
+	"m7s.live/engine/v4/util"
+)
+
+var _ SpesificTrack = (*H265)(nil)
+
+type H265 struct {
+	Video
+	VPS []byte `json:"-" yaml:"-"`
+}
+
+func NewH265(stream IStream, stuff ...any) (vt *H265) {
+	vt = &H265{}
+	vt.Video.CodecID = codec.CodecID_H265
+	vt.SetStuff("h265", byte(96), uint32(90000), vt, stuff, stream)
+	if vt.BytesPool == nil {
+		vt.BytesPool = make(util.BytesPool, 17)
+	}
+	vt.ParamaterSets = make(ParamaterSets, 3)
+	vt.nalulenSize = 4
+	vt.dtsEst = NewDTSEstimator()
+	return
+}
+
+func (vt *H265) WriteSliceBytes(slice []byte) {
+	if len(slice) == 0 {
+		vt.Error("H265 WriteSliceBytes got empty slice")
+		return
+	}
+	t := codec.ParseH265NALUType(slice[0])
+	if log.Trace {
+		vt.Trace("naluType", zap.Uint8("naluType", byte(t)))
+	}
+	switch t {
+	case codec.NAL_UNIT_VPS:
+		vt.VPS = slice
+		vt.ParamaterSets[0] = slice
+	case codec.NAL_UNIT_SPS:
+		vt.SPS = slice
+		vt.ParamaterSets[1] = slice
+		spsInfo, _ := codec.ParseHevcSPS(slice)
+		if spsInfo.Width != vt.SPSInfo.Width || spsInfo.Height != vt.SPSInfo.Height {
+			vt.Debug("SPS", zap.Any("SPSInfo", spsInfo))
+		}
+		vt.SPSInfo = spsInfo
+	case codec.NAL_UNIT_PPS:
+		vt.PPS = slice
+		vt.ParamaterSets[2] = slice
+		if vt.VPS != nil && vt.SPS != nil && vt.PPS != nil {
+			extraData, err := codec.BuildH265SeqHeaderFromVpsSpsPps(vt.VPS, vt.SPS, vt.PPS)
+			if err == nil {
+				vt.nalulenSize = (int(extraData[26]) & 0x03) + 1
+				vt.Video.WriteSequenceHead(extraData)
+			} else {
+				vt.Error("H265 BuildH265SeqHeaderFromVpsSpsPps", zap.Error(err))
+				// vt.Stream.Close()
+			}
+		}
+	case
+		codec.NAL_UNIT_CODED_SLICE_BLA,
+		codec.NAL_UNIT_CODED_SLICE_BLANT,
+		codec.NAL_UNIT_CODED_SLICE_BLA_N_LP,
+		codec.NAL_UNIT_CODED_SLICE_IDR,
+		codec.NAL_UNIT_CODED_SLICE_IDR_N_LP,
+		codec.NAL_UNIT_CODED_SLICE_CRA:
+		vt.Value.IFrame = true
+		vt.AppendAuBytes(slice)
+	case 0, 1, 2, 3, 4, 5, 6, 7, 8, 9:
+		vt.Value.IFrame = false
+		vt.AppendAuBytes(slice)
+	case codec.NAL_UNIT_SEI, codec.NAL_UNIT_SEI_SUFFIX:
+		vt.AppendAuBytes(slice)
+	case codec.NAL_UNIT_ACCESS_UNIT_DELIMITER:
+	default:
+		vt.Warn("nalu type not supported", zap.Uint("type", uint(t)))
+	}
+}
+
+func (vt *H265) WriteSequenceHead(head []byte) (err error) {
+	if vt.VPS, vt.SPS, vt.PPS, err = codec.ParseVpsSpsPpsFromSeqHeaderWithoutMalloc(head); err == nil {
+		vt.ParamaterSets[0] = vt.VPS
+		vt.ParamaterSets[1] = vt.SPS
+		vt.ParamaterSets[2] = vt.PPS
+		vt.SPSInfo, _ = codec.ParseHevcSPS(vt.SPS)
+		vt.nalulenSize = (int(head[26]) & 0x03) + 1
+		vt.Video.WriteSequenceHead(head)
+	} else {
+		vt.Error("H265 ParseVpsSpsPps Error")
+		vt.Stream.Close()
+	}
+	return
+}
+
+func (vt *H265) WriteRTPFrame(rtpItem *util.ListItem[RTPFrame]) {
+	defer func() {
+		err := recover()
+		if err != nil {
+			vt.Error("WriteRTPFrame panic", zap.Any("err", err))
+			vt.Stream.Close()
+		}
+	}()
+	frame := &rtpItem.Value
+	rv := vt.Value
+	rv.RTP.Push(rtpItem)
+	// TODO: DONL may need to be parsed if `sprop-max-don-diff` is greater than 0 on the RTP stream.
+	var usingDonlField bool
+	var buffer = util.Buffer(frame.Payload)
+	switch frame.H265Type() {
+	case codec.NAL_UNIT_RTP_AP:
+		buffer.ReadUint16()
+		if usingDonlField {
+			buffer.ReadUint16()
+		}
+		for buffer.CanRead() {
+			l := int(buffer.ReadUint16())
+			if buffer.CanReadN(l) {
+				vt.WriteSliceBytes(buffer.ReadN(l))
+			} else {
+				return
+			}
+			if usingDonlField {
+				buffer.ReadByte()
+			}
+		}
+	case codec.NAL_UNIT_RTP_FU:
+		if !buffer.CanReadN(3) {
+			return
+		}
+		first3 := buffer.ReadN(3)
+		fuHeader := first3[2]
+		if usingDonlField {
+			buffer.ReadUint16()
+		}
+		if naluType := fuHeader & 0b00111111; util.Bit1(fuHeader, 0) {
+			vt.WriteSliceByte(first3[0]&0b10000001|(naluType<<1), first3[1])
+		}
+		if rv.AUList.Pre != nil {
+			rv.AUList.Pre.Value.Push(vt.BytesPool.GetShell(buffer))
+		}
+	default:
+		vt.WriteSliceBytes(frame.Payload)
+	}
+	if frame.Marker {
+		vt.generateTimestamp(frame.Timestamp)
+		if !vt.dcChanged && rv.IFrame {
+			vt.insertDCRtp()
+		}
+		vt.Flush()
+	}
+}
+
+func (vt *H265) CompleteAVCC(rv *AVFrame) {
+	mem := vt.BytesPool.Get(8)
+	b := mem.Value
+	if rv.IFrame {
+		b[0] = 0b1001_0000 | byte(codec.PacketTypeCodedFrames)
+	} else {
+		b[0] = 0b1010_0000 | byte(codec.PacketTypeCodedFrames)
+	}
+	util.BigEndian.PutUint32(b[1:], codec.FourCC_H265_32)
+	// println(rv.PTS < rv.DTS, "\t", rv.PTS, "\t", rv.DTS, "\t", rv.PTS-rv.DTS)
+	// 写入CTS
+	util.PutBE(b[5:8], (rv.PTS-rv.DTS)/90)
+	rv.AVCC.Push(mem)
+	// if rv.AVCC.ByteLength != 5 {
+	// 	panic("error")
+	// }
+	// var tmp = 0
+	rv.AUList.Range(func(au *util.BLL) bool {
+		mem = vt.BytesPool.Get(4)
+		// println(au.ByteLength)
+		util.PutBE(mem.Value, uint32(au.ByteLength))
+		rv.AVCC.Push(mem)
+		au.Range(func(slice util.Buffer) bool {
+			rv.AVCC.Push(vt.BytesPool.GetShell(slice))
+			return true
+		})
+		// tmp += 4 + au.ByteLength
+		// if rv.AVCC.ByteLength != 5+tmp {
+		// 	panic("error")
+		// }
+		return true
+	})
+}
+
+// RTP格式补完
+func (vt *H265) CompleteRTP(value *AVFrame) {
+	// H265打包: https://blog.csdn.net/fanyun_01/article/details/114234290
+	var out [][][]byte
+	if value.IFrame {
+		out = append(out, [][]byte{vt.VPS}, [][]byte{vt.SPS}, [][]byte{vt.PPS})
+	}
+	vt.Value.AUList.Range(func(au *util.BLL) bool {
+		if au.ByteLength < RTPMTU {
+			out = append(out, au.ToBuffers())
+		} else {
+			startIndex := len(out)
+			var naluType codec.H265NALUType
+			r := au.NewReader()
+			b0, _ := r.ReadByte()
+			b1, _ := r.ReadByte()
+			naluType = naluType.Parse(b0)
+			b0 = (byte(codec.NAL_UNIT_RTP_FU) << 1) | (b0 & 0b10000001)
+			for bufs := r.ReadN(RTPMTU); len(bufs) > 0; bufs = r.ReadN(RTPMTU) {
+				out = append(out, append([][]byte{{b0, b1, byte(naluType)}}, bufs...))
+			}
+			out[startIndex][0][2] |= 1 << 7 // set start bit
+			out[len(out)-1][0][2] |= 1 << 6 // set end bit
+		}
+		return true
+	})
+	vt.PacketizeRTP(out...)
+}
+
+func (vt *H265) GetNALU_SEI() (item *util.ListItem[util.Buffer]) {
+	item = vt.BytesPool.Get(1)
+	item.Value[0] = 0b10000000 | byte(codec.NAL_UNIT_SEI<<1)
+	return
+}

+ 55 - 0
m7s.live/engine/v4@v4.14.6/track/opus.go

@@ -0,0 +1,55 @@
+package track
+
+import (
+	"github.com/pkg/errors"
+	"m7s.live/engine/v4/codec"
+	. "m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/util"
+)
+
+var _ SpesificTrack = (*Opus)(nil)
+
+func NewOpus(stream IStream, stuff ...any) (opus *Opus) {
+	opus = &Opus{}
+	opus.CodecID = codec.CodecID_OPUS
+	opus.SampleSize = 16
+	opus.Channels = 2
+	opus.AVCCHead = []byte{(byte(opus.CodecID) << 4) | (1 << 1)}
+	opus.SetStuff("opus", uint32(48000), byte(111), opus, stuff, stream)
+	if opus.BytesPool == nil {
+		opus.BytesPool = make(util.BytesPool, 17)
+	}
+	go opus.Attach()
+	return
+}
+
+type Opus struct {
+	Audio
+}
+
+func (opus *Opus) WriteAVCC(ts uint32, frame *util.BLL) error {
+	return errors.New("opus not support WriteAVCC")
+}
+
+func (opus *Opus) WriteRTPFrame(rtpItem *util.ListItem[RTPFrame]) {
+	frame := &rtpItem.Value
+	opus.Value.RTP.Push(rtpItem)
+	if opus.SampleRate != 90000 {
+		opus.generateTimestamp(uint32(uint64(frame.Timestamp) * 90000 / uint64(opus.SampleRate)))
+	}
+	opus.AppendAuBytes(frame.Payload)
+	opus.Flush()
+}
+
+func (opus *Opus) CompleteRTP(value *AVFrame) {
+	if value.AUList.ByteLength > RTPMTU {
+		var packets [][][]byte
+		r := value.AUList.Next.Value.NewReader()
+		for bufs := r.ReadN(RTPMTU); len(bufs) > 0; bufs = r.ReadN(RTPMTU) {
+			packets = append(packets, bufs)
+		}
+		opus.PacketizeRTP(packets...)
+	} else {
+		opus.Audio.CompleteRTP(value)
+	}
+}

+ 144 - 0
m7s.live/engine/v4@v4.14.6/track/reader-av.go

@@ -0,0 +1,144 @@
+package track
+
+import (
+	"errors"
+	"time"
+
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/log"
+)
+
+const (
+	READSTATE_INIT = iota
+	READSTATE_FIRST
+	READSTATE_NORMAL
+)
+const (
+	SUBMODE_REAL = iota
+	SUBMODE_NOJUMP
+	SUBMODE_BUFFER
+)
+
+var ErrDiscard = errors.New("discard")
+
+type AVRingReader struct {
+	RingReader[any, *common.AVFrame]
+	mode       int
+	Track      *Media
+	State      byte
+	FirstSeq   uint32
+	StartTs    time.Duration
+	FirstTs    time.Duration
+	SkipTs     time.Duration //ms
+	beforeJump time.Duration
+	ConfSeq    int
+	startTime  time.Time
+	AbsTime    uint32
+	Delay      uint32
+	*log.Logger
+}
+
+func (r *AVRingReader) DecConfChanged() bool {
+	return r.ConfSeq != r.Track.SequenceHeadSeq
+}
+
+func NewAVRingReader(t *Media) *AVRingReader {
+	t.Debug("reader +1", zap.Int32("count", t.ReaderCount.Add(1)))
+	return &AVRingReader{
+		Track: t,
+	}
+}
+
+func (r *AVRingReader) readFrame() (err error) {
+	err = r.ReadNext()
+	if err != nil {
+		return err
+	}
+	// 超过一半的缓冲区大小,说明Reader太慢,需要丢帧
+	if r.mode != SUBMODE_BUFFER && r.State == READSTATE_NORMAL && r.Track.LastValue.Sequence-r.Value.Sequence > uint32(r.Track.Size/2) && r.Track.IDRing != nil && r.Track.IDRing.Value.Sequence > r.Value.Sequence {
+		r.Warn("reader too slow", zap.Uint32("lastSeq", r.Track.LastValue.Sequence), zap.Uint32("seq", r.Value.Sequence))
+		return r.Read(r.Track.IDRing)
+	}
+	return
+}
+
+func (r *AVRingReader) ReadFrame(mode int) (err error) {
+	r.mode = mode
+	switch r.State {
+	case READSTATE_INIT:
+		r.Info("start read", zap.Int("mode", mode))
+		startRing := r.Track.Ring
+		if r.Track.IDRing != nil {
+			startRing = r.Track.IDRing
+		} else {
+			r.Warn("no IDRring")
+		}
+		switch mode {
+		case SUBMODE_REAL:
+			if r.Track.IDRing != nil {
+				r.State = READSTATE_FIRST
+			} else {
+				r.State = READSTATE_NORMAL
+			}
+		case SUBMODE_NOJUMP:
+			r.State = READSTATE_NORMAL
+		case SUBMODE_BUFFER:
+			if r.Track.HistoryRing != nil {
+				startRing = r.Track.HistoryRing
+			}
+			r.State = READSTATE_NORMAL
+		}
+		if err = r.StartRead(startRing); err != nil {
+			return
+		}
+		r.startTime = time.Now()
+		if r.FirstTs == 0 {
+			r.FirstTs = r.Value.Timestamp
+		}
+		r.SkipTs = r.FirstTs - r.StartTs
+		r.FirstSeq = r.Value.Sequence
+		r.Info("first frame read", zap.Duration("firstTs", r.FirstTs), zap.Uint32("firstSeq", r.FirstSeq))
+	case READSTATE_FIRST:
+		if r.Track.IDRing.Value.Sequence != r.FirstSeq {
+			if err = r.Read(r.Track.IDRing); err != nil {
+				return
+			}
+			r.SkipTs = r.Value.Timestamp - r.beforeJump - r.StartTs
+			r.Info("jump", zap.Uint32("skipSeq", r.Track.IDRing.Value.Sequence-r.FirstSeq), zap.Duration("skipTs", r.SkipTs))
+			r.State = READSTATE_NORMAL
+		} else {
+			if err = r.readFrame(); err != nil {
+				return
+			}
+			r.beforeJump = r.Value.Timestamp - r.FirstTs
+			// 防止过快消费
+			if fast := r.beforeJump - time.Since(r.startTime); fast > 0 && fast < time.Second {
+				time.Sleep(fast)
+			}
+		}
+	case READSTATE_NORMAL:
+		if err = r.readFrame(); err != nil {
+			return
+		}
+	}
+	r.AbsTime = uint32((r.Value.Timestamp - r.SkipTs).Milliseconds())
+	if r.AbsTime == 0 {
+		r.AbsTime = 1
+	}
+	// r.Delay = uint32((r.Track.LastValue.Timestamp - r.Value.Timestamp).Milliseconds())
+	r.Delay = uint32(r.Track.LastValue.Sequence - r.Value.Sequence)
+	// fmt.Println(r.Track.Name, r.Delay)
+	// fmt.Println(r.Track.Name, r.State, r.Value.Timestamp, r.SkipTs, r.AbsTime)
+	return
+}
+func (r *AVRingReader) GetPTS32() uint32 {
+	return uint32((r.Value.PTS - r.SkipTs*90/time.Millisecond))
+}
+func (r *AVRingReader) GetDTS32() uint32 {
+	return uint32((r.Value.DTS - r.SkipTs*90/time.Millisecond))
+}
+func (r *AVRingReader) ResetAbsTime() {
+	r.SkipTs = r.Value.Timestamp
+	r.AbsTime = 1
+}

+ 71 - 0
m7s.live/engine/v4@v4.14.6/track/reader-data.go

@@ -0,0 +1,71 @@
+package track
+
+import (
+	"m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/util"
+)
+
+type RingReader[T any, F common.IDataFrame[T]] struct {
+	*util.Ring[F]
+	Count int // 读取的帧数
+}
+
+func (r *RingReader[T, F]) StartRead(ring *util.Ring[F]) (err error) {
+	r.Ring = ring
+	if r.Value.IsDiscarded() {
+		return ErrDiscard
+	}
+	if r.Value.IsWriting() {
+		// t := time.Now()
+		r.Value.Wait()
+		// log.Info("wait", time.Since(t))
+	}
+	r.Count++
+	r.Value.ReaderEnter()
+	return
+}
+
+func (r *RingReader[T, F]) TryRead() (f F, err error) {
+	if r.Count > 0 {
+		preValue := r.Value
+		if preValue.IsDiscarded() {
+			preValue.ReaderLeave()
+			err = ErrDiscard
+			return
+		}
+		if r.Next().Value.IsWriting() {
+			return
+		}
+		defer preValue.ReaderLeave()
+		r.Ring = r.Next()
+	} else {
+		if r.Value.IsWriting() {
+			return
+		}
+	}
+	if r.Value.IsDiscarded() {
+		err = ErrDiscard
+		return
+	}
+	r.Count++
+	f = r.Value
+	r.Value.ReaderEnter()
+	return
+}
+
+func (r *RingReader[T, F]) ReadNext() (err error) {
+	return r.Read(r.Next())
+}
+
+func (r *RingReader[T, F]) Read(ring *util.Ring[F]) (err error) {
+	preValue := r.Value
+	defer preValue.ReaderLeave()
+	if preValue.IsDiscarded() {
+		return ErrDiscard
+	}
+	return r.StartRead(ring)
+}
+
+type DataReader[T any] struct {
+	RingReader[T, *common.DataFrame[T]]
+}

+ 104 - 0
m7s.live/engine/v4@v4.14.6/track/rtp.go

@@ -0,0 +1,104 @@
+package track
+
+import (
+	"time"
+
+	"github.com/pion/rtp"
+	"go.uber.org/zap"
+	. "m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/util"
+)
+
+const RTPMTU = 1400
+
+// WriteRTPPack 写入已反序列化的RTP包,已经排序过了的
+func (av *Media) WriteRTPPack(p *rtp.Packet) {
+	var frame RTPFrame
+	p.SSRC = av.SSRC
+	p.Padding = false
+	p.PaddingSize = 0
+	frame.Packet = p
+	av.Value.BytesIn += len(frame.Payload) + 12
+	av.lastSeq2 = av.lastSeq
+	av.lastSeq = frame.SequenceNumber
+	av.DropCount += int(av.lastSeq - av.lastSeq2 - 1)
+	if len(p.Payload) > 0 {
+		av.WriteRTPFrame(util.NewListItem(frame))
+	}
+}
+
+// WriteRTPFrame 写入未反序列化的RTP包, 未排序的
+func (av *Media) WriteRTP(raw *util.ListItem[RTPFrame]) {
+	for frame := av.recorderRTP(raw); frame != nil; frame = av.nextRTPFrame() {
+		frame.Value.SSRC = av.SSRC
+		av.Value.BytesIn += len(frame.Value.Payload) + 12
+		av.DropCount += int(av.lastSeq - av.lastSeq2 - 1)
+		if len(frame.Value.Payload) > 0 {
+			av.WriteRTPFrame(frame)
+			// av.Info("rtp", zap.Uint32("ts", (frame.Value.Timestamp)), zap.Int("len", len(frame.Value.Payload)), zap.Bool("marker", frame.Value.Marker), zap.Uint16("seq", frame.Value.SequenceNumber))
+		} else {
+			av.Debug("rtp payload is empty", zap.Uint32("ts", (frame.Value.Timestamp)), zap.Any("ext", frame.Value.GetExtensionIDs()), zap.Uint16("seq", frame.Value.SequenceNumber))
+			frame.Recycle()
+		}
+	}
+}
+
+// https://www.cnblogs.com/moonwalk/p/15903760.html
+// Packetize packetizes the payload of an RTP packet and returns one or more RTP packets
+func (av *Media) PacketizeRTP(payloads ...[][]byte) {
+	var rtpItem *util.ListItem[RTPFrame]
+	for _, pp := range payloads {
+		rtpItem = av.GetRTPFromPool()
+		packet := &rtpItem.Value
+		br := util.LimitBuffer{Buffer: packet.Payload}
+		if av.SampleRate != 90000 {
+			packet.Timestamp = uint32(time.Duration(av.SampleRate) * av.Value.PTS / 90000)
+		} else {
+			packet.Timestamp = uint32(av.Value.PTS)
+		}
+		packet.Marker = false
+		for _, p := range pp {
+			if _, err := br.Write(p); err != nil {
+				av.Error("rtp payload write error", zap.Error(err))
+				for i, pp := range payloads {
+					for j, p := range pp {
+						av.Error("rtp payload", zap.Int("i", i), zap.Int("j", j), zap.Int("len", len(p)))
+					}
+				}
+				return
+			}
+		}
+		packet.Payload = br.Bytes()
+		av.Value.RTP.Push(rtpItem)
+	}
+	// 最后一个rtp包标记为true
+	rtpItem.Value.Marker = true
+}
+
+type RTPDemuxer struct {
+	lastSeq  uint16 //上一个rtp包的序号
+	lastSeq2 uint16 //上上一个rtp包的序号
+	乱序重排     util.RTPReorder[*util.ListItem[RTPFrame]]
+}
+
+// 获取缓存中下一个rtpFrame
+func (av *RTPDemuxer) nextRTPFrame() (frame *util.ListItem[RTPFrame]) {
+	frame = av.乱序重排.Pop()
+	if frame == nil {
+		return
+	}
+	av.lastSeq2 = av.lastSeq
+	av.lastSeq = frame.Value.SequenceNumber
+	return
+}
+
+// 对RTP包乱序重排
+func (av *RTPDemuxer) recorderRTP(item *util.ListItem[RTPFrame]) (frame *util.ListItem[RTPFrame]) {
+	frame = av.乱序重排.Push(item.Value.SequenceNumber, item)
+	if frame == nil {
+		return
+	}
+	av.lastSeq2 = av.lastSeq
+	av.lastSeq = frame.Value.SequenceNumber
+	return
+}

+ 306 - 0
m7s.live/engine/v4@v4.14.6/track/video.go

@@ -0,0 +1,306 @@
+package track
+
+import (
+	"io"
+	"time"
+
+	"github.com/pion/rtp"
+	"go.uber.org/zap"
+	"m7s.live/engine/v4/codec"
+	"m7s.live/engine/v4/common"
+	. "m7s.live/engine/v4/common"
+	"m7s.live/engine/v4/util"
+)
+
+type Video struct {
+	Media
+	CodecID     codec.VideoCodecID
+	GOP         int  //关键帧间隔
+	nalulenSize int  //avcc格式中表示nalu长度的字节数,通常为4
+	dcChanged   bool //解码器配置是否改变了,一般由于变码率导致
+	dtsEst      *DTSEstimator
+	lostFlag    bool // 是否丢帧
+	codec.SPSInfo
+	ParamaterSets `json:"-" yaml:"-"`
+	SPS           []byte              `json:"-" yaml:"-"`
+	PPS           []byte              `json:"-" yaml:"-"`
+	SEIReader     *DataReader[[]byte] `json:"-" yaml:"-"`
+}
+
+func (v *Video) Attach() {
+	if v.Attached.CompareAndSwap(false, true) {
+		v.Info("attach video track", zap.Uint("width", v.Width), zap.Uint("height", v.Height))
+		if err := v.Stream.AddTrack(v).Await(); err != nil {
+			v.Error("attach video track failed", zap.Error(err))
+			v.Attached.Store(false)
+		} else {
+			v.Info("video track attached", zap.Uint("width", v.Width), zap.Uint("height", v.Height))
+		}
+	}
+}
+
+func (v *Video) Detach() {
+	if v.Attached.CompareAndSwap(true, false) {
+		v.Stream.RemoveTrack(v)
+	}
+}
+
+func (vt *Video) GetName() string {
+	if vt.Name == "" {
+		return vt.CodecID.String()
+	}
+	return vt.Name
+}
+
+// PlayFullAnnexB 订阅annex-b格式的流数据,每一个I帧增加sps、pps头
+// func (vt *Video) PlayFullAnnexB(ctx context.Context, onMedia func(net.Buffers) error) error {
+// 	for vr := vt.ReadRing(); ctx.Err() == nil; vr.MoveNext() {
+// 		vp := vr.Read(ctx)
+// 		var data net.Buffers
+// 		if vp.IFrame {
+// 			data = vt.GetAnnexB()
+// 		}
+// 		data = append(data, codec.NALU_Delimiter2)
+// 		for slice := vp.AUList.Head; slice != nil; slice = slice.Next {
+// 			data = append(data, slice.ToBuffers()...)
+// 			if slice.Next != nil {
+// 				data = append(data, codec.NALU_Delimiter1)
+// 			}
+// 		}
+
+//			if err := onMedia(data); err != nil {
+//				// TODO: log err
+//				return err
+//			}
+//		}
+//		return ctx.Err()
+//	}
+func (vt *Video) computeGOP() {
+	if vt.IDRing != nil {
+		vt.GOP = int(vt.Value.Sequence - vt.IDRing.Value.Sequence)
+		if vt.HistoryRing == nil {
+			vt.narrow(vt.GOP)
+		}
+	}
+	vt.AddIDR()
+	// var n int
+	// for i := 0; i < len(vt.BytesPool); i++ {
+	// 	n += vt.BytesPool[i].Length
+	// }
+	// println(n)
+}
+
+func (vt *Video) writeAnnexBSlice(nalu []byte) {
+	common.SplitAnnexB(nalu, vt.WriteSliceBytes, codec.NALU_Delimiter1)
+}
+
+func (vt *Video) WriteNalu(pts uint32, dts uint32, nalu []byte) {
+	if dts == 0 {
+		vt.generateTimestamp(pts)
+	} else {
+		vt.Value.PTS = time.Duration(pts)
+		vt.Value.DTS = time.Duration(dts)
+	}
+	vt.Value.BytesIn += len(nalu)
+	vt.WriteSliceBytes(nalu)
+	vt.Flush()
+}
+
+func (vt *Video) WriteAnnexB(pts uint32, dts uint32, frame []byte) {
+	if dts == 0 {
+		vt.generateTimestamp(pts)
+	} else {
+		vt.Value.PTS = time.Duration(pts)
+		vt.Value.DTS = time.Duration(dts)
+	}
+	vt.Value.BytesIn += len(frame)
+	common.SplitAnnexB(frame, vt.writeAnnexBSlice, codec.NALU_Delimiter2)
+	if vt.Value.AUList.ByteLength > 0 {
+		vt.Flush()
+	}
+}
+
+func (vt *Video) writeAVCCFrame(ts uint32, r *util.BLLReader, frame *util.BLL) (err error) {
+	var cts uint32
+	cts, err = r.ReadBE(3)
+	if err != nil {
+		return err
+	}
+	vt.Value.PTS = time.Duration(ts+cts) * 90
+	vt.Value.DTS = time.Duration(ts) * 90
+	var nalulen uint32
+	for nalulen, err = r.ReadBE(vt.nalulenSize); err == nil; nalulen, err = r.ReadBE(vt.nalulenSize) {
+		if remain := frame.ByteLength - r.GetOffset(); remain < int(nalulen) {
+			vt.Error("read nalu length error", zap.Int("nalulen", int(nalulen)), zap.Int("remain", remain))
+			frame.Recycle()
+			vt.Value.Reset()
+			return
+		}
+		vt.AppendAuBytes(r.ReadN(int(nalulen))...)
+	}
+	return nil
+}
+
+func (vt *Video) WriteAVCC(ts uint32, frame *util.BLL) (err error) {
+	if l := frame.ByteLength; l < 6 {
+		vt.Error("AVCC data too short", zap.Int("len", l))
+		return io.ErrShortWrite
+	}
+	// bbb := util.Buffer(frame.ToBytes()[5:])
+	r := frame.NewReader()
+	b, _ := r.ReadByte()
+	isExtHeader := (b >> 4) & 0b1000
+	frameType := (b >> 4) & 0b0111
+	vt.Value.IFrame = frameType == 1 || frameType == 4
+	packetType := b & 0b1111
+	if isExtHeader != 0 {
+		r.ReadBE(4) // fourcc
+		switch packetType {
+		case codec.PacketTypeSequenceStart:
+			err = vt.SpesificTrack.WriteSequenceHead(frame.ToBytes())
+			frame.Recycle()
+			return
+		case codec.PacketTypeCodedFrames:
+			err = vt.SpesificTrack.writeAVCCFrame(ts, r, frame)
+		case codec.PacketTypeCodedFramesX:
+		}
+	} else {
+		b, _ = r.ReadByte() //sequence frame flag
+		if b == 0 {
+			err = vt.SpesificTrack.WriteSequenceHead(frame.ToBytes())
+			frame.Recycle()
+			return
+		}
+		err = vt.SpesificTrack.writeAVCCFrame(ts, r, frame)
+	}
+	if err == nil {
+		vt.Value.WriteAVCC(ts, frame)
+		vt.Flush()
+	}
+	return
+}
+
+func (vt *Video) WriteSliceByte(b ...byte) {
+	// fmt.Println("write slice byte", b)
+	vt.WriteSliceBytes(b)
+}
+
+// 在I帧前面插入sps pps webrtc需要
+func (vt *Video) insertDCRtp() {
+	head := vt.Value.RTP.Next
+	for _, nalu := range vt.ParamaterSets {
+		var packet rtp.Packet
+		packet.Version = 2
+		packet.PayloadType = vt.PayloadType
+		packet.Payload = nalu
+		packet.SSRC = vt.SSRC
+		packet.Timestamp = uint32(vt.Value.PTS)
+		packet.Marker = false
+		head.InsertBeforeValue(RTPFrame{Packet: &packet})
+	}
+}
+
+func (vt *Video) generateTimestamp(ts uint32) {
+	if vt.State == TrackStateOffline {
+		vt.dtsEst = NewDTSEstimator()
+	}
+	vt.Value.PTS = time.Duration(ts)
+	vt.Value.DTS = time.Duration(vt.dtsEst.Feed(ts))
+}
+
+func (vt *Video) SetLostFlag() {
+	vt.lostFlag = true
+}
+
+func (vt *Video) CompleteAVCC(rv *AVFrame) {
+	mem := vt.BytesPool.Get(5)
+	b := mem.Value
+	if rv.IFrame {
+		b[0] = 0x10 | byte(vt.CodecID)
+	} else {
+		b[0] = 0x20 | byte(vt.CodecID)
+	}
+	b[1] = 1
+	// println(rv.PTS < rv.DTS, "\t", rv.PTS, "\t", rv.DTS, "\t", rv.PTS-rv.DTS)
+	// 写入CTS
+	util.PutBE(b[2:5], (rv.PTS-rv.DTS)/90)
+	rv.AVCC.Push(mem)
+	// if rv.AVCC.ByteLength != 5 {
+	// 	panic("error")
+	// }
+	// var tmp = 0
+	rv.AUList.Range(func(au *util.BLL) bool {
+		mem = vt.BytesPool.Get(4)
+		// println(au.ByteLength)
+		util.PutBE(mem.Value, uint32(au.ByteLength))
+		rv.AVCC.Push(mem)
+		au.Range(func(slice util.Buffer) bool {
+			rv.AVCC.Push(vt.BytesPool.GetShell(slice))
+			return true
+		})
+		// tmp += 4 + au.ByteLength
+		// if rv.AVCC.ByteLength != 5+tmp {
+		// 	panic("error")
+		// }
+		return true
+	})
+}
+
+func (vt *Video) Flush() {
+	rv := vt.Value
+	if vt.SEIReader != nil {
+		if seiFrame, err := vt.SEIReader.TryRead(); seiFrame != nil {
+			var au util.BLL
+			au.Push(vt.SpesificTrack.GetNALU_SEI())
+			au.Push(vt.BytesPool.GetShell(seiFrame.Data))
+			vt.Value.AUList.UnshiftValue(&au)
+		} else if err != nil {
+			vt.SEIReader = nil
+		}
+	}
+	if rv.IFrame {
+		vt.computeGOP()
+		vt.Stream.SetIDR(vt)
+	}
+
+	if !vt.Attached.Load() {
+		if vt.IDRing != nil && vt.SequenceHeadSeq > 0 {
+			defer vt.Attach()
+		} else {
+			rv.Reset()
+			return
+		}
+	}
+
+	if vt.lostFlag {
+		if rv.IFrame {
+			vt.lostFlag = false
+		} else {
+			rv.Reset()
+			return
+		}
+	}
+	vt.Media.Flush()
+	vt.dcChanged = false
+}
+
+func (vt *Video) WriteSequenceHead(sh []byte) {
+	vt.Media.WriteSequenceHead(sh)
+	vt.dcChanged = true
+}
+
+/*
+Access Unit的首个nalu是4字节起始码。
+这里举个例子说明,用JM可以生成这样一段码流(不要使用JM8.6,它在这部分与标准不符),这个码流可以见本楼附件:
+    SPS          (4字节头)
+    PPS          (4字节头)
+    SEI          (4字节头)
+    I0(slice0)     (4字节头)
+    I0(slice1)   (3字节头)
+    P1(slice0)     (4字节头)
+    P1(slice1)   (3字节头)
+    P2(slice0)     (4字节头)
+    P2(slice1)   (3字节头)
+I0(slice0)是序列第一帧(I帧)的第一个slice,是当前Access Unit的首个nalu,所以是4字节头。而I0(slice1)表示第一帧的第二个slice,所以是3字节头。P1(slice0) 、P1(slice1)同理。
+
+*/

+ 283 - 0
m7s.live/engine/v4@v4.14.6/util/amf.go

@@ -0,0 +1,283 @@
+package util
+
+import (
+	"fmt"
+	"io"
+	"reflect"
+)
+
+// Action Message Format -- AMF 0
+// Action Message Format -- AMF 3
+// http://download.macromedia.com/pub/labs/amf/amf0_spec_121207.pdf
+// http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/amf/pdf/amf-file-format-spec.pdf
+
+// AMF Object == AMF Object Type(1 byte) + AMF Object Value
+//
+// AMF Object Value :
+// AMF0_STRING : 2 bytes(datasize,记录string的长度) + data(string)
+// AMF0_OBJECT : AMF0_STRING + AMF Object
+// AMF0_NULL : 0 byte
+// AMF0_NUMBER : 8 bytes
+// AMF0_DATE : 10 bytes
+// AMF0_BOOLEAN : 1 byte
+// AMF0_ECMA_ARRAY : 4 bytes(arraysize,记录数组的长度) + AMF0_OBJECT
+// AMF0_STRICT_ARRAY : 4 bytes(arraysize,记录数组的长度) + AMF Object
+
+// 实际测试时,AMF0_ECMA_ARRAY数据如下:
+// 8 0 0 0 13 0 8 100 117 114 97 116 105 111 110 0 0 0 0 0 0 0 0 0 0 5 119 105 100 116 104 0 64 158 0 0 0 0 0 0 0 6 104 101 105 103 104 116 0 64 144 224 0 0 0 0 0
+// 8 0 0 0 13 | { 0 8 100 117 114 97 116 105 111 110 --- 0 0 0 0 0 0 0 0 0 } | { 0 5 119 105 100 116 104 --- 0 64 158 0 0 0 0 0 0 } | { 0 6 104 101 105 103 104 116 --- 0 64 144 224 0 0 0 0 0 } |...
+// 13 | {AMF0_STRING --- AMF0_NUMBER} | {AMF0_STRING --- AMF0_NUMBER} | {AMF0_STRING --- AMF0_NUMBER} | ...
+// 13 | {AMF0_OBJECT} | {AMF0_OBJECT} | {AMF0_OBJECT} | ...
+// 13 | {duration --- 0} | {width --- 1920} | {height --- 1080} | ...
+
+const (
+	AMF0_NUMBER = iota // 浮点数
+	AMF0_BOOLEAN
+	AMF0_STRING
+	AMF0_OBJECT
+	AMF0_MOVIECLIP
+	AMF0_NULL
+	AMF0_UNDEFINED
+	AMF0_REFERENCE
+	AMF0_ECMA_ARRAY
+	AMF0_END_OBJECT
+	AMF0_STRICT_ARRAY
+	AMF0_DATE
+	AMF0_LONG_STRING
+	AMF0_UNSUPPORTED
+	AMF0_RECORDSET
+	AMF0_XML_DOCUMENT
+	AMF0_TYPED_OBJECT
+	AMF0_AVMPLUS_OBJECT
+)
+const (
+	AMF3_UNDEFINED = iota
+	AMF3_NULL
+	AMF3_FALSE
+	AMF3_TRUE
+	AMF3_INTEGER
+	AMF3_DOUBLE
+	AMF3_STRING
+	AMF3_XML_DOC
+	AMF3_DATE
+	AMF3_ARRAY
+	AMF3_OBJECT
+	AMF3_XML
+	AMF3_BYTE_ARRAY
+	AMF3_VECTOR_INT
+	AMF3_VECTOR_UINT
+	AMF3_VECTOR_DOUBLE
+	AMF3_VECTOR_OBJECT
+	AMF3_DICTIONARY
+)
+
+var (
+	END_OBJ   = []byte{0, 0, AMF0_END_OBJECT}
+	ObjectEnd = &struct{}{}
+	Undefined = &struct{}{}
+)
+
+type EcmaArray map[string]any
+
+type AMF struct {
+	Buffer
+}
+
+func ReadAMF[T string | float64 | bool | map[string]any](amf *AMF) (result T) {
+	value, err := amf.Unmarshal()
+	if err != nil {
+		return
+	}
+	result, _ = value.(T)
+	return
+}
+
+func (amf *AMF) ReadShortString() (result string) {
+	return ReadAMF[string](amf)
+}
+
+func (amf *AMF) ReadNumber() (result float64) {
+	return ReadAMF[float64](amf)
+}
+
+func (amf *AMF) ReadObject() (result map[string]any) {
+	return ReadAMF[map[string]any](amf)
+}
+
+func (amf *AMF) ReadBool() (result bool) {
+	return ReadAMF[bool](amf)
+}
+
+func (amf *AMF) readKey() (string, error) {
+	if !amf.CanReadN(2) {
+		return "", io.ErrUnexpectedEOF
+	}
+	l := int(amf.ReadUint16())
+	if !amf.CanReadN(l) {
+		return "", io.ErrUnexpectedEOF
+	}
+	return string(amf.ReadN(l)), nil
+}
+
+func (amf *AMF) readProperty(m map[string]any) (obj any, err error) {
+	var k string
+	var v any
+	if k, err = amf.readKey(); err == nil {
+		if v, err = amf.Unmarshal(); k == "" && v == ObjectEnd {
+			obj = m
+		} else if err == nil {
+			m[k] = v
+		}
+	}
+	return
+}
+
+func (amf *AMF) Unmarshal() (obj any, err error) {
+	if !amf.CanRead() {
+		return nil, io.ErrUnexpectedEOF
+	}
+	defer func(b Buffer) {
+		if err != nil {
+			amf.Buffer = b
+		}
+	}(amf.Buffer)
+	switch t := amf.ReadByte(); t {
+	case AMF0_NUMBER:
+		if !amf.CanReadN(8) {
+			return 0, io.ErrUnexpectedEOF
+		}
+		obj = amf.ReadFloat64()
+	case AMF0_BOOLEAN:
+		if !amf.CanRead() {
+			return false, io.ErrUnexpectedEOF
+		}
+		obj = amf.ReadByte() == 1
+	case AMF0_STRING:
+		obj, err = amf.readKey()
+	case AMF0_OBJECT:
+		m := make(map[string]any)
+		for err == nil && obj == nil {
+			obj, err = amf.readProperty(m)
+		}
+	case AMF0_NULL:
+		return nil, nil
+	case AMF0_UNDEFINED:
+		return Undefined, nil
+	case AMF0_ECMA_ARRAY:
+		size := amf.ReadUint32()
+		m := make(EcmaArray)
+		for i := uint32(0); i < size && err == nil && obj == nil; i++ {
+			obj, err = amf.readProperty(m)
+		}
+	case AMF0_END_OBJECT:
+		return ObjectEnd, nil
+	case AMF0_STRICT_ARRAY:
+		size := amf.ReadUint32()
+		var list []any
+		for i := uint32(0); i < size; i++ {
+			v, err := amf.Unmarshal()
+			if err != nil {
+				return nil, err
+			}
+			list = append(list, v)
+		}
+		obj = list
+	case AMF0_DATE:
+		if !amf.CanReadN(10) {
+			return 0, io.ErrUnexpectedEOF
+		}
+		obj = amf.ReadFloat64()
+		amf.ReadN(2)
+	case AMF0_LONG_STRING,
+		AMF0_XML_DOCUMENT:
+		if !amf.CanReadN(4) {
+			return "", io.ErrUnexpectedEOF
+		}
+		l := int(amf.ReadUint32())
+		if !amf.CanReadN(l) {
+			return "", io.ErrUnexpectedEOF
+		}
+		obj = string(amf.ReadN(l))
+	default:
+		err = fmt.Errorf("unsupported type:%d", t)
+	}
+	return
+}
+
+func (amf *AMF) writeProperty(key string, v any) {
+	amf.WriteUint16(uint16(len(key)))
+	amf.WriteString(key)
+	amf.Marshal(v)
+}
+
+func MarshalAMFs(v ...any) []byte {
+	var amf AMF
+	return amf.Marshals(v...)
+}
+
+func (amf *AMF) Marshals(v ...any) []byte {
+	for _, vv := range v {
+		amf.Marshal(vv)
+	}
+	return amf.Buffer
+}
+
+func (amf *AMF) Marshal(v any) []byte {
+	if v == nil {
+		amf.WriteByte(AMF0_NULL)
+		return amf.Buffer
+	}
+	switch vv := v.(type) {
+	case string:
+		if l := len(vv); l > 0xFFFF {
+			amf.WriteByte(AMF0_LONG_STRING)
+			amf.WriteUint32(uint32(l))
+		} else {
+			amf.WriteByte(AMF0_STRING)
+			amf.WriteUint16(uint16(l))
+		}
+		amf.WriteString(vv)
+	case float64, uint, float32, int, int16, int32, int64, uint16, uint32, uint64, uint8, int8:
+		amf.WriteByte(AMF0_NUMBER)
+		amf.WriteFloat64(ToFloat64(vv))
+	case bool:
+		amf.WriteByte(AMF0_BOOLEAN)
+		if vv {
+			amf.WriteByte(1)
+		} else {
+			amf.WriteByte(0)
+		}
+	case EcmaArray:
+		amf.WriteByte(AMF0_ECMA_ARRAY)
+		amf.WriteUint32(uint32(len(vv)))
+		for k, v := range vv {
+			amf.writeProperty(k, v)
+		}
+		amf.Write(END_OBJ)
+	case map[string]any:
+		amf.WriteByte(AMF0_OBJECT)
+		for k, v := range vv {
+			amf.writeProperty(k, v)
+		}
+		amf.Write(END_OBJ)
+	default:
+		v := reflect.ValueOf(vv)
+		if !v.IsValid() {
+			amf.WriteByte(AMF0_NULL)
+			return amf.Buffer
+		}
+		switch v.Kind() {
+		case reflect.Slice, reflect.Array:
+			amf.WriteByte(AMF0_STRICT_ARRAY)
+			size := v.Len()
+			amf.WriteUint32(uint32(size))
+			for i := 0; i < size; i++ {
+				amf.Marshal(v.Index(i).Interface())
+			}
+			amf.Write(END_OBJ)
+		default:
+			panic("amf Marshal faild")
+		}
+	}
+	return amf.Buffer
+}

+ 28 - 0
m7s.live/engine/v4@v4.14.6/util/big_endian.go

@@ -0,0 +1,28 @@
+package util
+
+type Integer interface {
+	~int | ~int16 | ~int32 | ~int64 | ~uint | ~uint16 | ~uint32 | ~uint64
+}
+
+func PutBE[T Integer](b []byte, num T) []byte {
+	for i, n := 0, len(b); i < n; i++ {
+		b[i] = byte(num >> ((n - i - 1) << 3))
+	}
+	return b
+}
+
+func ReadBE[T Integer](b []byte) (num T) {
+	num = 0
+	for i, n := 0, len(b); i < n; i++ {
+		num += T(b[i]) << ((n - i - 1) << 3)
+	}
+	return
+}
+
+func GetBE[T Integer](b []byte, num *T) T {
+	*num = 0
+	for i, n := 0, len(b); i < n; i++ {
+		*num += T(b[i]) << ((n - i - 1) << 3)
+	}
+	return *num
+}

+ 269 - 0
m7s.live/engine/v4@v4.14.6/util/big_little_endian.go

@@ -0,0 +1,269 @@
+package util
+
+//
+// 注意:RTMP模式下都是大端模式
+//
+
+var LittleEndian littleEndian
+
+// BigEndian is the big-endian implementation of ByteOrder.
+var BigEndian bigEndian
+
+// 低位字节排放在内存的低地址端,高位字节排放在内存的高地址端.
+type littleEndian struct{}
+
+// b == 0x1234, b[0] == 0x12, b[1] == 0x34
+// b[0]低字节 b[1]高字节
+// 内存地址 低 -> 高
+// 0x34 0x12
+
+// byte(v)低字节 b[0]内存低地址
+// byte(v>>8)高字节 b[1]内存高地址
+
+// b == 2222 2222 1111 1111
+// b >> 8 -> 0000 0000 2222 2222
+// b << 8 -> 1111 1111 0000 0000
+
+func (littleEndian) Uint16(b []byte) uint16 { return uint16(b[0]) | uint16(b[1])<<8 }
+func (littleEndian) Uint24(b []byte) uint32 { return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 }
+func (littleEndian) Uint32(b []byte) uint32 {
+	return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24
+}
+func (littleEndian) Uint40(b []byte) uint64 {
+	return uint64(b[0]) | uint64(b[1])<<8 |
+		uint64(b[2])<<16 | uint64(b[3])<<24 | uint64(b[4])<<32
+}
+func (littleEndian) Uint48(b []byte) uint64 {
+	return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 |
+		uint64(b[3])<<24 | uint64(b[4])<<32 | uint64(b[5])<<40
+}
+func (littleEndian) Uint64(b []byte) uint64 {
+	return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 |
+		uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56
+}
+
+//
+// Put
+//
+
+func (littleEndian) PutUint16(b []byte, v uint16) {
+	b[0] = byte(v)
+	b[1] = byte(v >> 8)
+}
+func (littleEndian) PutUint24(b []byte, v uint32) {
+	b[0] = byte(v)
+	b[1] = byte(v >> 8)
+	b[2] = byte(v >> 16)
+}
+func (littleEndian) PutUint32(b []byte, v uint32) {
+	b[0] = byte(v)
+	b[1] = byte(v >> 8)
+	b[2] = byte(v >> 16)
+	b[3] = byte(v >> 24)
+}
+func (littleEndian) PutUint64(b []byte, v uint64) {
+	b[0] = byte(v)
+	b[1] = byte(v >> 8)
+	b[2] = byte(v >> 16)
+	b[3] = byte(v >> 24)
+	b[4] = byte(v >> 32)
+	b[5] = byte(v >> 40)
+	b[6] = byte(v >> 48)
+	b[7] = byte(v >> 56)
+}
+
+//
+// To
+//
+
+func (littleEndian) ToUint16(v uint16) []byte {
+	b := make([]byte, 2)
+	b[0] = byte(v)
+	b[1] = byte(v >> 8)
+	return b
+}
+func (littleEndian) ToUint24(v uint32) []byte {
+	b := make([]byte, 3)
+	b[0] = byte(v)
+	b[1] = byte(v >> 8)
+	b[2] = byte(v >> 16)
+	return b
+}
+func (littleEndian) ToUint32(v uint32) []byte {
+	b := make([]byte, 4)
+	b[0] = byte(v)
+	b[1] = byte(v >> 8)
+	b[2] = byte(v >> 16)
+	b[3] = byte(v >> 24)
+	return b
+}
+func (littleEndian) ToUint40(v uint64) []byte {
+	b := make([]byte, 5)
+	b[0] = byte(v)
+	b[1] = byte(v >> 8)
+	b[2] = byte(v >> 16)
+	b[3] = byte(v >> 24)
+	b[4] = byte(v >> 32)
+	return b
+}
+func (littleEndian) ToUint48(v uint64) []byte {
+	b := make([]byte, 6)
+	b[0] = byte(v)
+	b[1] = byte(v >> 8)
+	b[2] = byte(v >> 16)
+	b[3] = byte(v >> 24)
+	b[4] = byte(v >> 32)
+	b[5] = byte(v >> 40)
+	return b
+}
+func (littleEndian) ToUint64(v uint64) []byte {
+	b := make([]byte, 8)
+	b[0] = byte(v)
+	b[1] = byte(v >> 8)
+	b[2] = byte(v >> 16)
+	b[3] = byte(v >> 24)
+	b[4] = byte(v >> 32)
+	b[5] = byte(v >> 40)
+	b[6] = byte(v >> 48)
+	b[7] = byte(v >> 56)
+	return b
+}
+
+// 高位字节排放在内存的低地址端,低位字节排放在内存的高地址端
+type bigEndian struct{}
+
+// b == 0x1234, b[0] == 0x12, b[1] == 0x34
+// 内存地址 低 -> 高
+// 0x12 0x34
+func (bigEndian) Uint16(b []byte) uint16 { return uint16(b[1]) | uint16(b[0])<<8 }
+func (bigEndian) Uint24(b []byte) uint32 { return uint32(b[2]) | uint32(b[1])<<8 | uint32(b[0])<<16 }
+func (bigEndian) Uint32(b []byte) uint32 {
+	return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24
+}
+func (bigEndian) Uint40(b []byte) uint64 {
+	return uint64(b[4]) | uint64(b[3])<<8 |
+		uint64(b[2])<<16 | uint64(b[1])<<24 | uint64(b[0])<<32
+}
+func (bigEndian) Uint48(b []byte) uint64 {
+	return uint64(b[5]) | uint64(b[4])<<8 | uint64(b[3])<<16 |
+		uint64(b[2])<<24 | uint64(b[1])<<32 | uint64(b[0])<<40
+}
+func (bigEndian) Uint64(b []byte) uint64 {
+	return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 |
+		uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56
+}
+
+//
+// Put
+//
+
+func (bigEndian) PutUint16(b []byte, v uint16) {
+	b[0] = byte(v >> 8)
+	b[1] = byte(v)
+}
+func (bigEndian) PutUint24(b []byte, v uint32) {
+	b[0] = byte(v >> 16)
+	b[1] = byte(v >> 8)
+	b[2] = byte(v)
+}
+func (bigEndian) PutUint32(b []byte, v uint32) {
+	b[0] = byte(v >> 24)
+	b[1] = byte(v >> 16)
+	b[2] = byte(v >> 8)
+	b[3] = byte(v)
+}
+func (bigEndian) PutUint64(b []byte, v uint64) {
+	b[0] = byte(v >> 56)
+	b[1] = byte(v >> 48)
+	b[2] = byte(v >> 40)
+	b[3] = byte(v >> 32)
+	b[4] = byte(v >> 24)
+	b[5] = byte(v >> 16)
+	b[6] = byte(v >> 8)
+	b[7] = byte(v)
+}
+
+//
+// To
+//
+
+func (bigEndian) ToUint16(v uint16) []byte {
+	b := make([]byte, 2)
+	b[0] = byte(v >> 8)
+	b[1] = byte(v)
+	return b
+}
+func (bigEndian) ToUint24(v uint32) []byte {
+	b := make([]byte, 3)
+	b[0] = byte(v >> 16)
+	b[1] = byte(v >> 8)
+	b[2] = byte(v)
+	return b
+}
+func (bigEndian) ToUint32(v uint32) []byte {
+	b := make([]byte, 4)
+	b[0] = byte(v >> 24)
+	b[1] = byte(v >> 16)
+	b[2] = byte(v >> 8)
+	b[3] = byte(v)
+	return b
+}
+func (bigEndian) ToUint40(v uint64) []byte {
+	b := make([]byte, 5)
+	b[0] = byte(v >> 32)
+	b[1] = byte(v >> 24)
+	b[2] = byte(v >> 16)
+	b[3] = byte(v >> 8)
+	b[4] = byte(v)
+	return b
+}
+func (bigEndian) ToUint48(v uint64) []byte {
+	b := make([]byte, 6)
+	b[0] = byte(v >> 40)
+	b[1] = byte(v >> 32)
+	b[2] = byte(v >> 24)
+	b[3] = byte(v >> 16)
+	b[4] = byte(v >> 8)
+	b[5] = byte(v)
+	return b
+}
+func (bigEndian) ToUint64(v uint64) []byte {
+	b := make([]byte, 8)
+	b[0] = byte(v >> 56)
+	b[1] = byte(v >> 48)
+	b[2] = byte(v >> 40)
+	b[3] = byte(v >> 32)
+	b[4] = byte(v >> 24)
+	b[5] = byte(v >> 16)
+	b[6] = byte(v >> 8)
+	b[7] = byte(v)
+	return b
+}
+
+//哥伦布解码
+func GetUev(buff []byte, start int) (value int, pos int) {
+	l := len(buff)
+	var nZeroNum uint = 0
+	for start < l*8 {
+		if (buff[start/8] & (0x80 >> uint(start%8))) > 0 {
+			break
+		}
+		nZeroNum += 1
+		start += 1
+	}
+	dwRet := 0
+	start += 1
+	var i uint
+	for i = 0; i < nZeroNum; i++ {
+		dwRet <<= 1
+		if (buff[start/8] & (0x80 >> uint(start%8))) > 0 {
+			dwRet += 1
+		}
+		start += 1
+	}
+	return (1 << nZeroNum) - 1 + dwRet, start
+}
+
+func BigLittleSwap(v uint) uint {
+	return (v >> 24) | ((v>>16)&0xff)<<8 | ((v>>8)&0xff)<<16 | (v&0xff)<<24
+}

+ 118 - 0
m7s.live/engine/v4@v4.14.6/util/bits/bits.go

@@ -0,0 +1,118 @@
+package bits
+
+import (
+	"io"
+)
+
+type Reader struct {
+	R    io.Reader
+	n    int
+	bits uint64
+}
+
+func (self *Reader) ReadBits64(n int) (bits uint64, err error) {
+	if self.n < n {
+		var b [8]byte
+		var got int
+		want := (n - self.n + 7) / 8
+		if got, err = self.R.Read(b[:want]); err != nil {
+			return
+		}
+		if got < want {
+			err = io.EOF
+			return
+		}
+		for i := 0; i < got; i++ {
+			self.bits <<= 8
+			self.bits |= uint64(b[i])
+		}
+		self.n += got * 8
+	}
+	bits = self.bits >> uint(self.n-n)
+	self.bits ^= bits << uint(self.n-n)
+	self.n -= n
+	return
+}
+
+func (self *Reader) ReadBits(n int) (bits uint, err error) {
+	var bits64 uint64
+	if bits64, err = self.ReadBits64(n); err != nil {
+		return
+	}
+	bits = uint(bits64)
+	return
+}
+
+func (self *Reader) Read(p []byte) (n int, err error) {
+	for n < len(p) {
+		want := 8
+		if len(p)-n < want {
+			want = len(p) - n
+		}
+		var bits uint64
+		if bits, err = self.ReadBits64(want * 8); err != nil {
+			break
+		}
+		for i := 0; i < want; i++ {
+			p[n+i] = byte(bits >> uint((want-i-1)*8))
+		}
+		n += want
+	}
+	return
+}
+
+type Writer struct {
+	W    io.Writer
+	n    int
+	bits uint64
+}
+
+func (self *Writer) WriteBits64(bits uint64, n int) (err error) {
+	if self.n+n > 64 {
+		move := uint(64 - self.n)
+		mask := bits >> move
+		self.bits = (self.bits << move) | mask
+		self.n = 64
+		if err = self.FlushBits(); err != nil {
+			return
+		}
+		n -= int(move)
+		bits ^= (mask << move)
+	}
+	self.bits = (self.bits << uint(n)) | bits
+	self.n += n
+	return
+}
+
+func (self *Writer) WriteBits(bits uint, n int) (err error) {
+	return self.WriteBits64(uint64(bits), n)
+}
+
+func (self *Writer) Write(p []byte) (n int, err error) {
+	for n < len(p) {
+		if err = self.WriteBits64(uint64(p[n]), 8); err != nil {
+			return
+		}
+		n++
+	}
+	return
+}
+
+func (self *Writer) FlushBits() (err error) {
+	if self.n > 0 {
+		var b [8]byte
+		bits := self.bits
+		if self.n%8 != 0 {
+			bits <<= uint(8 - (self.n % 8))
+		}
+		want := (self.n + 7) / 8
+		for i := 0; i < want; i++ {
+			b[i] = byte(bits >> uint((want-i-1)*8))
+		}
+		if _, err = self.W.Write(b[:want]); err != nil {
+			return
+		}
+		self.n = 0
+	}
+	return
+}

+ 61 - 0
m7s.live/engine/v4@v4.14.6/util/bits/bits_test.go

@@ -0,0 +1,61 @@
+package bits
+
+import (
+	"bytes"
+	"testing"
+)
+
+func TestBits(t *testing.T) {
+	rdata := []byte{0xf3, 0xb3, 0x45, 0x60}
+	rbuf := bytes.NewReader(rdata[:])
+	r := &Reader{R: rbuf}
+	var u32 uint
+	if u32, _ = r.ReadBits(4); u32 != 0xf {
+		t.FailNow()
+	}
+	if u32, _ = r.ReadBits(4); u32 != 0x3 {
+		t.FailNow()
+	}
+	if u32, _ = r.ReadBits(2); u32 != 0x2 {
+		t.FailNow()
+	}
+	if u32, _ = r.ReadBits(2); u32 != 0x3 {
+		t.FailNow()
+	}
+	b := make([]byte, 2)
+	if r.Read(b); b[0] != 0x34 || b[1] != 0x56 {
+		t.FailNow()
+	}
+
+	wbuf := &bytes.Buffer{}
+	w := &Writer{W: wbuf}
+	w.WriteBits(0xf, 4)
+	w.WriteBits(0x3, 4)
+	w.WriteBits(0x2, 2)
+	w.WriteBits(0x3, 2)
+	n, _ := w.Write([]byte{0x34, 0x56})
+	if n != 2 {
+		t.FailNow()
+	}
+	w.FlushBits()
+	wdata := wbuf.Bytes()
+	if wdata[0] != 0xf3 || wdata[1] != 0xb3 || wdata[2] != 0x45 || wdata[3] != 0x60 {
+		t.FailNow()
+	}
+
+	b = make([]byte, 8)
+	PutUInt64BE(b, 0x11223344)
+	if b[0] != 0x11 || b[1] != 0x22 || b[2] != 0x33 || b[3] != 0x44 {
+		t.FailNow()
+	}
+}
+func PutUInt64BE(b []byte, v uint64) {
+	b[0] = byte(v >> 56)
+	b[1] = byte(v >> 48)
+	b[2] = byte(v >> 40)
+	b[3] = byte(v >> 32)
+	b[4] = byte(v >> 24)
+	b[5] = byte(v >> 16)
+	b[6] = byte(v >> 8)
+	b[7] = byte(v)
+}

+ 22 - 0
m7s.live/engine/v4@v4.14.6/util/bits/bufio/bufio.go

@@ -0,0 +1,22 @@
+package bufio
+
+import (
+	"io"
+)
+
+type Reader struct {
+	buf [][]byte
+	R   io.ReadSeeker
+}
+
+func NewReaderSize(r io.ReadSeeker, size int) *Reader {
+	buf := make([]byte, size*2)
+	return &Reader{
+		R:   r,
+		buf: [][]byte{buf[0:size], buf[size:]},
+	}
+}
+
+func (self *Reader) ReadAt(b []byte, off int64) (n int, err error) {
+	return
+}

+ 65 - 0
m7s.live/engine/v4@v4.14.6/util/bits/golomb_reader.go

@@ -0,0 +1,65 @@
+package bits
+
+import (
+	"io"
+)
+
+type GolombBitReader struct {
+	R    io.Reader
+	buf  [1]byte
+	left byte
+}
+
+func (self *GolombBitReader) ReadBit() (res uint, err error) {
+	if self.left == 0 {
+		if _, err = self.R.Read(self.buf[:]); err != nil {
+			return
+		}
+		self.left = 8
+	}
+	self.left--
+	res = uint(self.buf[0]>>self.left) & 1
+	return
+}
+
+func (self *GolombBitReader) ReadBits(n int) (res uint, err error) {
+	for i := 0; i < n; i++ {
+		var bit uint
+		if bit, err = self.ReadBit(); err != nil {
+			return
+		}
+		res |= bit << uint(n-i-1)
+	}
+	return
+}
+
+func (self *GolombBitReader) ReadExponentialGolombCode() (res uint, err error) {
+	i := 0
+	for {
+		var bit uint
+		if bit, err = self.ReadBit(); err != nil {
+			return
+		}
+		if !(bit == 0 && i < 32) {
+			break
+		}
+		i++
+	}
+	if res, err = self.ReadBits(i); err != nil {
+		return
+	}
+	res += (1 << uint(i)) - 1
+	return
+}
+
+func (self *GolombBitReader) ReadSE() (res uint, err error) {
+	if res, err = self.ReadExponentialGolombCode(); err != nil {
+		return
+	}
+	if res&0x01 != 0 {
+		res = (res + 1) / 2
+	} else {
+		res = -res / 2
+	}
+	return
+}

+ 3 - 0
m7s.live/engine/v4@v4.14.6/util/bits/pio/pio.go

@@ -0,0 +1,3 @@
+package pio
+
+var RecommendBufioSize = 1024 * 64

+ 121 - 0
m7s.live/engine/v4@v4.14.6/util/bits/pio/reader.go

@@ -0,0 +1,121 @@
+package pio
+
+func U8(b []byte) (i uint8) {
+	return b[0]
+}
+
+func U16BE(b []byte) (i uint16) {
+	i = uint16(b[0])
+	i <<= 8
+	i |= uint16(b[1])
+	return
+}
+
+func I16BE(b []byte) (i int16) {
+	i = int16(b[0])
+	i <<= 8
+	i |= int16(b[1])
+	return
+}
+
+func I24BE(b []byte) (i int32) {
+	i = int32(int8(b[0]))
+	i <<= 8
+	i |= int32(b[1])
+	i <<= 8
+	i |= int32(b[2])
+	return
+}
+
+func U24BE(b []byte) (i uint32) {
+	i = uint32(b[0])
+	i <<= 8
+	i |= uint32(b[1])
+	i <<= 8
+	i |= uint32(b[2])
+	return
+}
+
+func I32BE(b []byte) (i int32) {
+	i = int32(int8(b[0]))
+	i <<= 8
+	i |= int32(b[1])
+	i <<= 8
+	i |= int32(b[2])
+	i <<= 8
+	i |= int32(b[3])
+	return
+}
+
+func U32LE(b []byte) (i uint32) {
+	i = uint32(b[3])
+	i <<= 8
+	i |= uint32(b[2])
+	i <<= 8
+	i |= uint32(b[1])
+	i <<= 8
+	i |= uint32(b[0])
+	return
+}
+
+func U32BE(b []byte) (i uint32) {
+	i = uint32(b[0])
+	i <<= 8
+	i |= uint32(b[1])
+	i <<= 8
+	i |= uint32(b[2])
+	i <<= 8
+	i |= uint32(b[3])
+	return
+}
+
+func U40BE(b []byte) (i uint64) {
+	i = uint64(b[0])
+	i <<= 8
+	i |= uint64(b[1])
+	i <<= 8
+	i |= uint64(b[2])
+	i <<= 8
+	i |= uint64(b[3])
+	i <<= 8
+	i |= uint64(b[4])
+	return
+}
+
+func U64BE(b []byte) (i uint64) {
+	i = uint64(b[0])
+	i <<= 8
+	i |= uint64(b[1])
+	i <<= 8
+	i |= uint64(b[2])
+	i <<= 8
+	i |= uint64(b[3])
+	i <<= 8
+	i |= uint64(b[4])
+	i <<= 8
+	i |= uint64(b[5])
+	i <<= 8
+	i |= uint64(b[6])
+	i <<= 8
+	i |= uint64(b[7])
+	return
+}
+
+func I64BE(b []byte) (i int64) {
+	i = int64(int8(b[0]))
+	i <<= 8
+	i |= int64(b[1])
+	i <<= 8
+	i |= int64(b[2])
+	i <<= 8
+	i |= int64(b[3])
+	i <<= 8
+	i |= int64(b[4])
+	i <<= 8
+	i |= int64(b[5])
+	i <<= 8
+	i |= int64(b[6])
+	i <<= 8
+	i |= int64(b[7])
+	return
+}

+ 68 - 0
m7s.live/engine/v4@v4.14.6/util/bits/pio/vec.go

@@ -0,0 +1,68 @@
+package pio
+
+func VecLen(vec [][]byte) (n int) {
+	for _, b := range vec {
+		n += len(b)
+	}
+	return
+}
+
+func VecSliceTo(in [][]byte, out [][]byte, s int, e int) (n int) {
+	if s < 0 {
+		s = 0
+	}
+
+	if e >= 0 && e < s {
+		panic("pio: VecSlice start > end")
+	}
+
+	i := 0
+	off := 0
+	for s > 0 && i < len(in) {
+		left := len(in[i])
+		read := s
+		if left < read {
+			read = left
+		}
+		left -= read
+		off += read
+		s -= read
+		e -= read
+		if left == 0 {
+			i++
+			off = 0
+		}
+	}
+	if s > 0 {
+		panic("pio: VecSlice start out of range")
+	}
+
+	for e != 0 && i < len(in) {
+		left := len(in[i]) - off
+		read := left
+		if e > 0 && e < read {
+			read = e
+		}
+		out[n] = in[i][off : off+read]
+		n++
+		left -= read
+		e -= read
+		off += read
+		if left == 0 {
+			i++
+			off = 0
+		}
+	}
+	if e > 0 {
+		panic("pio: VecSlice end out of range")
+	}
+
+	return
+}
+
+func VecSlice(in [][]byte, s int, e int) (out [][]byte) {
+	out = make([][]byte, len(in))
+	n := VecSliceTo(in, out, s, e)
+	out = out[:n]
+	return
+}

+ 22 - 0
m7s.live/engine/v4@v4.14.6/util/bits/pio/vec_test.go

@@ -0,0 +1,22 @@
+package pio
+
+import (
+	"fmt"
+	"testing"
+)
+
+func TestExampleVec(t *testing.T) {
+	vec := [][]byte{[]byte{1, 2, 3}, []byte{4, 5, 6, 7, 8, 9}, []byte{10, 11, 12, 13}}
+	println(VecLen(vec))
+
+	vec = VecSlice(vec, 1, -1)
+	fmt.Println(vec)
+
+	vec = VecSlice(vec, 2, -1)
+	fmt.Println(vec)
+
+	vec = VecSlice(vec, 8, 8)
+	fmt.Println(vec)
+
+	// Output:
+}

+ 87 - 0
m7s.live/engine/v4@v4.14.6/util/bits/pio/writer.go

@@ -0,0 +1,87 @@
+package pio
+
+func PutU8(b []byte, v uint8) {
+	b[0] = v
+}
+
+func PutI16BE(b []byte, v int16) {
+	b[0] = byte(v >> 8)
+	b[1] = byte(v)
+}
+
+func PutU16BE(b []byte, v uint16) {
+	b[0] = byte(v >> 8)
+	b[1] = byte(v)
+}
+
+func PutI24BE(b []byte, v int32) {
+	b[0] = byte(v >> 16)
+	b[1] = byte(v >> 8)
+	b[2] = byte(v)
+}
+
+func PutU24BE(b []byte, v uint32) {
+	b[0] = byte(v >> 16)
+	b[1] = byte(v >> 8)
+	b[2] = byte(v)
+}
+
+func PutI32BE(b []byte, v int32) {
+	b[0] = byte(v >> 24)
+	b[1] = byte(v >> 16)
+	b[2] = byte(v >> 8)
+	b[3] = byte(v)
+}
+
+func PutU32BE(b []byte, v uint32) {
+	b[0] = byte(v >> 24)
+	b[1] = byte(v >> 16)
+	b[2] = byte(v >> 8)
+	b[3] = byte(v)
+}
+
+func PutU32LE(b []byte, v uint32) {
+	b[3] = byte(v >> 24)
+	b[2] = byte(v >> 16)
+	b[1] = byte(v >> 8)
+	b[0] = byte(v)
+}
+
+func PutU40BE(b []byte, v uint64) {
+	b[0] = byte(v >> 32)
+	b[1] = byte(v >> 24)
+	b[2] = byte(v >> 16)
+	b[3] = byte(v >> 8)
+	b[4] = byte(v)
+}
+
+func PutU48BE(b []byte, v uint64) {
+	b[0] = byte(v >> 40)
+	b[1] = byte(v >> 32)
+	b[2] = byte(v >> 24)
+	b[3] = byte(v >> 16)
+	b[4] = byte(v >> 8)
+	b[5] = byte(v)
+}
+
+func PutU64BE(b []byte, v uint64) {
+	b[0] = byte(v >> 56)
+	b[1] = byte(v >> 48)
+	b[2] = byte(v >> 40)
+	b[3] = byte(v >> 32)
+	b[4] = byte(v >> 24)
+	b[5] = byte(v >> 16)
+	b[6] = byte(v >> 8)
+	b[7] = byte(v)
+}
+
+func PutI64BE(b []byte, v int64) {
+	b[0] = byte(v >> 56)
+	b[1] = byte(v >> 48)
+	b[2] = byte(v >> 40)
+	b[3] = byte(v >> 32)
+	b[4] = byte(v >> 24)
+	b[5] = byte(v >> 16)
+	b[6] = byte(v >> 8)
+	b[7] = byte(v)
+}

+ 253 - 0
m7s.live/engine/v4@v4.14.6/util/buffer.go

@@ -0,0 +1,253 @@
+package util
+
+import (
+	"encoding/binary"
+	"fmt"
+	"io"
+	"math"
+	"net"
+)
+
+// Buffer 用于方便自动扩容的内存写入,已经读取
+type Buffer []byte
+
+// ReuseBuffer 重用buffer,内容可能会被覆盖,要尽早复制
+type ReuseBuffer struct {
+	Buffer
+}
+
+func (ReuseBuffer) Reuse() bool {
+	return true
+}
+
+// LimitBuffer 限制buffer的长度,不会改变原来的buffer,防止内存泄漏
+type LimitBuffer struct {
+	Buffer
+}
+
+func (b *LimitBuffer) ReadN(n int) (result LimitBuffer) {
+	result.Buffer = b.Buffer.ReadN(n)
+	return
+}
+
+func (b LimitBuffer) Clone() (result LimitBuffer) {
+	result.Buffer = b.Buffer.Clone()
+	return
+}
+
+func (b LimitBuffer) SubBuf(start int, length int) (result LimitBuffer) {
+	result.Buffer = b.Buffer.SubBuf(start, length)
+	return
+}
+
+func (b *LimitBuffer) Malloc(count int) (result LimitBuffer) {
+	l := b.Len()
+	newL := l + count
+	if c := b.Cap(); newL > c {
+		panic(fmt.Sprintf("LimitBuffer Malloc %d > %d", newL, c))
+	} else {
+		*b = b.SubBuf(0, newL)
+	}
+	return b.SubBuf(l, count)
+}
+
+func (b *LimitBuffer) Write(a []byte) (n int, err error) {
+	l := b.Len()
+	newL := l + len(a)
+	if c := b.Cap(); newL > c {
+		return 0, fmt.Errorf("LimitBuffer Write %d > %d", newL, c)
+		// panic(fmt.Sprintf("LimitBuffer Write %d > %d", newL, c))
+	} else {
+		b.Buffer = b.Buffer.SubBuf(0, newL)
+		copy(b.Buffer[l:], a)
+	}
+	return len(a), nil
+}
+
+// IBytes 用于区分传入的内存是否是复用内存,例如从网络中读取的数据,如果是复用内存,需要尽早复制
+type IBytes interface {
+	Len() int
+	Bytes() []byte
+	Reuse() bool
+}
+
+func (Buffer) Reuse() bool {
+	return false
+}
+
+func (b *Buffer) Read(buf []byte) (n int, err error) {
+	if !b.CanReadN(len(buf)) {
+		copy(buf, *b)
+		return b.Len(), io.EOF
+	}
+	ret := b.ReadN(len(buf))
+	copy(buf, ret)
+	return len(ret), err
+}
+
+func (b *Buffer) ReadN(n int) Buffer {
+	l := b.Len()
+	r := (*b)[:n]
+	*b = (*b)[n:l]
+	return r
+}
+func (b *Buffer) ReadFloat64() float64 {
+	return math.Float64frombits(b.ReadUint64())
+}
+func (b *Buffer) ReadUint64() uint64 {
+	return binary.BigEndian.Uint64(b.ReadN(8))
+}
+func (b *Buffer) ReadUint32() uint32 {
+	return binary.BigEndian.Uint32(b.ReadN(4))
+}
+func (b *Buffer) ReadUint24() uint32 {
+	return ReadBE[uint32](b.ReadN(3))
+}
+func (b *Buffer) ReadUint16() uint16 {
+	return binary.BigEndian.Uint16(b.ReadN(2))
+}
+func (b *Buffer) ReadByte() byte {
+	return b.ReadN(1)[0]
+}
+func (b *Buffer) WriteFloat64(v float64) {
+	PutBE(b.Malloc(8), math.Float64bits(v))
+}
+func (b *Buffer) WriteUint32(v uint32) {
+	binary.BigEndian.PutUint32(b.Malloc(4), v)
+}
+func (b *Buffer) WriteUint24(v uint32) {
+	PutBE(b.Malloc(3), v)
+}
+func (b *Buffer) WriteUint16(v uint16) {
+	binary.BigEndian.PutUint16(b.Malloc(2), v)
+}
+func (b *Buffer) WriteByte(v byte) {
+	b.Malloc(1)[0] = v
+}
+func (b *Buffer) WriteString(a string) {
+	*b = append(*b, a...)
+}
+func (b *Buffer) Write(a []byte) (n int, err error) {
+	l := b.Len()
+	newL := l + len(a)
+	if newL > b.Cap() {
+		*b = append(*b, a...)
+	} else {
+		*b = b.SubBuf(0, newL)
+		copy((*b)[l:], a)
+	}
+	return len(a), nil
+}
+
+func (b Buffer) Clone() (result Buffer) {
+	return append(result, b...)
+}
+
+func (b Buffer) Bytes() []byte {
+	return b
+}
+
+func (b Buffer) Len() int {
+	return len(b)
+}
+
+func (b Buffer) CanRead() bool {
+	return b.CanReadN(1)
+}
+
+func (b Buffer) CanReadN(n int) bool {
+	return b.Len() >= n
+}
+func (b Buffer) Cap() int {
+	return cap(b)
+}
+func (b Buffer) SubBuf(start int, length int) Buffer {
+	return b[start : start+length]
+}
+
+// Malloc 扩大原来的buffer的长度,返回新增的buffer
+func (b *Buffer) Malloc(count int) Buffer {
+	l := b.Len()
+	newL := l + count
+	if newL > b.Cap() {
+		n := make(Buffer, newL)
+		copy(n, *b)
+		*b = n
+	} else {
+		*b = b.SubBuf(0, newL)
+	}
+	return b.SubBuf(l, count)
+}
+
+// Relloc 改变 buffer 到指定大小
+func (b *Buffer) Relloc(count int) {
+	b.Reset()
+	b.Malloc(count)
+}
+
+func (b *Buffer) Reset() {
+	*b = b.SubBuf(0, 0)
+}
+
+func (b *Buffer) Split(n int) (result net.Buffers) {
+	origin := *b
+	for {
+		if b.CanReadN(n) {
+			result = append(result, b.ReadN(n))
+		} else {
+			result = append(result, *b)
+			*b = origin
+			return
+		}
+	}
+}
+
+func (b *Buffer) MarshalAMFs(v ...any) {
+	amf := AMF{*b}
+	*b = amf.Marshals(v...)
+}
+
+// ConcatBuffers 合并碎片内存为一个完整内存
+func ConcatBuffers[T ~[]byte](input []T) (out []byte) {
+	for _, v := range input {
+		out = append(out, v...)
+	}
+	return
+}
+
+// SizeOfBuffers 计算Buffers的内容长度
+func SizeOfBuffers[T ~[]byte](buf []T) (size int) {
+	for _, b := range buf {
+		size += len(b)
+	}
+	return
+}
+
+// SplitBuffers 按照一定大小分割 Buffers
+func SplitBuffers[T ~[]byte](buf []T, size int) (result [][]T) {
+	buf = append([]T(nil), buf...)
+	for total := SizeOfBuffers(buf); total > 0; {
+		if total <= size {
+			return append(result, buf)
+		} else {
+			var before []T
+			sizeOfBefore := 0
+			for _, b := range buf {
+				need := size - sizeOfBefore
+				if lenOfB := len(b); lenOfB > need {
+					before = append(before, b[:need])
+					result = append(result, before)
+					total -= need
+					buf[0] = b[need:]
+					break
+				} else {
+					sizeOfBefore += lenOfB
+					before = append(before, b)
+					total -= lenOfB
+					buf = buf[1:]
+				}
+			}
+		}
+	}
+	return
+}

+ 18 - 0
m7s.live/engine/v4@v4.14.6/util/buffer_test.go

@@ -0,0 +1,18 @@
+package util
+
+import (
+	"testing"
+)
+
+func TestBuffer(t *testing.T) {
+	t.Run(t.Name(), func(t *testing.T) {
+		var b Buffer
+		t.Log(b == nil)
+		b.Write([]byte{1, 2, 3})
+		if b == nil {
+			t.Fail()
+		} else {
+			t.Logf("b:% x", b)
+		}
+	})
+}

+ 407 - 0
m7s.live/engine/v4@v4.14.6/util/convert.go

@@ -0,0 +1,407 @@
+package util
+
+import (
+	"errors"
+	"io"
+	"strconv"
+	"strings"
+)
+
+/*
+func ReadByteToUintX(r io.Reader, l int) (data uint64, err error) {
+	if l%8 != 0 || l > 64 {
+		return 0, errors.New("disable convert")
+	}
+
+	bb := make([]byte, l)
+	if _, err := io.ReadFull(r, bb); err != nil {
+		return 0, err
+	}
+
+	switch l / 8 {
+	case 1:
+		{
+			return uint8(bb[0]), nil
+		}
+	case 2:
+		{
+			return BigEndian.Uint16(bb), nil
+		}
+	case 3:
+		{
+			return BigEndian.Uint24(bb), nil
+		}
+	case 4:
+		{
+			return BigEndian.Uint32(bb), nil
+		}
+	case 5:
+		{
+			//return BigEndian.Uint40(bb), nil
+			return 0, errors.New("disable convert")
+		}
+	case 6:
+		{
+			return BigEndian.Uint48(bb), nil
+		}
+	case 7:
+		{
+			//return BigEndian.Uint56(bb), nil
+			return 0, errors.New("disable convert")
+		}
+	case 8:
+		{
+			return BigEndian.Uint64(bb), nil
+		}
+	}
+
+	return 0, errors.New("convert not exist")
+}
+*/
+
+// // 千万注意大小端,RTMP是大端
+func ByteToUint32N(data []byte) (ret uint32, err error) {
+	if len(data) > 4 {
+		return 0, errors.New("ByteToUint32N error!")
+	}
+
+	for i := 0; i < len(data); i++ {
+		ret <<= 8
+		ret |= uint32(data[i])
+	}
+
+	return
+}
+
+// // 千万注意大小端,RTMP是大端
+func ByteToUint64N(data []byte) (ret uint64, err error) {
+	if len(data) > 8 {
+		return 0, errors.New("ByteToUint64N error!")
+	}
+
+	for i := 0; i < len(data); i++ {
+		ret <<= 8
+		ret |= uint64(data[i])
+	}
+
+	return
+}
+
+// 千万注意大小端,RTMP是大端
+func ByteToUint32(data []byte, bigEndian bool) (ret uint32, err error) {
+	if bigEndian {
+		return BigEndian.Uint32(data), nil
+	} else {
+		return LittleEndian.Uint32(data), nil
+	}
+}
+
+func Uint32ToByte(data uint32, bigEndian bool) (ret []byte, err error) {
+	if bigEndian {
+		return BigEndian.ToUint32(data), nil
+	} else {
+		return LittleEndian.ToUint32(data), nil
+	}
+}
+
+func ReadByteToUint8(r io.Reader) (data uint8, err error) {
+	bb := make([]byte, 1)
+	if _, err := io.ReadFull(r, bb); err != nil {
+		return 0, err
+	}
+
+	return uint8(bb[0]), nil
+}
+
+func ReadByteToUint16(r io.Reader, bigEndian bool) (data uint16, err error) {
+	bb := make([]byte, 2)
+	if _, err := io.ReadFull(r, bb); err != nil {
+		return 0, err
+	}
+
+	if bigEndian {
+		return BigEndian.Uint16(bb), nil
+	} else {
+		return LittleEndian.Uint16(bb), nil
+	}
+}
+
+func ReadByteToUint24(r io.Reader, bigEndian bool) (data uint32, err error) {
+	bb := make([]byte, 3)
+	if _, err := io.ReadFull(r, bb); err != nil {
+		return 0, err
+	}
+
+	if bigEndian {
+		return BigEndian.Uint24(bb), nil
+	} else {
+		return LittleEndian.Uint24(bb), nil
+	}
+}
+
+func ReadByteToUint32(r io.Reader, bigEndian bool) (data uint32, err error) {
+	bb := make([]byte, 4)
+	if _, err := io.ReadFull(r, bb); err != nil {
+		return 0, err
+	}
+
+	if bigEndian {
+		return BigEndian.Uint32(bb), nil
+	} else {
+		return LittleEndian.Uint32(bb), nil
+	}
+}
+
+func ReadByteToUint40(r io.Reader, bigEndian bool) (data uint64, err error) {
+	bb := make([]byte, 5)
+	if _, err := io.ReadFull(r, bb); err != nil {
+		return 0, err
+	}
+
+	if bigEndian {
+		return BigEndian.Uint40(bb), nil
+	} else {
+		return LittleEndian.Uint40(bb), nil
+	}
+}
+
+func ReadByteToUint48(r io.Reader, bigEndian bool) (data uint64, err error) {
+	bb := make([]byte, 6)
+	if _, err := io.ReadFull(r, bb); err != nil {
+		return 0, err
+	}
+
+	if bigEndian {
+		return BigEndian.Uint48(bb), nil
+	} else {
+		return LittleEndian.Uint48(bb), nil
+	}
+}
+
+/*
+func ReadByteToUint56(r io.Reader) (data uint64, err error) {
+	bb := make([]byte, 7)
+	if _, err := io.ReadFull(r, bb); err != nil {
+		return 0, err
+	}
+
+	return uint8(bb[0]), nil
+}
+*/
+
+func ReadByteToUint64(r io.Reader, bigEndian bool) (data uint64, err error) {
+	bb := make([]byte, 8)
+	if _, err := io.ReadFull(r, bb); err != nil {
+		return 0, err
+	}
+
+	if bigEndian {
+		return BigEndian.Uint64(bb), nil
+	} else {
+		return LittleEndian.Uint64(bb), nil
+	}
+}
+
+func WriteUint8ToByte(w io.Writer, data uint8) error {
+	bb := make([]byte, 8)
+	bb[0] = byte(data)
+	_, err := w.Write(bb[:1])
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func WriteUint16ToByte(w io.Writer, data uint16, bigEndian bool) error {
+	var bb []byte
+	if bigEndian {
+		bb = BigEndian.ToUint16(data)
+	} else {
+		bb = LittleEndian.ToUint16(data)
+	}
+
+	_, err := w.Write(bb)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func WriteUint24ToByte(w io.Writer, data uint32, bigEndian bool) error {
+	var bb []byte
+	if bigEndian {
+		bb = BigEndian.ToUint24(data)
+	} else {
+		bb = LittleEndian.ToUint24(data)
+	}
+
+	_, err := w.Write(bb)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func WriteUint32ToByte(w io.Writer, data uint32, bigEndian bool) error {
+	var bb []byte
+	if bigEndian {
+		bb = BigEndian.ToUint32(data)
+	} else {
+		bb = LittleEndian.ToUint32(data)
+	}
+
+	_, err := w.Write(bb)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func WriteUint40ToByte(w io.Writer, data uint64, bigEndian bool) error {
+	var bb []byte
+	if bigEndian {
+		bb = BigEndian.ToUint40(data)
+	} else {
+		bb = LittleEndian.ToUint40(data)
+	}
+
+	_, err := w.Write(bb)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func WriteUint48ToByte(w io.Writer, data uint64, bigEndian bool) error {
+	var bb []byte
+	if bigEndian {
+		bb = BigEndian.ToUint48(data)
+	} else {
+		bb = LittleEndian.ToUint48(data)
+	}
+
+	_, err := w.Write(bb)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func WriteUint64ToByte(w io.Writer, data uint64, bigEndian bool) error {
+	var bb []byte
+	if bigEndian {
+		bb = BigEndian.ToUint64(data)
+	} else {
+		bb = LittleEndian.ToUint64(data)
+	}
+
+	_, err := w.Write(bb)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func GetPtsDts(v uint64) uint64 {
+	// 4 + 3 + 1 + 15 + 1 + 15 + 1
+	// 0011
+	// 0010 + PTS[30-32] + marker_bit + PTS[29-15] + marker_bit + PTS[14-0] + marker_bit
+	pts1 := ((v >> 33) & 0x7) << 30
+	pts2 := ((v >> 17) & 0x7fff) << 15
+	pts3 := ((v >> 1) & 0x7fff)
+
+	return pts1 | pts2 | pts3
+}
+
+func PutPtsDts(v uint64) uint64 {
+	// 4 + 3 + 1 + 15 + 1 + 15 + 1
+	// 0011
+	// 0010 + PTS[30-32] + marker_bit + PTS[29-15] + marker_bit + PTS[14-0] + marker_bit
+	// 0x100010001
+	// 0001 0000 0000 0000 0001 0000 0000 0000 0001
+	// 3个 market_it
+	pts1 := (v >> 30) & 0x7 << 33
+	pts2 := (v >> 15) & 0x7fff << 17
+	pts3 := (v & 0x7fff) << 1
+
+	return pts1 | pts2 | pts3 | 0x100010001
+}
+
+func GetPCR(v uint64) uint64 {
+	// program_clock_reference_base(33) + Reserved(6) + program_clock_reference_extension(9)
+	base := v >> 15
+	ext := v & 0x1ff
+	return base*300 + ext
+}
+
+func PutPCR(pcr uint64) uint64 {
+	base := pcr / 300
+	ext := pcr % 300
+	return base<<15 | 0x3f<<9 | ext
+}
+
+func GetFillBytes(data byte, n int) []byte {
+	b := make([]byte, n)
+	for i := range b {
+		b[i] = data
+	}
+
+	return b
+}
+func ToFloat64(num interface{}) float64 {
+	switch v := num.(type) {
+	case uint:
+		return float64(v)
+	case int:
+		return float64(v)
+	case uint8:
+		return float64(v)
+	case uint16:
+		return float64(v)
+	case uint32:
+		return float64(v)
+	case uint64:
+		return float64(v)
+	case int8:
+		return float64(v)
+	case int16:
+		return float64(v)
+	case int32:
+		return float64(v)
+	case int64:
+		return float64(v)
+	case float64:
+		return v
+	case float32:
+		return float64(v)
+	}
+	return 0
+}
+
+func Conf2Listener(conf string) (protocol string, ports []uint16) {
+	var port string
+	protocol, port, _ = strings.Cut(conf, ":")
+	if r := strings.Split(port, "-"); len(r) == 2 {
+		min, err := strconv.Atoi(r[0])
+		if err != nil {
+			return
+		}
+		max, err := strconv.Atoi(r[1])
+		if err != nil {
+			return
+		}
+		if min < max {
+			ports = append(ports, uint16(min), uint16(max))
+		}
+	} else if p, err := strconv.Atoi(port); err == nil {
+		ports = append(ports, uint16(p))
+	}
+	return
+}

+ 126 - 0
m7s.live/engine/v4@v4.14.6/util/crc32.go

@@ -0,0 +1,126 @@
+package util
+
+import (
+	"fmt"
+	"io"
+	"io/ioutil"
+)
+
+var Crc32_Table = []uint32{
+	0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA,
+	0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
+	0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
+	0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91,
+	0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE,
+	0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
+	0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC,
+	0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5,
+	0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
+	0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B,
+	0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940,
+	0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
+	0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116,
+	0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,
+	0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924,
+	0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D,
+	0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A,
+	0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
+	0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818,
+	0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01,
+	0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
+	0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457,
+	0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C,
+	0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
+	0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2,
+	0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB,
+	0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0,
+	0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9,
+	0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086,
+	0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
+	0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4,
+	0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD,
+	0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A,
+	0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683,
+	0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8,
+	0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
+	0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE,
+	0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7,
+	0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
+	0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5,
+	0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252,
+	0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
+	0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60,
+	0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79,
+	0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
+	0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F,
+	0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04,
+	0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
+	0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A,
+	0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713,
+	0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38,
+	0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21,
+	0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E,
+	0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
+	0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C,
+	0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45,
+	0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2,
+	0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB,
+	0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0,
+	0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
+	0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6,
+	0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,
+	0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
+	0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D,
+}
+
+type Crc32Reader struct {
+	R     io.Reader
+	Crc32 uint32
+}
+
+type Crc32Writer struct {
+	W     io.Writer
+	Crc32 uint32
+}
+
+func (cr *Crc32Reader) Read(b []byte) (n int, err error) {
+	if n, err = cr.R.Read(b); err != nil {
+		return
+	}
+
+	cr.Crc32 = getCrc32(cr.Crc32, b)
+
+	return
+}
+
+func (cr *Crc32Reader) ReadCrc32UIntAndCheck() (err error) {
+	_, err = io.CopyN(ioutil.Discard, cr, 4)
+	if err != nil {
+		return err
+	}
+
+	if cr.Crc32 != 0 {
+		err = fmt.Errorf("crc32(%x) != 0", cr.Crc32)
+		return err
+	}
+
+	return nil
+}
+
+func (wr *Crc32Writer) Write(b []byte) (n int, err error) {
+	if n, err = wr.W.Write(b); err != nil {
+		return
+	}
+
+	wr.Crc32 = getCrc32(wr.Crc32, b)
+
+	return
+}
+
+func getCrc32(crc uint32, data []byte) uint32 {
+	for _, v := range data {
+		crc = Crc32_Table[v^byte(crc)] ^ (crc >> 8)
+	}
+
+	return crc
+}

+ 86 - 0
m7s.live/engine/v4@v4.14.6/util/index.go

@@ -0,0 +1,86 @@
+package util
+
+import (
+	"context"
+	"log"
+	"os"
+	"os/signal"
+	"path/filepath"
+	"runtime"
+	"strings"
+	"syscall"
+)
+
+var Null = struct{}{}
+
+func Clone[T any](x T) *T {
+	return &x
+}
+
+func initFatalLog() *os.File {
+	fatal_log_dir := "./fatal"
+	if _fatal_log := os.Getenv("M7S_FATAL_LOG"); _fatal_log != "" {
+		fatal_log_dir = _fatal_log
+	}
+	os.MkdirAll(fatal_log_dir, 0766)
+	fatal_log := filepath.Join(fatal_log_dir, "latest.log")
+	info, err := os.Stat(fatal_log)
+	if err == nil && info.Size() != 0 {
+		os.Rename(fatal_log, filepath.Join(fatal_log_dir, info.ModTime().Format("2006-01-02 15:04:05")+".log"))
+	}
+	logFile, err := os.OpenFile(fatal_log, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666)
+	if err != nil {
+		log.Println("服务启动出错", "打开异常日志文件失败", err)
+		return nil
+	}
+	return logFile
+}
+
+func CurrentDir(path ...string) string {
+	if _, currentFilePath, _, _ := runtime.Caller(1); len(path) == 0 {
+		return filepath.Dir(currentFilePath)
+	} else {
+		return filepath.Join(filepath.Dir(currentFilePath), filepath.Join(path...))
+	}
+}
+
+// 检查文件或目录是否存在
+// 如果由 filename 指定的文件或目录存在则返回 true,否则返回 false
+func Exist(filename string) bool {
+	_, err := os.Stat(filename)
+	return err == nil || os.IsExist(err)
+}
+
+func ConvertNum[F Integer, T Integer](from F, to T) T {
+	return T(from)
+}
+
+// Bit1 检查字节中的某一位是否为1 |0|1|2|3|4|5|6|7|
+func Bit1(b byte, index int) bool {
+	return b&(1<<(7-index)) != 0
+}
+
+func WaitTerm(cancel context.CancelFunc) {
+	sigc := make(chan os.Signal, 1)
+	signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
+	defer signal.Stop(sigc)
+	<-sigc
+	cancel()
+}
+
+// 判断目录是否是基础目录的子目录
+func IsSubdir(baseDir, joinedDir string) bool {
+	rel, err := filepath.Rel(baseDir, joinedDir)
+	if err != nil {
+		return false
+	}
+	return !strings.HasPrefix(rel, "..") && !strings.HasPrefix(rel, "/")
+}
+
+func Conditoinal[T any](cond bool, t, f T) T {
+	if cond {
+		return t
+	} else {
+		return f
+	}
+}

+ 31 - 0
m7s.live/engine/v4@v4.14.6/util/ip.go

@@ -0,0 +1,31 @@
+package util
+
+import "net"
+
+// IsLANAddr 检测 IP 地址字符串是否是内网地址
+func IsLANAddr(ip string) (bool, string) {
+	return IsLANIp(net.ParseIP(ip))
+}
+
+// IsLANIp 检测 IP 地址是否是内网地址
+// 通过直接对比ip段范围效率更高
+func IsLANIp(ip net.IP) (bool, string) {
+	if ip.IsLoopback() {
+		return true, "127.0.0.0/24"
+	}
+
+	ip4 := ip.To4()
+	if ip4 == nil {
+		return false, ""
+	}
+	if ip4[0] == 10 { // 10.0.0.0/8
+		return true, "10.0.0.0/8"
+	} else if ip4[0] == 172 && ip4[1] >= 16 && ip4[1] <= 31 { // 172.16.0.0/12
+		return true, "172.16.0.0/12"
+	} else if ip4[0] == 169 && ip4[1] == 254 { // 169.254.0.0/16
+		return true, "169.254.0.0/16"
+	} else if ip4[0] == 192 && ip4[1] == 168 { //192.168.0.0/16
+		return true, "192.168.0.0/16"
+	}
+	return false, ""
+}

+ 13 - 0
m7s.live/engine/v4@v4.14.6/util/linux.go

@@ -0,0 +1,13 @@
+// +build !windows
+
+package util
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+)
+
+func CreateShutdownScript() error{
+	return ioutil.WriteFile("shutdown.sh", []byte(fmt.Sprintf("kill -9 %d", os.Getpid())), 0777)
+}

+ 179 - 0
m7s.live/engine/v4@v4.14.6/util/list.go

@@ -0,0 +1,179 @@
+package util
+
+// 带回收功能的泛型双向链表
+
+type IList[T any] interface {
+	Push(*ListItem[T])
+	Shift() *ListItem[T]
+	Clear()
+}
+
+type ListItem[T any] struct {
+	Value     T
+	Next, Pre *ListItem[T] `json:"-" yaml:"-"`
+	Pool      *List[T]     `json:"-" yaml:"-"` // 回收池
+	list      *List[T]
+	reset     bool // 是否需要重置
+}
+
+func NewListItem[T any](value T) *ListItem[T] {
+	return &ListItem[T]{Value: value, reset: true}
+}
+
+func (item *ListItem[T]) InsertBefore(insert *ListItem[T]) {
+	if insert.list != nil {
+		panic("item already in list")
+	}
+	item.Pre.InsertAfter(insert)
+}
+func (item *ListItem[T]) InsertBeforeValue(value T) (result *ListItem[T]) {
+	result = &ListItem[T]{Value: value}
+	item.InsertBefore(result)
+	return
+}
+func (item *ListItem[T]) InsertAfter(insert *ListItem[T]) {
+	if insert.list != nil {
+		panic("item already in list")
+	}
+	insert.list = item.list
+	insert.Next = item.Next
+	insert.Pre = item
+	item.Next.Pre = insert
+	item.Next = insert
+	item.list.Length++
+}
+
+func (item *ListItem[T]) InsertAfterValue(value T) (result *ListItem[T]) {
+	result = &ListItem[T]{Value: value}
+	item.InsertAfter(result)
+	return
+}
+
+func (item *ListItem[T]) IsRoot() bool {
+	return &item.list.ListItem == item
+}
+
+func (item *ListItem[T]) Recycle() {
+	hasPool := item.list != item.Pool && item.Pool != nil && item.Pool.Length < PoolSize
+	if item.reset || !hasPool {
+		var null T
+		item.Value = null
+	}
+	if hasPool {
+		item.Pool.Push(item)
+	} else {
+		item.Pool = nil
+		item.list = nil
+		item.Next = nil
+		item.Pre = nil
+	}
+}
+
+func (item *ListItem[T]) Range(do func(value T) bool) {
+	item.RangeItem(func(item *ListItem[T]) bool {
+		return do(item.Value)
+	})
+}
+
+func (item *ListItem[T]) RangeItem(do func(*ListItem[T]) bool) {
+	for ; item != nil && item.list != nil && !item.IsRoot() && do(item); item = item.Next {
+	}
+}
+
+type List[T any] struct {
+	ListItem[T]
+	Length int
+}
+
+func (p *List[T]) PushValue(value T) {
+	p.Push(&ListItem[T]{Value: value, reset: true})
+}
+
+func (p *List[T]) Push(item *ListItem[T]) {
+	if item.list != nil {
+		panic("item already in list")
+	}
+	if p.Length == 0 {
+		p.Next = &p.ListItem
+		p.Pre = &p.ListItem
+		p.ListItem.list = p
+	}
+	p.Pre.InsertAfter(item)
+}
+
+func (p *List[T]) UnshiftValue(value T) {
+	p.Unshift(&ListItem[T]{Value: value})
+}
+
+func (p *List[T]) Unshift(item *ListItem[T]) {
+	if item.list != nil {
+		panic("item already in list")
+	}
+	if p.Length == 0 {
+		p.Next = &p.ListItem
+		p.Pre = &p.ListItem
+		p.ListItem.list = p
+	}
+	p.Next.InsertBefore(item)
+}
+
+func (p *List[T]) ShiftValue() T {
+	return p.Shift().Value
+}
+
+func (p *List[T]) PoolShift() (head *ListItem[T]) {
+	if head = p.Shift(); head == nil {
+		head = &ListItem[T]{Pool: p}
+	}
+	return
+}
+
+func (p *List[T]) Shift() (head *ListItem[T]) {
+	if p.Length == 0 {
+		return
+	}
+	head = p.Next
+	p.Next = head.Next
+	head.Pre = nil
+	head.Next = nil
+	head.list = nil
+	p.Length--
+	return
+}
+
+func (p *List[T]) Clear() {
+	p.Next = &p.ListItem
+	p.Pre = &p.ListItem
+	p.Length = 0
+}
+
+func (p *List[T]) Range(do func(value T) bool) {
+	if p.Length > 0 {
+		p.Next.Range(do)
+	}
+}
+
+func (p *List[T]) RangeItem(do func(*ListItem[T]) bool) {
+	if p.Length > 0 {
+		p.Next.RangeItem(do)
+	}
+}
+
+func (p *List[T]) Recycle() {
+	for item := p.Shift(); item != nil; item = p.Shift() {
+		item.Recycle()
+	}
+	if p.Length != 0 {
+		panic("recycle list error")
+	}
+}
+
+// Transfer 把链表中的所有元素转移到另一个链表中
+func (p *List[T]) Transfer(target IList[T]) {
+	for item := p.Shift(); item != nil; item = p.Shift() {
+		target.Push(item)
+	}
+	if p.Length != 0 {
+		panic("transfer list error")
+	}
+}

+ 68 - 0
m7s.live/engine/v4@v4.14.6/util/map.go

@@ -0,0 +1,68 @@
+package util
+
+import "sync"
+
+type Map[K comparable, V any] struct {
+	sync.Map
+}
+
+func (m *Map[K, V]) Add(k K, v V) bool {
+	_, loaded := m.LoadOrStore(k, v)
+	return !loaded
+}
+
+func (m *Map[K, V]) Set(k K, v V) {
+	m.Store(k, v)
+}
+
+func (m *Map[K, V]) Has(k K) (ok bool) {
+	_, ok = m.Load(k)
+	return
+}
+
+func (m *Map[K, V]) Len() (l int) {
+	m.Map.Range(func(k, v interface{}) bool {
+		l++
+		return true
+	})
+	return
+}
+
+func (m *Map[K, V]) Get(k K) (result V) {
+	v, ok := m.Load(k)
+	if !ok {
+		return
+	}
+	return v.(V)
+}
+
+func (m *Map[K, V]) Delete(k K) (v V, ok bool) {
+	var r any
+	if r, ok = m.Map.LoadAndDelete(k); ok {
+		v = r.(V)
+	}
+	return
+}
+
+func (m *Map[K, V]) ToList() (r []V) {
+	m.Map.Range(func(k, v interface{}) bool {
+		r = append(r, v.(V))
+		return true
+	})
+	return
+}
+
+func MapList[K comparable, V any, R any](m *Map[K, V], f func(K, V) R) (r []R) {
+	m.Map.Range(func(k, v interface{}) bool {
+		r = append(r, f(k.(K), v.(V)))
+		return true
+	})
+	return
+}
+
+func (m *Map[K, V]) Range(f func(K, V)) {
+	m.Map.Range(func(k, v interface{}) bool {
+		f(k.(K), v.(V))
+		return true
+	})
+}

+ 342 - 0
m7s.live/engine/v4@v4.14.6/util/pool.go

@@ -0,0 +1,342 @@
+package util
+
+import (
+	"io"
+	"net"
+)
+
+var PoolSize = 16
+
+type Recyclable interface {
+	Recycle()
+}
+type BLLReader struct {
+	*ListItem[Buffer]
+	pos int
+}
+
+func (r *BLLReader) CanRead() bool {
+	return r.ListItem != nil && !r.IsRoot()
+}
+
+func (r *BLLReader) Skip(n int) (err error) {
+	for r.CanRead() {
+		l := r.Value.Len() - r.pos
+		if l > n {
+			r.pos += n
+			return
+		}
+		n -= l
+		r.ListItem = r.Next
+		r.pos = 0
+	}
+	return io.EOF
+}
+
+func (r *BLLReader) ReadByte() (b byte, err error) {
+	for r.CanRead() {
+		l := r.Value.Len() - r.pos
+		if l > 0 {
+			b = r.Value[r.pos]
+			r.pos++
+			return
+		}
+		r.ListItem = r.Next
+		r.pos = 0
+	}
+	return 0, io.EOF
+}
+
+func (r *BLLReader) LEB128Unmarshal() (uint, int, error) {
+	v := uint(0)
+	n := 0
+
+	for i := 0; i < 8; i++ {
+		b, err := r.ReadByte()
+		if err != nil {
+			return 0, 0, err
+		}
+		v |= (uint(b&0b01111111) << (i * 7))
+		n++
+
+		if (b & 0b10000000) == 0 {
+			break
+		}
+	}
+
+	return v, n, nil
+}
+
+func (r *BLLReader) ReadBE(n int) (be uint32, err error) {
+	for i := 0; i < n; i++ {
+		b, err := r.ReadByte()
+		if err != nil {
+			return 0, err
+		}
+		be += uint32(b) << ((n - i - 1) << 3)
+	}
+	return
+}
+
+func (r *BLLReader) ReadN(n int) (result net.Buffers) {
+	for r.CanRead() {
+		l := r.Value.Len() - r.pos
+		if l > n {
+			result = append(result, r.Value[r.pos:r.pos+n])
+			r.pos += n
+			return
+		}
+		result = append(result, r.Value[r.pos:])
+		n -= l
+		r.ListItem = r.Next
+		r.pos = 0
+	}
+	return
+}
+
+func (r *BLLReader) WriteNTo(n int, result *net.Buffers) (actual int) {
+	actual = n
+	for r.CanRead() {
+		l := r.Value.Len() - r.pos
+		if l > n {
+			*result = append(*result, r.Value[r.pos:r.pos+n])
+			r.pos += n
+			return
+		}
+		*result = append(*result, r.Value[r.pos:])
+		n -= l
+		r.ListItem = r.Next
+		r.pos = 0
+	}
+	return actual - n
+}
+
+func (r *BLLReader) GetOffset() int {
+	return r.pos
+}
+
+type BLLsReader struct {
+	*ListItem[*BLL]
+	BLLReader
+}
+
+func (r *BLLsReader) CanRead() bool {
+	return r.ListItem != nil && !r.IsRoot()
+}
+
+func (r *BLLsReader) ReadByte() (b byte, err error) {
+	if r.BLLReader.CanRead() {
+		b, err = r.BLLReader.ReadByte()
+		if err == nil {
+			return
+		}
+	}
+	r.ListItem = r.Next
+	if !r.CanRead() {
+		return 0, io.EOF
+	}
+	r.BLLReader = *r.Value.NewReader()
+	return r.BLLReader.ReadByte()
+}
+
+type BLLs struct {
+	List[*BLL]
+	ByteLength int
+}
+
+func (list *BLLs) PushValue(item *BLL) {
+	if list == nil {
+		return
+	}
+	list.List.PushValue(item)
+	list.ByteLength += item.ByteLength
+}
+
+func (list *BLLs) Push(item *ListItem[Buffer]) {
+	if list == nil {
+		return
+	}
+	if list.List.Length == 0 {
+		var bll BLL
+		bll.Push(item)
+		list.PushValue(&bll)
+	} else {
+		list.Pre.Value.Push(item)
+		list.ByteLength += item.Value.Len()
+	}
+}
+
+func (list *BLLs) ToList() (result [][][]byte) {
+	list.Range(func(bll *BLL) bool {
+		result = append(result, bll.ToBuffers())
+		return true
+	})
+	return
+}
+
+func (list *BLLs) ToBuffers() (result net.Buffers) {
+	list.Range(func(bll *BLL) bool {
+		result = append(result, bll.ToBuffers()...)
+		return true
+	})
+	return
+}
+
+func (list *BLLs) ToBytes() (result []byte) {
+	list.Range(func(bll *BLL) bool {
+		result = append(result, bll.ToBytes()...)
+		return true
+	})
+	return
+}
+
+func (list *BLLs) Recycle() {
+	list.Range(func(bll *BLL) bool {
+		bll.Recycle()
+		return true
+	})
+	list.Clear()
+	list.ByteLength = 0
+}
+
+func (list *BLLs) NewReader() *BLLsReader {
+	return &BLLsReader{list.Next, *list.Next.Value.NewReader()}
+}
+
+// ByteLinkList
+type BLL struct {
+	List[Buffer]
+	ByteLength int
+}
+
+func (list *BLL) NewReader() *BLLReader {
+	return &BLLReader{list.Next, 0}
+}
+
+// func (list *BLL) Concat(list2 BLL) {
+// 	list.Tail.Next = list2.Head
+// 	list.Tail = list2.Tail
+// 	list.Length += list2.Length
+// 	list.ByteLength += list2.ByteLength
+// }
+
+func (list *BLL) Push(item *ListItem[Buffer]) {
+	if list == nil {
+		return
+	}
+	list.List.Push(item)
+	list.ByteLength += item.Value.Len()
+}
+
+func (list *BLL) Shift() (item *ListItem[Buffer]) {
+	if list == nil || list.Length == 0 {
+		return
+	}
+	item = list.List.Shift()
+	list.ByteLength -= item.Value.Len()
+	return
+}
+
+func (list *BLL) Clear() {
+	list.List.Clear()
+	list.ByteLength = 0
+}
+
+func (list *BLL) ToBuffers() (result net.Buffers) {
+	list.Range(func(item Buffer) bool {
+		result = append(result, item)
+		return true
+	})
+	return
+}
+
+func (list *BLL) WriteTo(w io.Writer) (int64, error) {
+	t := list.ToBuffers()
+	return t.WriteTo(w)
+}
+
+func (list *BLL) ToBytes() (b []byte) {
+	b = make([]byte, 0, list.ByteLength)
+	list.Range(func(item Buffer) bool {
+		b = append(b, item...)
+		return true
+	})
+	return
+}
+
+// 全部回收掉
+func (list *BLL) Recycle() {
+	list.List.Recycle()
+	list.ByteLength = 0
+}
+
+func (list *BLL) GetByte(index int) (b byte) {
+	list.Range(func(item Buffer) bool {
+		l := item.Len()
+		if index < l {
+			b = item[index]
+			return false
+		}
+		index -= l
+		return true
+	})
+	return
+}
+
+func (list *BLL) GetUint24(index int) uint32 {
+	return list.GetUintN(index, 3)
+}
+
+func (list *BLL) GetUintN(index int, n int) (result uint32) {
+	for i := 0; i < n; i++ {
+		result += uint32(list.GetByte(index+i)) << ((n - i - 1) << 3)
+	}
+	return
+}
+
+// func (b *Buffer) ToBuffers() (result net.Buffers) {
+// 	for p := b.Next; p != &b.ListItem; p = p.Next {
+// 		result = append(result, p.Value)
+// 	}
+// 	return
+// }
+
+type BytesPool []List[Buffer]
+
+// 获取来自真实内存的切片的——假内存块,即只回收外壳
+func (p BytesPool) GetShell(b []byte) (item *ListItem[Buffer]) {
+	if len(p) == 0 {
+		return &ListItem[Buffer]{Value: b}
+	}
+	item = p[0].PoolShift()
+	item.Value = b
+	item.reset = true
+	return
+}
+
+func (p BytesPool) Get(size int) (item *ListItem[Buffer]) {
+	for i := 1; i < len(p); i++ {
+		if level := 1 << i; level >= size {
+			if item = p[i].PoolShift(); cap(item.Value) > 0 {
+				item.Value = item.Value.SubBuf(0, size)
+			} else {
+				item.Value = make(Buffer, size, level)
+			}
+			return
+		}
+	}
+	// Pool 中没有就无法回收
+	if item == nil {
+		item = &ListItem[Buffer]{
+			Value: make(Buffer, size),
+			reset: true,
+		}
+	}
+	return
+}
+
+type Pool[T any] List[T]
+
+func (p *Pool[T]) Get() (item *ListItem[T]) {
+	return (*List[T])(p).PoolShift()
+}

+ 77 - 0
m7s.live/engine/v4@v4.14.6/util/reorder.go

@@ -0,0 +1,77 @@
+package util
+
+var RTPReorderBufferLen uint16 = 50
+
+// RTPReorder RTP包乱序重排
+type RTPReorder[T comparable] struct {
+	lastSeq uint16 //最新收到的rtp包序号
+	queue   []T    // 缓存队列,0号元素位置代表lastReq+1,永远保持为空
+	Total   uint32 // 总共收到的包数量
+	Drop    uint32 // 丢弃的包数量
+}
+
+func (p *RTPReorder[T]) Push(seq uint16, v T) (result T) {
+	p.Total++
+	// 初始化
+	if len(p.queue) == 0 {
+		p.lastSeq = seq
+		p.queue = make([]T, RTPReorderBufferLen)
+		return v
+	}
+	if seq < p.lastSeq && p.lastSeq-seq < 0x8000 {
+		// 旧的包直接丢弃
+		p.Drop++
+		return
+	}
+	delta := seq - p.lastSeq
+	if delta == 0 {
+		// 重复包
+		p.Drop++
+		return
+	}
+	if delta == 1 {
+		// 正常顺序,无需缓存
+		p.lastSeq = seq
+		p.pop()
+		return v
+	}
+	if RTPReorderBufferLen < delta {
+		//超过缓存最大范围,无法挽回,只能造成丢包(序号断裂)
+		for {
+			p.lastSeq++
+			delta--
+			head := p.pop()
+			// 可以放得进去了
+			if delta == RTPReorderBufferLen {
+				p.queue[RTPReorderBufferLen-1] = v
+				p.queue[0] = result
+				return head
+			} else if head != result {
+				p.Drop++
+			}
+		}
+	} else {
+		// 出现后面的包先到达,缓存起来
+		p.queue[delta-1] = v
+		return
+	}
+}
+
+func (p *RTPReorder[T]) pop() (result T) {
+	copy(p.queue, p.queue[1:]) //整体数据向前移动一位,保持0号元素代表lastSeq+1
+	p.queue[RTPReorderBufferLen-1] = result
+	return p.queue[0]
+}
+
+// Pop 从缓存中取出一个包,需要连续调用直到返回nil
+func (p *RTPReorder[T]) Pop() (result T) {
+	if len(p.queue) == 0 {
+		return
+	}
+	if next := p.queue[0]; next != result {
+		result = next
+		p.lastSeq++
+		p.pop()
+	}
+	return
+}

+ 38 - 0
m7s.live/engine/v4@v4.14.6/util/reorder_test.go

@@ -0,0 +1,38 @@
+package util
+
+import (
+	"testing"
+)
+
+type stuff struct {
+	seq uint16
+}
+
+func (s *stuff) Clone() *stuff {
+	return s
+}
+
+func TestReorder(t *testing.T) {
+	t.Run(t.Name(), func(t *testing.T) {
+		var recoder RTPReorder[*stuff]
+		for i := (uint16)(0); i < 25; i++ {
+			recoder.Push(i*2, &stuff{seq: i * 2})
+		}
+		if recoder.Pop() != nil {
+			t.Error("pop nil")
+		}
+		for i := (uint16)(0); i < 25; i++ {
+			x := recoder.Push(i*2+1, &stuff{seq: i*2 + 1})
+			if x != nil {
+				t.Logf("%d", x.seq)
+			}
+			for {
+				if x = recoder.Pop(); x == nil {
+					break
+				} else {
+					t.Logf("%d", x.seq)
+				}
+			}
+		}
+	})
+}

+ 39 - 0
m7s.live/engine/v4@v4.14.6/util/retry.go

@@ -0,0 +1,39 @@
+package util
+
+import (
+	"math/rand"
+	"time"
+)
+
+func init() {
+	rand.Seed(time.Now().UnixNano())
+}
+
+func Retry(attempts int, sleep time.Duration, f func() error) error {
+	if err := f(); err != nil {
+		if s, ok := err.(retryStop); ok {
+			// Return the original error for later checking
+			return s.error
+		}
+
+		if attempts--; attempts > 0 {
+			// Add some randomness to prevent creating a Thundering Herd
+			jitter := time.Duration(rand.Int63n(int64(sleep)))
+			sleep = sleep + jitter/2
+
+			time.Sleep(sleep)
+			return Retry(attempts, 2*sleep, f)
+		}
+		return err
+	}
+
+	return nil
+}
+
+type retryStop struct {
+	error
+}
+
+func RetryStopErr(err error) retryStop {
+	return retryStop{err}
+}

+ 136 - 0
m7s.live/engine/v4@v4.14.6/util/ring.go

@@ -0,0 +1,136 @@
+package util
+
+// A Ring is an element of a circular list, or ring.
+// Rings do not have a beginning or end; a pointer to any ring element
+// serves as reference to the entire ring. Empty rings are represented
+// as nil Ring pointers. The zero value for a Ring is a one-element
+// ring with a nil Value.
+//
+type Ring[T any] struct {
+	next, prev *Ring[T]
+	Value      T // for use by client; untouched by this library
+}
+
+func (r *Ring[T]) init() *Ring[T] {
+	r.next = r
+	r.prev = r
+	return r
+}
+
+// Next returns the next ring element. r must not be empty.
+func (r *Ring[T]) Next() *Ring[T] {
+	if r.next == nil {
+		return r.init()
+	}
+	return r.next
+}
+
+// Prev returns the previous ring element. r must not be empty.
+func (r *Ring[T]) Prev() *Ring[T] {
+	if r.next == nil {
+		return r.init()
+	}
+	return r.prev
+}
+
+// Move moves n % r.Len() elements backward (n < 0) or forward (n >= 0)
+// in the ring and returns that ring element. r must not be empty.
+//
+func (r *Ring[T]) Move(n int) *Ring[T] {
+	if r.next == nil {
+		return r.init()
+	}
+	switch {
+	case n < 0:
+		for ; n < 0; n++ {
+			r = r.prev
+		}
+	case n > 0:
+		for ; n > 0; n-- {
+			r = r.next
+		}
+	}
+	return r
+}
+
+// New creates a ring of n elements.
+func NewRing[T any](n int) *Ring[T] {
+	if n <= 0 {
+		return nil
+	}
+	r := new(Ring[T])
+	p := r
+	for i := 1; i < n; i++ {
+		p.next = &Ring[T]{prev: p}
+		p = p.next
+	}
+	p.next = r
+	r.prev = p
+	return r
+}
+
+// Link connects ring r with ring s such that r.Next()
+// becomes s and returns the original value for r.Next().
+// r must not be empty.
+//
+// If r and s point to the same ring, linking
+// them removes the elements between r and s from the ring.
+// The removed elements form a subring and the result is a
+// reference to that subring (if no elements were removed,
+// the result is still the original value for r.Next(),
+// and not nil).
+//
+// If r and s point to different rings, linking
+// them creates a single ring with the elements of s inserted
+// after r. The result points to the element following the
+// last element of s after insertion.
+//
+func (r *Ring[T]) Link(s *Ring[T]) *Ring[T] {
+	n := r.Next()
+	if s != nil {
+		p := s.Prev()
+		// Note: Cannot use multiple assignment because
+		// evaluation order of LHS is not specified.
+		r.next = s
+		s.prev = r
+		n.prev = p
+		p.next = n
+	}
+	return n
+}
+
+// Unlink removes n % r.Len() elements from the ring r, starting
+// at r.Next(). If n % r.Len() == 0, r remains unchanged.
+// The result is the removed subring. r must not be empty.
+//
+func (r *Ring[T]) Unlink(n int) *Ring[T] {
+	if n <= 0 {
+		return nil
+	}
+	return r.Link(r.Move(n + 1))
+}
+
+// Len computes the number of elements in ring r.
+// It executes in time proportional to the number of elements.
+//
+func (r *Ring[T]) Len() int {
+	n := 0
+	if r != nil {
+		n = 1
+		for p := r.Next(); p != r; p = p.next {
+			n++
+		}
+	}
+	return n
+}
+
+// Do calls function f on each element of the ring, in forward order.
+// The behavior of Do is undefined if f changes *r.
+func (r *Ring[T]) Do(f func(T)) {
+	if r != nil {
+		f(r.Value)
+		for p := r.Next(); p != r; p = p.next {
+			f(p.Value)
+		}
+	}
+}

+ 88 - 0
m7s.live/engine/v4@v4.14.6/util/safe_chan.go

@@ -0,0 +1,88 @@
+package util
+
+import (
+	"context"
+	"errors"
+	"math"
+	"sync/atomic"
+	"time"
+)
+
+// SafeChan安全的channel,可以防止close后被写入的问题
+type SafeChan[T any] struct {
+	C       chan T
+	senders int32 //当前发送者数量
+}
+
+func (sc *SafeChan[T]) Init(n int) {
+	sc.C = make(chan T, n)
+}
+
+// Close senders为0的时候可以安全关闭,否则不能关闭
+func (sc *SafeChan[T]) Close() bool {
+	if atomic.CompareAndSwapInt32(&sc.senders, 0, math.MinInt32) {
+		close(sc.C)
+		return true
+	}
+	return false
+}
+
+func (sc *SafeChan[T]) Send(v T) bool {
+	// senders增加后为正数说明没有被channel没有被关闭,可以发送数据
+	if atomic.AddInt32(&sc.senders, 1) > 0 {
+		sc.C <- v
+		atomic.AddInt32(&sc.senders, -1)
+		return true
+	}
+	return false
+}
+
+func (sc *SafeChan[T]) IsClosed() bool {
+	return atomic.LoadInt32(&sc.senders) < 0
+}
+
+func (sc *SafeChan[T]) IsEmpty() bool {
+	return atomic.LoadInt32(&sc.senders) == 0
+}
+
+func (sc *SafeChan[T]) IsFull() bool {
+	return atomic.LoadInt32(&sc.senders) > 0
+}
+
+var errResolved = errors.New("resolved")
+
+type Promise[S any] struct {
+	context.Context
+	context.CancelCauseFunc
+	context.CancelFunc
+	Value S
+}
+
+func (r *Promise[S]) Resolve() {
+	r.CancelCauseFunc(errResolved)
+}
+
+func (r *Promise[S]) Reject(err error) {
+	r.CancelCauseFunc(err)
+}
+
+func (p *Promise[S]) Await() (err error) {
+	<-p.Done()
+	err = context.Cause(p.Context)
+	if err == errResolved {
+		err = nil
+	}
+	p.CancelFunc()
+	return
+}
+
+func NewPromise[S any](value S) *Promise[S] {
+	ctx0, cancel0 := context.WithTimeout(context.Background(), time.Second*10)
+	ctx, cancel := context.WithCancelCause(ctx0)
+	return &Promise[S]{
+		Value:           value,
+		Context:         ctx,
+		CancelCauseFunc: cancel,
+		CancelFunc:      cancel0,
+	}
+}

+ 36 - 0
m7s.live/engine/v4@v4.14.6/util/slice.go

@@ -0,0 +1,36 @@
+package util
+
+type Slice[T comparable] []T
+
+func (s Slice[T]) Len() int {
+	return len(s)
+}
+
+func (s *Slice[T]) Add(v T) {
+	*s = append(*s, v)
+}
+
+func (s *Slice[T]) Delete(v T) bool {
+	for i, val := range *s {
+		if val == v {
+			*s = append((*s)[:i], (*s)[i+1:]...)
+			return true
+		}
+	}
+	return false
+}
+
+func (s *Slice[T]) Reset() {
+	if len(*s) > 0 {
+		*s = (*s)[:0]
+	}
+}
+
+func (s *Slice[T]) ResetAppend(first T) {
+	s.Reset()
+	s.Add(first)
+}
+
+func LastElement[T any](s []T) T {
+	return s[len(s)-1]
+}

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels