8 changed files with 463 additions and 4 deletions
@ -0,0 +1,24 @@ |
|||||
|
# 支持零配置启动,即无需填写配置默认启用所有的插件。 |
||||
|
# 只需要填写需要修改的配置项即可。不需要将所有的配置都填写进来!!。 |
||||
|
# 全局配置参考 https://m7s.live/guide/config.html |
||||
|
# 插件配置参考各个插件的文档 |
||||
|
# 插件都有一个enable配置,如果为false则不启用该插件,默认为true即不需要配置。 |
||||
|
|
||||
|
# global: |
||||
|
# console: |
||||
|
# secret: "ab0f6913670062af4d2f15c621205178" |
||||
|
# http: |
||||
|
# listenaddrtls: :8081 |
||||
|
# certfile: monibuca.com.pem |
||||
|
# keyfile: monibuca.com.key |
||||
|
webrtc: |
||||
|
iceservers: [ ] |
||||
|
publicip: [ '192.168.1.119' ] # 可以是数组也可以是字符串(内部自动转成数组) |
||||
|
portmin: 10000 |
||||
|
portmax: 20000 |
||||
|
inviteportfixed: false # 设备将流发送的端口,是否固定 on 发送流到多路复用端口 如9000 off 自动从 mix_port - max_port 之间的值中 选一个可以用的端口 |
||||
|
iceudpmux: 9000 # 接收设备端rtp流的多路复用端口 |
||||
|
pli: 2000000000 # 2s |
||||
|
hls: |
||||
|
fragment: 3s # TS分片长度 |
||||
|
window: 3 # 实时流m3u8文件包含的TS文件数 |
@ -0,0 +1,150 @@ |
|||||
|
package webrtc |
||||
|
|
||||
|
import ( |
||||
|
"bytes" |
||||
|
"encoding/binary" |
||||
|
"errors" |
||||
|
"strconv" |
||||
|
// "fmt"
|
||||
|
. "github.com/pion/webrtc/v3" |
||||
|
) |
||||
|
|
||||
|
// H265
|
||||
|
// https://zhuanlan.zhihu.com/p/458497037
|
||||
|
const ( |
||||
|
NALU_H265_VPS = 0x4001 |
||||
|
NALU_H265_SPS = 0x4201 |
||||
|
NALU_H265_PPS = 0x4401 |
||||
|
NALU_H265_SEI = 0x4e01 |
||||
|
NALU_H265_IFRAME = 0x2601 |
||||
|
NALU_H265_PFRAME = 0x0201 |
||||
|
HEVC_NAL_TRAIL_N = 0 |
||||
|
HEVC_NAL_TRAIL_R = 1 |
||||
|
HEVC_NAL_TSA_N = 2 |
||||
|
HEVC_NAL_TSA_R = 3 |
||||
|
HEVC_NAL_STSA_N = 4 |
||||
|
HEVC_NAL_STSA_R = 5 |
||||
|
HEVC_NAL_BLA_W_LP = 16 |
||||
|
HEVC_NAL_BLA_W_RADL = 17 |
||||
|
HEVC_NAL_BLA_N_LP = 18 |
||||
|
HEVC_NAL_IDR_W_RADL = 19 |
||||
|
HEVC_NAL_IDR_N_LP = 20 |
||||
|
HEVC_NAL_CRA_NUT = 21 |
||||
|
HEVC_NAL_RADL_N = 6 |
||||
|
HEVC_NAL_RADL_R = 7 |
||||
|
HEVC_NAL_RASL_N = 8 |
||||
|
HEVC_NAL_RASL_R = 9 |
||||
|
MAXPACKETSIZE = 65536 |
||||
|
) |
||||
|
|
||||
|
func SendH265FrameData(dc *DataChannel, data []byte, timestamp int64) { |
||||
|
if len(data) > 4 && dc != nil && dc.ReadyState() == DataChannelStateOpen { |
||||
|
var frametypestr string |
||||
|
glength := len(data) |
||||
|
count := glength / MAXPACKETSIZE |
||||
|
rem := glength % MAXPACKETSIZE |
||||
|
packets := count |
||||
|
if rem != 0 { |
||||
|
packets++ |
||||
|
} |
||||
|
temptype, frametype, err := GetFrameType(data) |
||||
|
if err != nil { |
||||
|
|
||||
|
} else { |
||||
|
frametypestr, err = GetFrameTypeName(frametype) |
||||
|
} |
||||
|
|
||||
|
startstr := "h265 start ,FrameType:" + frametypestr + ",nalutype:" + strconv.Itoa(int(temptype)) + ",pts:" + strconv.FormatInt(timestamp, 10) + ",Packetslen:" + strconv.Itoa(glength) + ",packets:" + strconv.Itoa(packets) + ",rem:" + strconv.Itoa(rem) |
||||
|
|
||||
|
_ = dc.SendText(startstr) |
||||
|
i := 0 |
||||
|
for i = 0; i < count; i++ { |
||||
|
length := i * MAXPACKETSIZE |
||||
|
_ = dc.Send(data[length : length+MAXPACKETSIZE]) |
||||
|
} |
||||
|
if rem != 0 { |
||||
|
_ = dc.Send(data[glength-rem : glength]) |
||||
|
} |
||||
|
_ = dc.SendText("h265 end") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func GetFrameType(pdata []byte) (uint8, uint16, error) { |
||||
|
var frametype uint16 |
||||
|
|
||||
|
destcount := 0 |
||||
|
if FindStartCode2(pdata) { |
||||
|
destcount = 3 |
||||
|
} else if FindStartCode3(pdata) { |
||||
|
destcount = 4 |
||||
|
} else { |
||||
|
return 0, 0, errors.New("not find") |
||||
|
} |
||||
|
temptype := (pdata[destcount] & 0x7E) >> 1 |
||||
|
bytesBuffer := bytes.NewBuffer(pdata[destcount : destcount+2]) |
||||
|
binary.Read(bytesBuffer, binary.BigEndian, &frametype) |
||||
|
return temptype, frametype, nil |
||||
|
} |
||||
|
|
||||
|
func GetFrameTypeName(frametype uint16) (string, error) { |
||||
|
switch frametype { |
||||
|
case NALU_H265_VPS: |
||||
|
return "H265_FRAME_VPS", nil |
||||
|
case NALU_H265_SPS: |
||||
|
return "H265_FRAME_SPS", nil |
||||
|
case NALU_H265_PPS: |
||||
|
return "H265_FRAME_PPS", nil |
||||
|
case NALU_H265_SEI: |
||||
|
return "H265_FRAME_SEI", nil |
||||
|
case NALU_H265_IFRAME: |
||||
|
return "H265_FRAME_I", nil |
||||
|
case NALU_H265_PFRAME: |
||||
|
return "H265_FRAME_P", nil |
||||
|
default: |
||||
|
return "", errors.New("frametype unsupport") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func FindStartCode2(Buf []byte) bool { |
||||
|
if Buf[0] != 0 || Buf[1] != 0 || Buf[2] != 1 { |
||||
|
return false //判断是否为0x000001,如果是返回1
|
||||
|
} else { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func FindStartCode3(Buf []byte) bool { |
||||
|
if Buf[0] != 0 || Buf[1] != 0 || Buf[2] != 0 || Buf[3] != 1 { |
||||
|
return false //判断是否为0x00000001,如果是返回1
|
||||
|
} else { |
||||
|
return true |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func Add3ZoneOne(h265frame []byte) []byte { |
||||
|
var hBuf = [4]byte{0, 0, 0, 1} |
||||
|
var data []byte |
||||
|
for i := range hBuf { |
||||
|
data = append(data, hBuf[i]) |
||||
|
} |
||||
|
for i := range h265frame { |
||||
|
data = append(data, h265frame[i]) |
||||
|
} |
||||
|
return data |
||||
|
} |
||||
|
|
||||
|
func AddBufs(A []byte, B []byte) []byte { |
||||
|
var data []byte |
||||
|
for i := range A { |
||||
|
data = append(data, A[i]) |
||||
|
} |
||||
|
for i := range B { |
||||
|
data = append(data, B[i]) |
||||
|
} |
||||
|
return data |
||||
|
} |
||||
|
|
||||
|
type WebRtcReturn struct { |
||||
|
SessionDescription |
||||
|
IsH265 bool `json:"isH265"` |
||||
|
} |
@ -0,0 +1,50 @@ |
|||||
|
package extend |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"m7s.live/engine/v4/codec" |
||||
|
"m7s.live/engine/v4/common" |
||||
|
"m7s.live/engine/v4/track" |
||||
|
"m7s.live/engine/v4/util" |
||||
|
"net" |
||||
|
) |
||||
|
|
||||
|
func ReadRing(vt *track.Video) *common.RingBuffer[common.AVFrame] { |
||||
|
return util.Clone(vt.Media.RingBuffer) |
||||
|
} |
||||
|
|
||||
|
func Read(r *common.RingBuffer[common.AVFrame], ctx context.Context) (item *common.AVFrame) { |
||||
|
for item = &r.Value; ctx.Err() == nil && !item.CanRead; { |
||||
|
} |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
// PlayFullAnnexB 订阅annex-b格式的流数据,每一个I帧增加sps、pps头
|
||||
|
func PlayFullAnnexB(vt *track.Video, ctx context.Context, onMedia func(net.Buffers) error) error { |
||||
|
for vr := ReadRing(vt); ctx.Err() == nil; vr.MoveNext() { |
||||
|
vp := Read(vr, ctx) |
||||
|
var data net.Buffers |
||||
|
if vp.IFrame { |
||||
|
for _, nalu := range vt.ParamaterSets { |
||||
|
data = append(data, codec.NALU_Delimiter2, nalu) |
||||
|
} |
||||
|
} |
||||
|
data = append(data, codec.NALU_Delimiter2) |
||||
|
|
||||
|
i := 0 |
||||
|
vp.AUList.Range(func(au *util.BLL) bool { |
||||
|
if i > 0 { |
||||
|
data = append(data, codec.NALU_Delimiter1, au.ToBytes()) |
||||
|
} else { |
||||
|
data = append(data, au.ToBuffers()...) |
||||
|
} |
||||
|
i++ |
||||
|
return true |
||||
|
}) |
||||
|
if err := onMedia(data); err != nil { |
||||
|
// TODO: log err
|
||||
|
return err |
||||
|
} |
||||
|
} |
||||
|
return ctx.Err() |
||||
|
} |
@ -0,0 +1,130 @@ |
|||||
|
package webrtc |
||||
|
|
||||
|
import ( |
||||
|
"fmt" |
||||
|
"github.com/pion/rtcp" |
||||
|
. "github.com/pion/webrtc/v3" |
||||
|
. "m7s.live/engine/v4" |
||||
|
"m7s.live/engine/v4/codec" |
||||
|
"m7s.live/engine/v4/track" |
||||
|
"m7s.live/engine/v4/util" |
||||
|
"m7s.live/plugin/webrtc/v4/extend" |
||||
|
"net" |
||||
|
"strings" |
||||
|
"time" |
||||
|
) |
||||
|
|
||||
|
type WebRTCSubscriberPro struct { |
||||
|
Subscriber |
||||
|
WebRTCIO |
||||
|
videoTrack *TrackLocalStaticRTP |
||||
|
audioTrack *TrackLocalStaticRTP |
||||
|
isH265 bool |
||||
|
} |
||||
|
|
||||
|
func (suber *WebRTCSubscriberPro) OnEvent(event any) { |
||||
|
switch v := event.(type) { |
||||
|
case *track.Video: |
||||
|
if v.CodecID == codec.CodecID_H264 { |
||||
|
suber.isH265 = false |
||||
|
pli := "42001f" |
||||
|
//pli = fmt.Sprintf("%x", v.GetDecoderConfiguration().Raw[0][1:4])
|
||||
|
if !strings.Contains(suber.SDP, pli) { |
||||
|
pli = reg_level.FindAllStringSubmatch(suber.SDP, -1)[0][1] |
||||
|
} |
||||
|
suber.videoTrack, _ = NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: MimeTypeH264, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=" + pli}, "video", "m7s") |
||||
|
rtpSender, _ := suber.PeerConnection.AddTrack(suber.videoTrack) |
||||
|
go func() { |
||||
|
rtcpBuf := make([]byte, 1500) |
||||
|
for { |
||||
|
if n, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { |
||||
|
return |
||||
|
} else { |
||||
|
if p, err := rtcp.Unmarshal(rtcpBuf[:n]); err == nil { |
||||
|
for _, pp := range p { |
||||
|
switch pp.(type) { |
||||
|
case *rtcp.PictureLossIndication: |
||||
|
// fmt.Println("PictureLossIndication")
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
}() |
||||
|
suber.Subscriber.AddTrack(v) //接受这个track
|
||||
|
} |
||||
|
start := time.Now().UnixMilli() |
||||
|
var rtcDc *DataChannel |
||||
|
if v.CodecID == codec.CodecID_H265 { |
||||
|
suber.isH265 = true |
||||
|
nInSendH265Track := 0 |
||||
|
suber.PeerConnection.OnDataChannel(func(dc *DataChannel) { |
||||
|
rtcDc = dc |
||||
|
rtcDc.OnOpen(func() { |
||||
|
annexB := v.GetAnnexB() |
||||
|
var h265frame []byte |
||||
|
for _, p := range annexB { //拼接消息头
|
||||
|
h265frame = AddBufs(h265frame, p) |
||||
|
} |
||||
|
va := v.IDRing.Value |
||||
|
va.AUList.Range(func(au *util.BLL) bool { |
||||
|
packets := au.ToBuffers() |
||||
|
for _, packet := range packets { |
||||
|
h265frame = AddBufs(h265frame, Add3ZoneOne(packet)) |
||||
|
} |
||||
|
return true |
||||
|
}) |
||||
|
SendH265FrameData(rtcDc, h265frame, va.Timestamp.UnixMilli()-start) |
||||
|
}) |
||||
|
rtcDc.OnMessage(func(msg DataChannelMessage) { |
||||
|
msg_ := string(msg.Data) |
||||
|
fmt.Println(msg_) |
||||
|
}) |
||||
|
rtcDc.OnClose(func() { |
||||
|
nInSendH265Track-- |
||||
|
}) |
||||
|
}) |
||||
|
go extend.PlayFullAnnexB(v, suber.IO, func(frame net.Buffers) error { |
||||
|
var h265frame []byte |
||||
|
for _, packet := range frame { |
||||
|
if len(h265frame) == 0 { |
||||
|
h265frame = packet |
||||
|
} else { |
||||
|
h265frame = AddBufs(h265frame, packet) |
||||
|
} |
||||
|
} |
||||
|
timestamp := time.Now().UnixMilli() |
||||
|
SendH265FrameData(rtcDc, h265frame, timestamp-start) |
||||
|
return nil |
||||
|
}) |
||||
|
} |
||||
|
case *track.Audio: |
||||
|
audioMimeType := MimeTypePCMA |
||||
|
if v.CodecID == codec.CodecID_PCMU { |
||||
|
audioMimeType = MimeTypePCMU |
||||
|
} |
||||
|
if v.CodecID == codec.CodecID_PCMA || v.CodecID == codec.CodecID_PCMU { |
||||
|
suber.audioTrack, _ = NewTrackLocalStaticRTP(RTPCodecCapability{MimeType: audioMimeType}, "audio", "m7s") |
||||
|
suber.PeerConnection.AddTrack(suber.audioTrack) |
||||
|
suber.Subscriber.AddTrack(v) //接受这个track
|
||||
|
} |
||||
|
case VideoRTP: |
||||
|
suber.videoTrack.WriteRTP(&v.Packet) |
||||
|
case AudioRTP: |
||||
|
suber.audioTrack.WriteRTP(&v.Packet) |
||||
|
case ISubscriber: |
||||
|
suber.OnConnectionStateChange(func(pcs PeerConnectionState) { |
||||
|
suber.Info("Connection State has changed:" + pcs.String()) |
||||
|
switch pcs { |
||||
|
case PeerConnectionStateConnected: |
||||
|
suber.Info("Connection State has changed:") |
||||
|
go suber.PlayRTP() |
||||
|
case PeerConnectionStateDisconnected, PeerConnectionStateFailed: |
||||
|
suber.Stop() |
||||
|
suber.PeerConnection.Close() |
||||
|
} |
||||
|
}) |
||||
|
default: |
||||
|
suber.Subscriber.OnEvent(event) |
||||
|
} |
||||
|
} |
@ -0,0 +1,30 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"context" |
||||
|
"m7s.live/engine/v4" |
||||
|
|
||||
|
//_ "m7s.live/plugin/hdl/v4"
|
||||
|
_ "m7s.live/plugin/hls/v4" |
||||
|
_ "m7s.live/plugin/hook/v4" |
||||
|
//_ "m7s.live/plugin/jessica/v4"
|
||||
|
//_ "m7s.live/plugin/logrotate/v4"
|
||||
|
//_ "m7s.live/plugin/preview/v4"
|
||||
|
//_ "m7s.live/plugin/record/v4"
|
||||
|
_ "m7s.live/plugin/rtmp/v4" |
||||
|
_ "m7s.live/plugin/rtsp/v4" |
||||
|
//_ "m7s.live/plugin/snap/v4"
|
||||
|
_ "m7s.live/plugin/webrtc/v4" |
||||
|
) |
||||
|
|
||||
|
// engine Jun 25, 2022
|
||||
|
// webrtc Sep 24, 2022
|
||||
|
// rtmp Sep 24, 2022
|
||||
|
// rtsp Sep 6, 2022
|
||||
|
// go build -ldflags "-w -s" -o build\ test/test.go
|
||||
|
func main() { |
||||
|
err := engine.Run(context.Background(), "config.yaml") |
||||
|
if err != nil { |
||||
|
return |
||||
|
} |
||||
|
} |
@ -0,0 +1,71 @@ |
|||||
|
package webrtc |
||||
|
|
||||
|
import ( |
||||
|
"encoding/json" |
||||
|
. "github.com/pion/webrtc/v3" |
||||
|
"io" |
||||
|
"net/http" |
||||
|
) |
||||
|
|
||||
|
func (conf *WebRTCConfig) PlayV3_(w http.ResponseWriter, r *http.Request) { |
||||
|
w.Header().Set("Access-Control-Max-Age", "86400") |
||||
|
w.Header().Set("Access-Control-Allow-Methods", "*") |
||||
|
w.Header().Set("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization") |
||||
|
w.Header().Set("Access-Control-Expose-Headers", "*") |
||||
|
w.Header().Set("Access-Control-Allow-Credentials", "true") |
||||
|
w.Header().Set("Content-Type", "application/json") |
||||
|
streamPath := r.URL.Path[len("/webrtc/playv3/"):] |
||||
|
bytes, err := io.ReadAll(r.Body) |
||||
|
var suber WebRTCSubscriberPro |
||||
|
var offer SessionDescription |
||||
|
if err = json.Unmarshal(bytes, &offer); err != nil { |
||||
|
return |
||||
|
} |
||||
|
suber.SDP = offer.SDP |
||||
|
if suber.PeerConnection, err = conf.api.NewPeerConnection(Configuration{}); err != nil { |
||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
suber.OnICECandidate(func(ice *ICECandidate) { |
||||
|
if ice != nil { |
||||
|
suber.Info(ice.ToJSON().Candidate) |
||||
|
} |
||||
|
}) |
||||
|
if err = suber.SetRemoteDescription(SessionDescription{Type: SDPTypeOffer, SDP: suber.SDP}); err != nil { |
||||
|
http.Error(w, err.Error(), http.StatusInternalServerError) |
||||
|
return |
||||
|
} |
||||
|
if err = WebRTCPlugin.Subscribe(streamPath, &suber); err != nil { |
||||
|
http.Error(w, err.Error(), http.StatusBadRequest) |
||||
|
return |
||||
|
} |
||||
|
if sdp, err := suber.GetAnswerV3(); err == nil { |
||||
|
ret := WebRtcReturn{} |
||||
|
_ = json.Unmarshal(sdp, &ret) |
||||
|
ret.IsH265 = suber.isH265 |
||||
|
byt, _ := json.Marshal(ret) |
||||
|
_, _ = w.Write(byt) |
||||
|
// w.Write(sdp)
|
||||
|
} else { |
||||
|
http.Error(w, err.Error(), http.StatusBadRequest) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func (IO *WebRTCIO) GetAnswerV3() ([]byte, error) { |
||||
|
// Sets the LocalDescription, and starts our UDP listeners
|
||||
|
answer, err := IO.CreateAnswer(nil) |
||||
|
if err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
gatherComplete := GatheringCompletePromise(IO.PeerConnection) |
||||
|
if err := IO.SetLocalDescription(answer); err != nil { |
||||
|
return nil, err |
||||
|
} |
||||
|
<-gatherComplete |
||||
|
|
||||
|
if bytes, err := json.Marshal(IO.LocalDescription()); err != nil { |
||||
|
return bytes, err |
||||
|
} else { |
||||
|
return bytes, nil |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue