├── README.md
├── conf
├── conf.go
└── seal.yaml
├── cross_platform_linux
├── hls
├── hls.go
├── hls_aac_jitter.go
├── hls_avc_aac_codec.go
├── hls_cache.go
├── hls_codec_sample.go
├── hls_codec_sample_unit.go
├── hls_const.go
├── hls_file_write.go
├── hls_mpegts_frame.go
├── hls_mpegts_writer.go
├── hls_muxer.go
├── hls_segment.go
└── hls_ts_muxer.go
├── hls_server.go
├── kernel
├── mem_pool.go
└── tcp_socket.go
├── rtmp
├── co
│ ├── const.go
│ ├── consumer.go
│ ├── cycle.go
│ ├── gop_cache.go
│ ├── handshake.go
│ ├── msg_abort.go
│ ├── msg_ack.go
│ ├── msg_aggre.go
│ ├── msg_amf.go
│ ├── msg_audio.go
│ ├── msg_set_ack.go
│ ├── msg_set_band.go
│ ├── msg_set_chunk.go
│ ├── msg_user_ctrl.go
│ ├── msg_video.go
│ ├── on_recv_msg.go
│ ├── playing.go
│ ├── recv_msg.go
│ ├── res_ack.go
│ ├── send_msg.go
│ ├── send_pkt.go
│ └── source.go
├── flv
│ └── flv_codec.go
└── pt
│ ├── amf.go
│ ├── chunk.go
│ ├── handshake.go
│ ├── message.go
│ ├── packet.go
│ ├── packet_acknowlegement.go
│ ├── packet_band_width.go
│ ├── packet_call.go
│ ├── packet_call_res.go
│ ├── packet_close_stream.go
│ ├── packet_connect.go
│ ├── packet_connect_res.go
│ ├── packet_create_stream.go
│ ├── packet_create_stream_res.go
│ ├── packet_fmle_start.go
│ ├── packet_fmle_start_res.go
│ ├── packet_on_custom_data.go
│ ├── packet_on_meta_data.go
│ ├── packet_on_status_call.go
│ ├── packet_on_status_data.go
│ ├── packet_onbw_done.go
│ ├── packet_pause.go
│ ├── packet_play.go
│ ├── packet_play_res.go
│ ├── packet_publish.go
│ ├── packet_sample_access.go
│ ├── packet_set_chunk_size.go
│ ├── packet_set_peer_bandwidth.go
│ ├── packet_set_window_ack_size.go
│ ├── packet_user_control.go
│ ├── stack.go
│ └── time_jitter.go
├── rtmp_server.go
├── seal.go
└── seal_vs_srs.md
/README.md:
--------------------------------------------------------------------------------
1 | # Seal
2 |
3 | seal is rtmp server written by go language, main refer to rtmp server open source https://github.com/ossrs/srs
4 |
5 | ## Usage
6 | * build
7 |
8 | download https://github.com/calabashdad/seal to ```go path```, run ```go build```
9 |
10 | you can also use cross platform build, like build a linux version if you are on mac, run ```cross_platform_linux```
11 |
12 | * run console mode
13 |
14 | ```./seal -c seal.yaml```
15 | * run daemon mode
16 |
17 | ```nohup ./seal -c seal.yaml &```
18 | * mock stream publish
19 |
20 |
for((;;)); do \
21 | ffmpeg -re -i lindan.flv \
22 | -vcodec copy -acodec copy \
23 | -f flv -y rtmp://127.0.0.1/live/test; \
24 | sleep 3
25 | done
26 |
27 | * use vlc play
28 |
29 | rtmp ```rtmp://127.0.0.1/live/test```
30 |
31 | hls ```http://127.0.0.1:35418/live/test.m3u8```
32 |
33 | http-flv ```http://127.0.0.1:35418/live/test.flv```
34 |
35 | ## platform
36 | go is cross platform
37 | * linux
38 | * mac
39 | * windows
40 |
41 | ## support
42 | * rtmp protocol (h264 aac)
43 | * hls (include http server)
44 | * http-flv (include http server)
45 |
46 | ## plan to support
47 | * h265
48 | * transcode(audio to aac)
49 | * http stats query
50 | * video on demand
51 | * video encry
52 | * auth token dynamicly
53 | * mini rtmp server in embed device
--------------------------------------------------------------------------------
/conf/conf.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "io/ioutil"
5 | "log"
6 | "os"
7 |
8 | "github.com/calabashdad/utiltools"
9 | "github.com/yaml"
10 | )
11 |
12 | // GlobalConfInfo global config info
13 | var GlobalConfInfo confInfo
14 |
15 | type systemConfInfo struct {
16 | CPUNums uint32 `yaml:"cpuNums"`
17 | }
18 |
19 | type rtmpConfInfo struct {
20 | Listen string `yaml:"listen"`
21 | TimeOut uint32 `yaml:"timeout"`
22 | ChunkSize uint32 `yaml:"chunkSize"`
23 | Atc bool `yaml:"atc"`
24 | AtcAuto bool `yaml:"atcAuto"`
25 | TimeJitter uint32 `yaml:"timeJitter"`
26 | ConsumerQueueSize uint32 `yaml:"consumerQueueSize"`
27 | }
28 |
29 | type hlsConfInfo struct {
30 | Enable string `yaml:"enable"`
31 | HlsFragment int `yaml:"hlsFragment"`
32 | HlsWindow int `yaml:"hlsWindow"`
33 | HlsPath string `yaml:"hlsPath"`
34 | HttpListen string `yaml:"httpListen"`
35 | }
36 |
37 | type confInfo struct {
38 | System systemConfInfo `yaml:"system"`
39 | Rtmp rtmpConfInfo `yaml:"rtmp"`
40 | Hls hlsConfInfo `yaml:"hls"`
41 | }
42 |
43 | func (t *confInfo) Loads(c string) (err error) {
44 | defer func() {
45 | if err := recover(); err != nil {
46 | log.Println(utiltools.PanicTrace())
47 | }
48 |
49 | }()
50 |
51 | var f *os.File
52 | if f, err = os.Open(c); err != nil {
53 | log.Println("Open config failed, err is", err)
54 | return err
55 | }
56 | defer f.Close()
57 |
58 | var data []byte
59 | if data, err = ioutil.ReadAll(f); err != nil {
60 | log.Println("config file loads failed, ", err.Error())
61 | return err
62 | }
63 |
64 | if err = yaml.Unmarshal(data, t); err != nil {
65 | log.Println("error:", err.Error())
66 | return err
67 | }
68 |
69 | return nil
70 | }
71 |
--------------------------------------------------------------------------------
/conf/seal.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | # system config
3 | system:
4 | # set cpu nums of app run
5 | # 0. auto, app will detect cpu nums by itself
6 | # 1,2,3... app will run on cpuNums
7 | # recommand: 0
8 | cpuNums: 0
9 |
10 | # rmtp protocol config
11 | rtmp:
12 | # rtmp server listen port.
13 | # recommand is 1935
14 | listen: 1935
15 |
16 | # defalut is 30 seonds.
17 | timeout: 30
18 |
19 | # chunk size. [128, 65535]
20 | # recommand is 60000
21 | chunkSize: 60000
22 |
23 | # atc whether atc(use absolute time and donot adjust time),
24 | # directly use msg time and donot adjust if atc is true,
25 | # otherwise, adjust msg time to start from 0 to make flash happy.
26 | # recommand is true.
27 | atc: false
28 |
29 | # atcAuto if bravo-atc find in metadata, open atc.
30 | # recommand is true.
31 | atcAuto: true
32 |
33 | # the time jitter algorithm:
34 | # 1. full, to ensure stream start at zero, and ensure stream monotonically increasing.
35 | # 2. zero, only ensure sttream start at zero, ignore timestamp jitter.
36 | # 3. off, disable the time jitter algorithm, like atc.
37 | # recommand is 1 (full)
38 | timeJitter: 1
39 |
40 | # the message queue size for the consumer, like player.
41 | # limit the size in seconds, and drop the old msg if full.
42 | consumerQueueSize: 5
43 |
44 | # hls protocol config
45 | hls:
46 | # enable true is open hls, false close
47 | enable: true
48 |
49 | # hls fragement time, in seconds
50 | # a fragment is a ts file.
51 | hlsFragment: 4
52 |
53 | # get the hls window time, in seconds
54 | # a window is a set of ts collection in m3u8
55 | # tsNums * hlsFragment
56 | hlsWindow: 20
57 |
58 | # the ts/m3u8 file store path
59 | hlsPath: /Users/yangkai/tmp
60 |
61 | # http server for hls
62 | # request format is http://ip:port/app/stream.m3u8
63 | # e.g. http://127.0.0.1:35418/live/test.m3u8
64 | httpListen: 7001
65 |
--------------------------------------------------------------------------------
/cross_platform_linux:
--------------------------------------------------------------------------------
1 | #cross platform linux
2 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
--------------------------------------------------------------------------------
/hls/hls.go:
--------------------------------------------------------------------------------
1 | package hls
2 |
3 | import (
4 | "log"
5 | "seal/rtmp/pt"
6 |
7 | "github.com/calabashdad/utiltools"
8 | )
9 |
10 | // SourceStream delivery RTMP stream to HLS(m3u8 and ts),
11 | type SourceStream struct {
12 | muxer *hlsMuxer
13 | cache *hlsCache
14 |
15 | codec *avcAacCodec
16 | sample *codecSample
17 | jitter *pt.TimeJitter
18 |
19 | // we store the stream dts,
20 | // for when we notice the hls cache to publish,
21 | // it need to know the segment start dts.
22 | //
23 | // for example. when republish, the stream dts will
24 | // monotonically increase, and the ts dts should start
25 | // from current dts.
26 | //
27 | // or, simply because the HlsCache never free when unpublish,
28 | // so when publish or republish it must start at stream dts,
29 | // not zero dts.
30 | streamDts int64
31 | }
32 |
33 | // NewSourceStream new a hls source stream
34 | func NewSourceStream() *SourceStream {
35 | return &SourceStream{
36 | muxer: newHlsMuxer(),
37 | cache: newHlsCache(),
38 |
39 | codec: newAvcAacCodec(),
40 | sample: newCodecSample(),
41 | jitter: pt.NewTimeJitter(),
42 | }
43 | }
44 |
45 | // OnMeta process metadata
46 | func (hls *SourceStream) OnMeta(pkt *pt.OnMetaDataPacket) (err error) {
47 | defer func() {
48 | if err := recover(); err != nil {
49 | log.Println(utiltools.PanicTrace())
50 | }
51 | }()
52 |
53 | if nil == pkt {
54 | return
55 | }
56 |
57 | if err = hls.codec.metaDataDemux(pkt); err != nil {
58 | return
59 | }
60 |
61 | return
62 | }
63 |
64 | // OnAudio process on audio data, mux to ts
65 | func (hls *SourceStream) OnAudio(msg *pt.Message) (err error) {
66 | defer func() {
67 | if err := recover(); err != nil {
68 | log.Println(utiltools.PanicTrace())
69 | }
70 | }()
71 |
72 | hls.sample.clear()
73 | if err = hls.codec.audioAacDemux(msg.Payload.Payload, hls.sample); err != nil {
74 | log.Println("hls codec demux audio failed, err=", err)
75 | return
76 | }
77 |
78 | if hls.codec.audioCodecID != pt.RtmpCodecAudioAAC {
79 | //log.Println("codec audio codec id is not aac, codeID=", hls.codec.audioCodecID)
80 | return
81 | }
82 |
83 | // ignore sequence header
84 | if pt.RtmpCodecAudioTypeSequenceHeader == hls.sample.aacPacketType {
85 | if err = hls.cache.onSequenceHeader(hls.muxer); err != nil {
86 | log.Println("hls cache on sequence header failed, err=", err)
87 | return
88 | }
89 |
90 | return
91 | }
92 |
93 | hls.jitter.Correct(msg, 0, 0, pt.RtmpTimeJitterFull)
94 |
95 | // the pts calc from rtmp/flv header
96 | pts := int64(msg.Header.Timestamp * 90)
97 |
98 | // for pure audio, update the stream dts also
99 | hls.streamDts = pts
100 |
101 | if err = hls.cache.writeAudio(hls.codec, hls.muxer, pts, hls.sample); err != nil {
102 | log.Println("hls cache write audio failed, err=", err)
103 | return
104 | }
105 |
106 | return
107 | }
108 |
109 | // OnVideo process on video data, mux to ts
110 | func (hls *SourceStream) OnVideo(msg *pt.Message) (err error) {
111 | defer func() {
112 | if err := recover(); err != nil {
113 | log.Println(utiltools.PanicTrace())
114 | }
115 | }()
116 |
117 | hls.sample.clear()
118 | if err = hls.codec.videoAvcDemux(msg.Payload.Payload, hls.sample); err != nil {
119 | log.Println("hls codec demuxer video failed, err=", err)
120 | return
121 | }
122 |
123 | // ignore info frame,
124 | if pt.RtmpCodecVideoAVCFrameVideoInfoFrame == hls.sample.frameType {
125 | return
126 | }
127 |
128 | if hls.codec.videoCodecID != pt.RtmpCodecVideoAVC {
129 | return
130 | }
131 |
132 | // ignore sequence header
133 | if pt.RtmpCodecVideoAVCFrameKeyFrame == hls.sample.frameType &&
134 | pt.RtmpCodecVideoAVCTypeSequenceHeader == hls.sample.frameType {
135 | return hls.cache.onSequenceHeader(hls.muxer)
136 | }
137 |
138 | hls.jitter.Correct(msg, 0, 0, pt.RtmpTimeJitterFull)
139 |
140 | dts := msg.Header.Timestamp * 90
141 | hls.streamDts = int64(dts)
142 |
143 | if err = hls.cache.writeVideo(hls.codec, hls.muxer, int64(dts), hls.sample); err != nil {
144 | log.Println("hls cache write video failed")
145 | return
146 | }
147 |
148 | return
149 | }
150 |
151 | // OnPublish publish stream event, continue to write the m3u8,
152 | // for the muxer object not destroyed.
153 | func (hls *SourceStream) OnPublish(app string, stream string) (err error) {
154 | defer func() {
155 | if err := recover(); err != nil {
156 | log.Println(utiltools.PanicTrace())
157 | }
158 | }()
159 |
160 | if err = hls.cache.onPublish(hls.muxer, app, stream, hls.streamDts); err != nil {
161 | return
162 | }
163 |
164 | return
165 | }
166 |
167 | // OnUnPublish the unpublish event, only close the muxer, donot destroy the
168 | // muxer, for when we continue to publish, the m3u8 will continue.
169 | func (hls *SourceStream) OnUnPublish() (err error) {
170 | defer func() {
171 | if err := recover(); err != nil {
172 | log.Println(utiltools.PanicTrace())
173 | }
174 | }()
175 |
176 | if err = hls.cache.onUnPublish(hls.muxer); err != nil {
177 | return
178 | }
179 |
180 | return
181 | }
182 |
183 | func (hls *SourceStream) hlsMux() {
184 | defer func() {
185 | if err := recover(); err != nil {
186 | log.Println(utiltools.PanicTrace())
187 | }
188 | }()
189 | return
190 | }
191 |
--------------------------------------------------------------------------------
/hls/hls_aac_jitter.go:
--------------------------------------------------------------------------------
1 | package hls
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/calabashdad/utiltools"
7 | )
8 |
9 | // jitter correct for audio,
10 | // the sample rate 44100/32000 will lost precise,
11 | // when mp4/ts(tbn=90000) covert to flv/rtmp(1000),
12 | // so the Hls on ipad or iphone will corrupt,
13 | // @see nginx-rtmp: est_pts
14 | type hlsAacJitter struct {
15 | basePts int64
16 | nbSamples int64
17 | syncMs int
18 | }
19 |
20 | func newHlsAacJitter() *hlsAacJitter {
21 | return &hlsAacJitter{
22 | syncMs: hlsConfDefaultAacSync,
23 | }
24 | }
25 |
26 | // when buffer start, calc the "correct" pts for ts,
27 | // @param flv_pts, the flv pts calc from flv header timestamp,
28 | // @param sample_rate, the sample rate in format(flv/RTMP packet header).
29 | // @param aac_sample_rate, the sample rate in codec(sequence header).
30 | // @return the calc correct pts.
31 | func (ha *hlsAacJitter) onBufferStart(flvPts int64, sampleRate int, aacSampleRate int) (calcCorrectPts int64) {
32 | defer func() {
33 | if err := recover(); err != nil {
34 | log.Println(utiltools.PanicTrace())
35 | }
36 | }()
37 |
38 | // use sample rate in flv/RTMP.
39 | flvSampleRate := flvSampleRates[sampleRate&0x03]
40 |
41 | // override the sample rate by sequence header
42 | if hlsAacSampleRateUnset != aacSampleRate {
43 | flvSampleRate = aacSampleRates[aacSampleRate]
44 | }
45 |
46 | // sync time set to 0, donot adjust the aac timestamp.
47 | if 0 == ha.syncMs {
48 | return flvPts
49 | }
50 |
51 | // @see: ngx_rtmp_hls_audio
52 | // drop the rtmp audio packet timestamp, re-calc it by sample rate.
53 | //
54 | // resample for the tbn of ts is 90000, flv is 1000,
55 | // we will lost timestamp if use audio packet timestamp,
56 | // so we must resample. or audio will corrupt in IOS.
57 | estPts := ha.basePts + ha.nbSamples*int64(90000)*int64(hlsAacSampleSize)/int64(flvSampleRate)
58 | dpts := estPts - flvPts
59 |
60 | if (dpts <= int64(ha.syncMs)*90) && (dpts >= int64(ha.syncMs)*int64(-90)) {
61 | ha.nbSamples++
62 | return estPts
63 | }
64 |
65 | // resync
66 | ha.basePts = flvPts
67 | ha.nbSamples = 1
68 |
69 | return flvPts
70 | }
71 |
72 | // when buffer continue, muxer donot write to file,
73 | // the audio buffer continue grow and donot need a pts,
74 | // for the ts audio PES packet only has one pts at the first time.
75 | func (ha *hlsAacJitter) onBufferContinue() {
76 | defer func() {
77 | if err := recover(); err != nil {
78 | log.Println(utiltools.PanicTrace())
79 | }
80 | }()
81 |
82 | ha.nbSamples++
83 |
84 | return
85 | }
86 |
--------------------------------------------------------------------------------
/hls/hls_avc_aac_codec.go:
--------------------------------------------------------------------------------
1 | package hls
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "seal/rtmp/pt"
7 |
8 | "encoding/binary"
9 | "github.com/calabashdad/utiltools"
10 | )
11 |
12 | // the h264/avc and aac codec, for media stream.
13 | //
14 | // to demux the FLV/RTMP video/audio packet to sample,
15 | // add each NALUs of h.264 as a sample unit to sample,
16 | // while the entire aac raw data as a sample unit.
17 | //
18 | // for sequence header,
19 | // demux it and save it in the avc_extra_data and aac_extra_data,
20 | //
21 | // for the codec info, such as audio sample rate,
22 | // decode from FLV/RTMP header, then use codec info in sequence
23 | // header to override it.
24 | type avcAacCodec struct {
25 | // metadata specified
26 | duration int
27 | width int
28 | height int
29 | frameRate int
30 |
31 | videoCodecID int
32 | videoDataRate int // in bps
33 | audioCodecID int
34 | audioDataRate int // in bps
35 |
36 | // video specified
37 | // profile_idc, H.264-AVC-ISO_IEC_14496-10.pdf, page 45.
38 | avcProfile uint8
39 | // level_idc, H.264-AVC-ISO_IEC_14496-10.pdf, page 45.
40 | avcLevel uint8
41 | // lengthSizeMinusOne, H.264-AVC-ISO_IEC_14496-15.pdf, page 16
42 | nalUnitLength int8
43 | sequenceParameterSetLength uint16
44 | sequenceParameterSetNALUnit []byte
45 | pictureParameterSetLength uint16
46 | pictureParameterSetNALUnit []byte
47 |
48 | // audio specified
49 | // 1.6.2.1 AudioSpecificConfig, in aac-mp4a-format-ISO_IEC_14496-3+2001.pdf, page 33.
50 | // audioObjectType, value defines in 7.1 Profiles, aac-iso-13818-7.pdf, page 40.
51 | aacProfile uint8
52 | // samplingFrequencyIndex
53 | aacSampleRate uint8
54 | // channelConfiguration
55 | aacChannels uint8
56 |
57 | // the avc extra data, the AVC sequence header,
58 | // without the flv codec header,
59 | // @see: ffmpeg, AVCodecContext::extradata
60 | avcExtraSize int
61 | avcExtraData []byte
62 | // the aac extra data, the AAC sequence header,
63 | // without the flv codec header,
64 | // @see: ffmpeg, AVCodecContext::extradata
65 | aacExtraSize int
66 | aacExtraData []byte
67 | }
68 |
69 | func newAvcAacCodec() *avcAacCodec {
70 | return &avcAacCodec{
71 | aacSampleRate: hlsAacSampleRateUnset,
72 | }
73 | }
74 |
75 | // demux the metadata, to get the stream info,
76 | // for instance, the width/height, sample rate.
77 | // @param metadata, the metadata amf0 object. assert not NULL.
78 | func (codec *avcAacCodec) metaDataDemux(pkt *pt.OnMetaDataPacket) (err error) {
79 | defer func() {
80 | if err := recover(); err != nil {
81 | log.Println(utiltools.PanicTrace())
82 | }
83 | }()
84 |
85 | if nil == pkt {
86 | return
87 | }
88 |
89 | if v := pkt.GetProperty("duration"); v != nil {
90 | codec.duration = int(v.(float64))
91 | }
92 |
93 | if v := pkt.GetProperty("width"); v != nil {
94 | codec.width = int(v.(float64))
95 | }
96 |
97 | if v := pkt.GetProperty("height"); v != nil {
98 | codec.height = int(v.(float64))
99 | }
100 |
101 | if v := pkt.GetProperty("framerate"); v != nil {
102 | codec.frameRate = int(v.(float64))
103 | }
104 |
105 | if v := pkt.GetProperty("videocodecid"); v != nil {
106 | codec.videoCodecID = int(v.(float64))
107 | }
108 |
109 | if v := pkt.GetProperty("videodatarate"); v != nil {
110 | codec.videoDataRate = int(1000 * v.(float64))
111 | }
112 |
113 | if v := pkt.GetProperty("audiocodecid"); v != nil {
114 | codec.audioCodecID = int(v.(float64))
115 | }
116 |
117 | if v := pkt.GetProperty("audiodatarate"); v != nil {
118 | codec.audioDataRate = int(1000 * v.(float64))
119 | }
120 |
121 | return
122 | }
123 |
124 | // demux the audio packet in aac codec.
125 | // the packet mux in FLV/RTMP format defined in flv specification.
126 | // demux the audio speicified data(sound_format, sound_size, ...) to sample.
127 | // demux the aac specified data(aac_profile, ...) to codec from sequence header.
128 | // demux the aac raw to sample units.
129 | func (codec *avcAacCodec) audioAacDemux(data []byte, sample *codecSample) (err error) {
130 | defer func() {
131 | if err := recover(); err != nil {
132 | log.Println(utiltools.PanicTrace())
133 | }
134 | }()
135 |
136 | sample.isVideo = false
137 |
138 | dataLen := len(data)
139 | if dataLen <= 0 {
140 | return
141 | }
142 |
143 | var offset int
144 |
145 | if dataLen-offset < 1 {
146 | return
147 | }
148 |
149 | // @see: E.4.2 Audio Tags, video_file_format_spec_v10_1.pdf, page 76
150 | soundFormat := data[offset]
151 | offset++
152 |
153 | soundType := soundFormat & 0x01
154 | soundSize := (soundFormat >> 1) & 0x01
155 | soundRate := (soundFormat >> 2) & 0x03
156 | soundFormat = (soundFormat >> 4) & 0x0f
157 |
158 | codec.audioCodecID = int(soundFormat)
159 | sample.soundType = int(soundType)
160 | sample.soundRate = int(soundRate)
161 | sample.soundSize = int(soundSize)
162 |
163 | // only support for aac
164 | if pt.RtmpCodecAudioAAC != codec.audioCodecID {
165 | //log.Println("hls only support audio aac, actual is ", codec.audioCodecID)
166 | return
167 | }
168 |
169 | if dataLen-offset < 1 {
170 | return
171 | }
172 |
173 | aacPacketType := data[offset]
174 | offset++
175 | sample.aacPacketType = int(aacPacketType)
176 |
177 | if pt.RtmpCodecAudioTypeSequenceHeader == aacPacketType {
178 | // AudioSpecificConfig
179 | // 1.6.2.1 AudioSpecificConfig, in aac-mp4a-format-ISO_IEC_14496-3+2001.pdf, page 33.
180 | codec.aacExtraSize = dataLen - offset
181 | if codec.aacExtraSize > 0 {
182 | codec.aacExtraData = make([]byte, codec.aacExtraSize)
183 | copy(codec.aacExtraData, data[offset:])
184 | }
185 |
186 | // only need to decode the first 2bytes:
187 | // audioObjectType, aac_profile, 5bits.
188 | // samplingFrequencyIndex, aac_sample_rate, 4bits.
189 | // channelConfiguration, aac_channels, 4bits
190 | if dataLen-offset < 2 {
191 | return
192 | }
193 |
194 | codec.aacProfile = data[offset]
195 | offset++
196 | codec.aacSampleRate = data[offset]
197 | offset++
198 |
199 | codec.aacChannels = (codec.aacSampleRate >> 3) & 0x0f
200 | codec.aacSampleRate = ((codec.aacProfile << 1) & 0x0e) | ((codec.aacSampleRate >> 7) & 0x01)
201 | codec.aacProfile = (codec.aacProfile >> 3) & 0x1f
202 |
203 | if 0 == codec.aacProfile || 0x1f == codec.aacProfile {
204 | err = fmt.Errorf("hls decdoe audio aac sequence header failed, aac profile=%d", codec.aacProfile)
205 | return
206 | }
207 |
208 | // the profile = object_id + 1
209 | // @see aac-mp4a-format-ISO_IEC_14496-3+2001.pdf, page 78,
210 | // Table 1. A.9 MPEG-2 Audio profiles and MPEG-4 Audio object types
211 | // so the aac_profile should plus 1, not minus 1, and nginx-rtmp used it to
212 | // downcast aac SSR to LC.
213 | codec.aacProfile--
214 |
215 | } else if pt.RtmpCodecAudioTypeRawData == aacPacketType {
216 | // ensure the sequence header demuxed
217 | if 0 == len(codec.aacExtraData) {
218 | return
219 | }
220 |
221 | // Raw AAC frame data in UI8 []
222 | // 6.3 Raw Data, aac-iso-13818-7.pdf, page 28
223 | if err = sample.addSampleUnit(data[offset:]); err != nil {
224 | return
225 | }
226 | }
227 |
228 | // reset the sample rate by sequence header
229 | if codec.aacSampleRate != hlsAacSampleRateUnset {
230 | var aacSampleRates = []int{
231 | 96000, 88200, 64000, 48000,
232 | 44100, 32000, 24000, 22050,
233 | 16000, 12000, 11025, 8000,
234 | 7350, 0, 0, 0,
235 | }
236 |
237 | switch aacSampleRates[codec.aacSampleRate] {
238 | case 11025:
239 | sample.soundRate = pt.RtmpCodecAudioSampleRate11025
240 | case 22050:
241 | sample.soundRate = pt.RtmpCodecAudioSampleRate22050
242 | case 44100:
243 | sample.soundRate = pt.RtmpCodecAudioSampleRate44100
244 | default:
245 | }
246 | }
247 |
248 | return
249 | }
250 |
251 | // demux the video packet in h.264 codec.
252 | // the packet mux in FLV/RTMP format defined in flv specification.
253 | // demux the video specified data(frame_type, codec_id, ...) to sample.
254 | // demux the h.264 sepcified data(avc_profile, ...) to codec from sequence header.
255 | // demux the h.264 NALUs to sampe units.
256 | func (codec *avcAacCodec) videoAvcDemux(data []byte, sample *codecSample) (err error) {
257 | defer func() {
258 | if err := recover(); err != nil {
259 | log.Println(utiltools.PanicTrace())
260 | }
261 | }()
262 |
263 | sample.isVideo = true
264 |
265 | maxLen := len(data)
266 |
267 | if maxLen <= 0 {
268 | return
269 | }
270 |
271 | var offset int
272 |
273 | // video decode
274 | if maxLen-offset < 1 {
275 | return
276 | }
277 |
278 | // @see: E.4.3 Video Tags, video_file_format_spec_v10_1.pdf, page 78
279 | frameType := data[offset]
280 | offset++
281 |
282 | codecID := frameType & 0x0f
283 | frameType = (frameType >> 4) & 0x0f
284 |
285 | sample.frameType = int(frameType)
286 |
287 | // ignore info frame without error
288 | if pt.RtmpCodecVideoAVCFrameVideoInfoFrame == sample.frameType {
289 | return
290 | }
291 |
292 | // only support h.264/avc
293 | if pt.RtmpCodecVideoAVC != codecID {
294 | return
295 | }
296 | codec.videoCodecID = int(codecID)
297 |
298 | if maxLen-offset < 4 {
299 | return
300 | }
301 |
302 | avcPacketType := data[offset]
303 | offset++
304 |
305 | // 3bytes,
306 | compositionTime := int(data[offset])<<16 + int(data[offset+1])<<8 + int(data[offset+2])
307 | offset += 3
308 |
309 | // pts = dts + cts
310 | sample.cts = compositionTime
311 | sample.avcPacketType = int(avcPacketType)
312 |
313 | if pt.RtmpCodecVideoAVCTypeSequenceHeader == avcPacketType {
314 | // AVCDecoderConfigurationRecord
315 | // 5.2.4.1.1 Syntax, H.264-AVC-ISO_IEC_14496-15.pdf, page 16
316 | codec.avcExtraSize = maxLen - offset
317 | if codec.avcExtraSize > 0 {
318 | codec.avcExtraData = make([]byte, codec.avcExtraSize)
319 | copy(codec.avcExtraData, data[offset:offset+codec.avcExtraSize])
320 | }
321 |
322 | if maxLen-offset < 6 {
323 | return
324 | }
325 |
326 | // configurationVersion
327 | offset++
328 |
329 | // AVCProfileIndication
330 | codec.avcProfile = data[offset]
331 | offset++
332 |
333 | // profile_compatibility
334 | offset++
335 |
336 | // AVCLevelIndication
337 | codec.avcLevel = data[offset]
338 | offset++
339 |
340 | // parse the NALU size.
341 | lengthSizeMinusOne := data[offset]
342 | offset++
343 |
344 | lengthSizeMinusOne &= 0x03
345 | codec.nalUnitLength = int8(lengthSizeMinusOne)
346 |
347 | // 1 sps
348 | if maxLen-offset < 1 {
349 | return
350 | }
351 |
352 | numOfSequenceParameterSets := data[offset]
353 | offset++
354 | numOfSequenceParameterSets &= 0x1f
355 | if numOfSequenceParameterSets != 1 {
356 | err = fmt.Errorf("hsl decode avc sequence header size failed")
357 | return
358 | }
359 |
360 | if maxLen-offset < 2 {
361 | return
362 | }
363 |
364 | codec.sequenceParameterSetLength = binary.BigEndian.Uint16(data[offset : offset+2])
365 | offset += 2
366 |
367 | if maxLen-offset < int(codec.sequenceParameterSetLength) {
368 | err = fmt.Errorf("hsl decode avc sequence header data failed")
369 | return
370 | }
371 |
372 | if codec.sequenceParameterSetLength > 0 {
373 | codec.sequenceParameterSetNALUnit = make([]byte, codec.sequenceParameterSetLength)
374 | copy(codec.sequenceParameterSetNALUnit, data[offset:offset+int(codec.sequenceParameterSetLength)])
375 | offset += int(codec.sequenceParameterSetLength)
376 | }
377 |
378 | // 1 pps
379 | if maxLen-offset < 1 {
380 | return
381 | }
382 |
383 | numOfPictureParameterSets := data[offset]
384 | offset++
385 |
386 | if numOfPictureParameterSets != 1 {
387 | err = fmt.Errorf("hls decode video avc sequence header pps failed")
388 | return
389 | }
390 |
391 | if maxLen-offset < 2 {
392 | return
393 | }
394 |
395 | codec.pictureParameterSetLength = binary.BigEndian.Uint16(data[offset : offset+2])
396 | offset += 2
397 |
398 | if maxLen-offset < int(codec.pictureParameterSetLength) {
399 | return
400 | }
401 |
402 | if codec.pictureParameterSetLength > 0 {
403 | codec.pictureParameterSetNALUnit = make([]byte, codec.pictureParameterSetLength)
404 | copy(codec.pictureParameterSetNALUnit, data[offset:offset+int(codec.pictureParameterSetLength)])
405 | offset += int(codec.pictureParameterSetLength)
406 | }
407 |
408 | } else if pt.RtmpCodecVideoAVCTypeNALU == avcPacketType {
409 | // ensure the sequence header demuxed
410 | if len(codec.pictureParameterSetNALUnit) <= 0 {
411 | return
412 | }
413 |
414 | // One or more NALUs (Full frames are required)
415 | // 5.3.4.2.1 Syntax, H.264-AVC-ISO_IEC_14496-15.pdf, page 20
416 |
417 | pictureLength := maxLen - offset
418 | for i := 0; i < pictureLength; {
419 | if maxLen-offset < int(codec.nalUnitLength)+1 {
420 | return
421 | }
422 |
423 | nalUnitLength := 0
424 | switch codec.nalUnitLength {
425 | case 3:
426 | nalUnitLength = int(binary.BigEndian.Uint32(data[offset : offset+4]))
427 | offset += 4
428 | case 2:
429 | nalUnitLength = int(data[offset])<<16 + int(data[offset+1])<<8 + int(data[offset+2])
430 | offset += 3
431 | case 1:
432 | nalUnitLength = int(binary.BigEndian.Uint16(data[offset : offset+2]))
433 | offset += 2
434 | default:
435 | nalUnitLength = int(data[offset])
436 | offset++
437 | }
438 |
439 | // maybe stream is AnnexB format.
440 | if nalUnitLength < 0 {
441 | return
442 | }
443 |
444 | // NALUnit
445 | if maxLen-offset < nalUnitLength {
446 | return
447 | }
448 |
449 | // 7.3.1 NAL unit syntax, H.264-AVC-ISO_IEC_14496-10.pdf, page 44.
450 | if err = sample.addSampleUnit(data[offset : offset+nalUnitLength]); err != nil {
451 | err = fmt.Errorf("hls add video sample failed")
452 | return
453 | }
454 | offset += nalUnitLength
455 |
456 | i += int(codec.nalUnitLength) + 1 + nalUnitLength
457 | }
458 |
459 | }
460 |
461 | return
462 | }
463 |
--------------------------------------------------------------------------------
/hls/hls_cache.go:
--------------------------------------------------------------------------------
1 | package hls
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "seal/conf"
7 |
8 | "github.com/calabashdad/utiltools"
9 | "seal/rtmp/pt"
10 | )
11 |
12 | // hls stream cache,
13 | // use to cache hls stream and flush to hls muxer.
14 | //
15 | // when write stream to ts file:
16 | // video frame will directly flush to M3u8Muxer,
17 | // audio frame need to cache, because it's small and flv tbn problem.
18 | //
19 | // whatever, the Hls cache used to cache video/audio,
20 | // and flush video/audio to m3u8 muxer if needed.
21 | //
22 | // about the flv tbn problem:
23 | // flv tbn is 1/1000, ts tbn is 1/90000,
24 | // when timestamp convert to flv tbn, it will loose precise,
25 | // so we must gather audio frame together, and recalc the timestamp @see SrsHlsAacJitter,
26 | // we use a aac jitter to correct the audio pts.
27 | type hlsCache struct {
28 | // current frame and buffer
29 | af *mpegTsFrame
30 | ab []byte
31 | vf *mpegTsFrame
32 | vb []byte
33 |
34 | // the audio cache buffer start pts, to flush audio if full
35 | audioBufferStartPts int64
36 | // time jitter for aac
37 | aacJitter *hlsAacJitter
38 | }
39 |
40 | func newHlsCache() *hlsCache {
41 | return &hlsCache{
42 | af: newMpegTsFrame(),
43 | vf: newMpegTsFrame(),
44 | aacJitter: newHlsAacJitter(),
45 | }
46 | }
47 |
48 | // when publish stream
49 | func (hc *hlsCache) onPublish(muxer *hlsMuxer, app string, stream string, segmentStartDts int64) (err error) {
50 | defer func() {
51 | if err := recover(); err != nil {
52 | log.Println(utiltools.PanicTrace())
53 | }
54 | }()
55 |
56 | hlsFragment := conf.GlobalConfInfo.Hls.HlsFragment
57 | hlsWindow := conf.GlobalConfInfo.Hls.HlsWindow
58 | hlsPath := conf.GlobalConfInfo.Hls.HlsPath
59 |
60 | // open muxer
61 | if err = muxer.updateConfig(app, stream, hlsPath, hlsFragment, hlsWindow); err != nil {
62 | return
63 | }
64 |
65 | if err = muxer.segmentOpen(segmentStartDts); err != nil {
66 | log.Println("segment open failed, err=", err)
67 | return
68 | }
69 |
70 | return
71 | }
72 |
73 | // when unpublish stream
74 | func (hc *hlsCache) onUnPublish(muxer *hlsMuxer) (err error) {
75 | defer func() {
76 | if err := recover(); err != nil {
77 | log.Println(utiltools.PanicTrace())
78 | }
79 | }()
80 |
81 | if err = muxer.flushAudio(hc.af, &hc.ab); err != nil {
82 | log.Println("m3u8 muxer flush audio failed, err=", err)
83 | return
84 | }
85 |
86 | if err = muxer.segmentClose("unpublish"); err != nil {
87 | return
88 | }
89 |
90 | return
91 | }
92 |
93 | // when get sequence header,
94 | // must write a #EXT-X-DISCONTINUITY to m3u8.
95 | // @see: hls-m3u8-draft-pantos-http-live-streaming-12.txt
96 | // @see: 3.4.11. EXT-X-DISCONTINUITY
97 | func (hc *hlsCache) onSequenceHeader(muxer *hlsMuxer) (err error) {
98 | defer func() {
99 | if err := recover(); err != nil {
100 | log.Println(utiltools.PanicTrace())
101 | }
102 | }()
103 |
104 | if err = muxer.onSequenceHeader(); err != nil {
105 | return
106 | }
107 |
108 | return
109 | }
110 |
111 | // write audio to cache, if need to flush, flush to muxer
112 | func (hc *hlsCache) writeAudio(codec *avcAacCodec, muxer *hlsMuxer, pts int64, sample *codecSample) (err error) {
113 | defer func() {
114 | if err := recover(); err != nil {
115 | log.Println(utiltools.PanicTrace())
116 | }
117 | }()
118 |
119 | if 0 == len(hc.ab) {
120 | pts = hc.aacJitter.onBufferStart(pts, sample.soundRate, int(codec.aacSampleRate))
121 |
122 | hc.af.dts = pts
123 | hc.af.pts = pts
124 | hc.audioBufferStartPts = pts
125 |
126 | hc.af.pid = tsAudioPid
127 | hc.af.sid = tsAudioAac
128 | } else {
129 | hc.aacJitter.onBufferContinue()
130 | }
131 |
132 | // write audio to cache
133 | if err = hc.cacheAudio(codec, sample); err != nil {
134 | log.Println("hls cache audio failed, err=", err)
135 | return
136 | }
137 |
138 | if len(hc.ab) > hlsAudioCacheSize {
139 | if err = muxer.flushAudio(hc.af, &hc.ab); err != nil {
140 | log.Println("flush audio failed, err=", err)
141 | return
142 | }
143 |
144 | }
145 |
146 | //in ms, audio delay to flush the audios.
147 | var audioDelay = int64(hlsAacDelay)
148 | // flush if audio delay exceed
149 | if pts-hc.audioBufferStartPts > audioDelay*90 {
150 | if err = muxer.flushAudio(hc.af, &hc.ab); err != nil {
151 | return
152 | }
153 | }
154 |
155 | // reap when current source is pure audio.
156 | // it maybe changed when stream info changed,
157 | // for example, pure audio when start, audio/video when publishing,
158 | // pure audio again for audio disabled.
159 | // so we reap event when the audio incoming when segment overflow.
160 | // we use absolutely overflow of segment to make jwplayer/ffplay happy
161 | if muxer.isSegmentAbsolutelyOverflow() {
162 | if err = hc.reapSegment("audio", muxer, hc.af.pts); err != nil {
163 | log.Println("reap segment failed, err=", err)
164 | return
165 | }
166 | log.Println("reap segment success")
167 | }
168 |
169 | return
170 | }
171 |
172 | // wirte video to muxer
173 | func (hc *hlsCache) writeVideo(codec *avcAacCodec, muxer *hlsMuxer, dts int64, sample *codecSample) (err error) {
174 | defer func() {
175 | if err := recover(); err != nil {
176 | log.Println(utiltools.PanicTrace())
177 | }
178 | }()
179 |
180 | if err = hc.cacheVideo(codec, sample); err != nil {
181 | return
182 | }
183 |
184 | hc.vf.dts = dts
185 | hc.vf.pts = hc.vf.dts + int64(sample.cts)*int64(90)
186 | hc.vf.pid = tsVideoPid
187 | hc.vf.sid = tsVideoAvc
188 | hc.vf.key = sample.frameType == pt.RtmpCodecVideoAVCFrameKeyFrame
189 |
190 | // new segment when:
191 | // 1. base on gop.
192 | // 2. some gops duration overflow.
193 | if hc.vf.key && muxer.isSegmentOverflow() {
194 | if err = hc.reapSegment("video", muxer, hc.vf.dts); err != nil {
195 | return
196 | }
197 | }
198 |
199 | // flush video when got one
200 | if err = muxer.flushVideo(hc.af, hc.ab, hc.vf, &hc.vb); err != nil {
201 | log.Println("m3u8 muxer flush video failed")
202 | return
203 | }
204 |
205 | return
206 | }
207 |
208 | // reopen the muxer for a new hls segment,
209 | // close current segment, open a new segment,
210 | // then write the key frame to the new segment.
211 | // so, user must reap_segment then flush_video to hls muxer.
212 | func (hc *hlsCache) reapSegment(logDesc string, muxer *hlsMuxer, segmentStartDts int64) (err error) {
213 | defer func() {
214 | if err := recover(); err != nil {
215 | log.Println(utiltools.PanicTrace())
216 | }
217 | }()
218 |
219 | if err = muxer.segmentClose(logDesc); err != nil {
220 | log.Println("m3u8 muxer close segment failed, err=", err)
221 | return
222 | }
223 |
224 | if err = muxer.segmentOpen(segmentStartDts); err != nil {
225 | log.Println("m3u8 muxer open segment failed, err=", err)
226 | return
227 | }
228 |
229 | // segment open, flush the audio.
230 | // @see: ngx_rtmp_hls_open_fragment
231 | /* start fragment with audio to make iPhone happy */
232 | if err = muxer.flushAudio(hc.af, &hc.ab); err != nil {
233 | log.Println("m3u8 muxer flush audio failed, err=", err)
234 | return
235 | }
236 |
237 | return
238 | }
239 |
240 | func (hc *hlsCache) cacheAudio(codec *avcAacCodec, sample *codecSample) (err error) {
241 | defer func() {
242 | if err := recover(); err != nil {
243 | log.Println(utiltools.PanicTrace())
244 | }
245 | }()
246 |
247 | // AAC-ADTS
248 | // 6.2 Audio Data Transport Stream, ADTS
249 | // in aac-iso-13818-7.pdf, page 26.
250 | // fixed 7bytes header
251 | adtsHeader := [7]uint8{0xff, 0xf1, 0x00, 0x00, 0x00, 0x0f, 0xfc}
252 |
253 | for i := 0; i < sample.nbSampleUnits; i++ {
254 | sampleUnit := sample.sampleUnits[i]
255 | size := len(sampleUnit.payload)
256 |
257 | if size <= 0 || size > 0x1fff {
258 | err = fmt.Errorf("invalied aac frame length=%d", size)
259 | return
260 | }
261 |
262 | // the frame length is the AAC raw data plus the adts header size.
263 | frameLen := size + 7
264 |
265 | // adts_fixed_header
266 | // 2B, 16bits
267 | // int16_t syncword; //12bits, '1111 1111 1111'
268 | // int8_t ID; //1bit, '0'
269 | // int8_t layer; //2bits, '00'
270 | // int8_t protection_absent; //1bit, can be '1'
271 |
272 | // 12bits
273 | // int8_t profile; //2bit, 7.1 Profiles, page 40
274 | // TSAacSampleFrequency sampling_frequency_index; //4bits, Table 35, page 46
275 | // int8_t private_bit; //1bit, can be '0'
276 | // int8_t channel_configuration; //3bits, Table 8
277 | // int8_t original_or_copy; //1bit, can be '0'
278 | // int8_t home; //1bit, can be '0'
279 |
280 | // adts_variable_header
281 | // 28bits
282 | // int8_t copyright_identification_bit; //1bit, can be '0'
283 | // int8_t copyright_identification_start; //1bit, can be '0'
284 | // int16_t frame_length; //13bits
285 | // int16_t adts_buffer_fullness; //11bits, 7FF signals that the bitstream is a variable rate bitstream.
286 | // int8_t number_of_raw_data_blocks_in_frame; //2bits, 0 indicating 1 raw_data_block()
287 |
288 | // profile, 2bits
289 | adtsHeader[2] = (codec.aacProfile << 6) & 0xc0
290 | // sampling_frequency_index 4bits
291 | adtsHeader[2] |= (codec.aacSampleRate << 2) & 0x3c
292 | // channel_configuration 3bits
293 | adtsHeader[2] |= (codec.aacChannels >> 2) & 0x01
294 | adtsHeader[3] = (codec.aacChannels << 6) & 0xc0
295 | // frame_length 13bits
296 | adtsHeader[3] |= uint8((frameLen >> 11) & 0x03)
297 | adtsHeader[4] = uint8((frameLen >> 3) & 0xff)
298 | adtsHeader[5] = uint8((frameLen << 5) & 0xe0)
299 | // adts_buffer_fullness; //11bits
300 | adtsHeader[5] |= 0x1f
301 |
302 | // copy to audio buffer
303 | hc.ab = append(hc.ab, adtsHeader[:]...)
304 | hc.ab = append(hc.ab, sampleUnit.payload[:]...)
305 |
306 | }
307 |
308 | return
309 | }
310 |
311 | func (hc *hlsCache) cacheVideo(codec *avcAacCodec, sample *codecSample) (err error) {
312 | defer func() {
313 | if err := recover(); err != nil {
314 | log.Println(utiltools.PanicTrace())
315 | }
316 | }()
317 |
318 | // for type1/5/6, insert aud packet.
319 | audNal := []byte{0x00, 0x00, 0x00, 0x01, 0x09, 0xf0}
320 |
321 | spsPpsSent := false
322 | audSent := false
323 |
324 | // a ts sample is format as:
325 | // 00 00 00 01 // header
326 | // xxxxxxx // data bytes
327 | // 00 00 01 // continue header
328 | // xxxxxxx // data bytes.
329 | // so, for each sample, we append header in aud_nal, then appends the bytes in sample.
330 | for i := 0; i < sample.nbSampleUnits; i++ {
331 | sampleUnit := sample.sampleUnits[i]
332 | size := len(sampleUnit.payload)
333 |
334 | if size <= 0 {
335 | return
336 | }
337 |
338 | // step 1:
339 | // first, before each "real" sample,
340 | // we add some packets according to the nal_unit_type,
341 | // for example, when got nal_unit_type=5, insert SPS/PPS before sample.
342 |
343 | // 5bits, 7.3.1 NAL unit syntax,
344 | // H.264-AVC-ISO_IEC_14496-10.pdf, page 44.
345 | var nalUnitType uint8
346 | nalUnitType = sampleUnit.payload[0]
347 | nalUnitType &= 0x1f
348 |
349 | // @see: ngx_rtmp_hls_video
350 | // Table 7-1 – NAL unit type codes, page 61
351 | // 1: Coded slice
352 | if 1 == nalUnitType {
353 | spsPpsSent = false
354 | }
355 |
356 | // 6: Supplemental enhancement information (SEI) sei_rbsp( ), page 61
357 | // @see: ngx_rtmp_hls_append_aud
358 | if !audSent {
359 | // @remark, when got type 9, we donot send aud_nal, but it will make ios unhappy, so we remove it.
360 | if 1 == nalUnitType || 5 == nalUnitType || 6 == nalUnitType {
361 | hc.vb = append(hc.vb, audNal...)
362 | audSent = true
363 | }
364 | }
365 |
366 | // 5: Coded slice of an IDR picture.
367 | // insert sps/pps before IDR or key frame is ok.
368 | if 5 == nalUnitType && !spsPpsSent {
369 | spsPpsSent = true
370 |
371 | // @see: ngx_rtmp_hls_append_sps_pps
372 | if codec.sequenceParameterSetLength > 0 {
373 | // AnnexB prefix, for sps always 4 bytes header
374 | hc.vb = append(hc.vb, audNal[:4]...)
375 | // sps
376 | hc.vb = append(hc.vb, codec.sequenceParameterSetNALUnit[:codec.sequenceParameterSetLength]...)
377 | }
378 |
379 | if codec.pictureParameterSetLength > 0 {
380 | // AnnexB prefix, for pps always 4 bytes header
381 | hc.vb = append(hc.vb, audNal[:4]...)
382 | // pps
383 | hc.vb = append(hc.vb, codec.pictureParameterSetNALUnit[:codec.pictureParameterSetLength]...)
384 | }
385 | }
386 |
387 | // 7-9, ignore, @see: ngx_rtmp_hls_video
388 | if nalUnitType >= 7 && nalUnitType <= 9 {
389 | continue
390 | }
391 |
392 | // step 2:
393 | // output the "real" sample, in buf.
394 | // when we output some special assist packets according to nal_unit_type
395 |
396 | // sample start prefix, '00 00 00 01' or '00 00 01'
397 | pAudnal := 0 + 1
398 | endAudnal := pAudnal + 3
399 |
400 | // first AnnexB prefix is long (4 bytes)
401 | if 0 == len(hc.vb) {
402 | pAudnal = 0
403 | }
404 | hc.vb = append(hc.vb, audNal[pAudnal:pAudnal+endAudnal-pAudnal]...)
405 |
406 | // sample data
407 | hc.vb = append(hc.vb, sampleUnit.payload...)
408 | }
409 |
410 | return
411 | }
412 |
--------------------------------------------------------------------------------
/hls/hls_codec_sample.go:
--------------------------------------------------------------------------------
1 | package hls
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "seal/rtmp/pt"
7 |
8 | "github.com/calabashdad/utiltools"
9 | )
10 |
11 | // the samples in the flv audio/video packet.
12 | // the sample used to analysis a video/audio packet,
13 | // split the h.264 NALUs to buffers, or aac raw data to a buffer,
14 | // and decode the video/audio specified infos.
15 | //
16 | // the sample unit:
17 | // a video packet codec in h.264 contains many NALUs, each is a sample unit.
18 | // a audio packet codec in aac is a sample unit.
19 | // @remark, the video/audio sequence header is not sample unit,
20 | // all sequence header stores as extra data,
21 | // @remark, user must clear all samples before decode a new video/audio packet.
22 | type codecSample struct {
23 | // each audio/video raw data packet will dumps to one or multiple buffers,
24 | // the buffers will write to hls and clear to reset.
25 | // generally, aac audio packet corresponding to one buffer,
26 | // where avc/h264 video packet may contains multiple buffer.
27 | nbSampleUnits int
28 | sampleUnits [hlsMaxCodecSample]codecSampleUnit
29 |
30 | // whether the sample is video sample which demux from video packet.
31 | isVideo bool
32 |
33 | // CompositionTime, video_file_format_spec_v10_1.pdf, page 78.
34 | // cts = pts - dts, where dts = flvheader->timestamp.
35 | cts int
36 |
37 | // video specified
38 | frameType int
39 | avcPacketType int
40 |
41 | // audio specified
42 | soundRate int
43 | soundSize int
44 | soundType int
45 | aacPacketType int
46 | }
47 |
48 | func newCodecSample() *codecSample {
49 | return &codecSample{
50 | isVideo: false,
51 | frameType: pt.RtmpCodecVideoAVCFrameReserved,
52 | avcPacketType: pt.RtmpCodecVideoAVCTypeReserved,
53 | soundRate: pt.RtmpCodecAudioSampleRateReserved,
54 | soundSize: pt.RtmpCodecAudioSampleSizeReserved,
55 | soundType: pt.RtmpCodecAudioSoundTypeReserved,
56 | aacPacketType: pt.RtmpCodecAudioTypeReserved,
57 | }
58 | }
59 |
60 | // clear all samples.
61 | // in a word, user must clear sample before demux it.
62 | func (sample *codecSample) clear() (err error) {
63 |
64 | sample.isVideo = false
65 | sample.nbSampleUnits = 0
66 |
67 | sample.cts = 0
68 | sample.frameType = pt.RtmpCodecVideoAVCFrameReserved
69 | sample.avcPacketType = pt.RtmpCodecVideoAVCTypeReserved
70 |
71 | sample.soundRate = pt.RtmpCodecAudioSampleRateReserved
72 | sample.soundSize = pt.RtmpCodecAudioSampleSizeReserved
73 | sample.soundType = pt.RtmpCodecAudioSoundTypeReserved
74 | sample.aacPacketType = pt.RtmpCodecAudioTypeReserved
75 |
76 | return
77 | }
78 |
79 | func (sample *codecSample) addSampleUnit(data []byte) (err error) {
80 | defer func() {
81 | if err := recover(); err != nil {
82 | log.Println(utiltools.PanicTrace())
83 | }
84 | }()
85 |
86 | if sample.nbSampleUnits >= hlsMaxCodecSample {
87 | err = fmt.Errorf("hls decode samples error, exceed the max count, nbSampleUnits=%d", sample.nbSampleUnits)
88 | return
89 | }
90 |
91 | sample.sampleUnits[sample.nbSampleUnits].payload = data
92 | sample.nbSampleUnits++
93 |
94 | return
95 | }
96 |
--------------------------------------------------------------------------------
/hls/hls_codec_sample_unit.go:
--------------------------------------------------------------------------------
1 | package hls
2 |
3 | // the codec sample unit.
4 | // for h.264 video packet, a NALU is a sample unit.
5 | // for aac raw audio packet, a NALU is the entire aac raw data.
6 | // for sequence header, it's not a sample unit.
7 | type codecSampleUnit struct {
8 | payload []byte
9 | }
10 |
--------------------------------------------------------------------------------
/hls/hls_const.go:
--------------------------------------------------------------------------------
1 | package hls
2 |
3 | const (
4 | hlsMaxCodecSample = 128
5 | hlsAacSampleRateUnset = 15
6 | )
7 |
8 | const (
9 | // in ms, for HLS aac sync time.
10 | hlsConfDefaultAacSync = 100
11 | )
12 |
13 | // the mpegts header specifed the video/audio pid.
14 | const (
15 | tsVideoPid = 256
16 | tsAudioPid = 257
17 | )
18 |
19 | // ts aac stream id.
20 | const tsAudioAac = 0xc0
21 |
22 | // ts avc stream id.
23 | const tsVideoAvc = 0xe0
24 |
25 | // the public data, event HLS disable, others can use it.
26 | // 0 = 5.5 kHz = 5512 Hz
27 | // 1 = 11 kHz = 11025 Hz
28 | // 2 = 22 kHz = 22050 Hz
29 | // 3 = 44 kHz = 44100 Hz
30 | var flvSampleRates = []int{5512, 11025, 22050, 44100}
31 |
32 | // the sample rates in the codec,
33 | // in the sequence header.
34 | var aacSampleRates = []int{
35 | 96000, 88200, 64000, 48000,
36 | 44100, 32000, 24000, 22050,
37 | 16000, 12000, 11025, 8000,
38 | 7350, 0, 0, 0}
39 |
40 | // @see: ngx_rtmp_hls_audio
41 | // We assume here AAC frame size is 1024
42 | // Need to handle AAC frames with frame size of 960 */
43 | const hlsAacSampleSize = 1024
44 |
45 | // max PES packets size to flush the video.
46 | const hlsAudioCacheSize = 1024 * 1024
47 |
48 | // @see: NGX_RTMP_HLS_DELAY,
49 | // 63000: 700ms, ts_tbn=90000
50 | // 72000: 800ms, ts_tbn=90000
51 | const hlsAutoDelay = 72000
52 |
53 | // drop the segment when duration of ts too small.
54 | const hlsSegmentMinDurationMs = 100
55 |
56 | // in ms, for HLS aac flush the audio
57 | const hlsAacDelay = 100
58 |
--------------------------------------------------------------------------------
/hls/hls_file_write.go:
--------------------------------------------------------------------------------
1 | package hls
2 |
3 | import (
4 | "log"
5 | "os"
6 |
7 | "github.com/calabashdad/utiltools"
8 | )
9 |
10 | // write file
11 | type fileWriter struct {
12 | file string
13 | f *os.File
14 | }
15 |
16 | func newFileWriter() *fileWriter {
17 | return &fileWriter{}
18 | }
19 |
20 | func (fw *fileWriter) open(file string) (err error) {
21 | defer func() {
22 | if err := recover(); err != nil {
23 | log.Println(utiltools.PanicTrace())
24 | }
25 | }()
26 |
27 | if nil != fw.f {
28 | // already opened
29 | return
30 | }
31 |
32 | fw.f, err = os.OpenFile(file, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
33 | if err != nil {
34 | log.Println("open file error, file=", file)
35 | return
36 | }
37 |
38 | fw.file = file
39 |
40 | return
41 | }
42 |
43 | func (fw *fileWriter) close() {
44 | defer func() {
45 | if err := recover(); err != nil {
46 | log.Println(utiltools.PanicTrace())
47 | }
48 | }()
49 |
50 | if nil == fw.f {
51 | return
52 | }
53 |
54 | fw.f.Close()
55 |
56 | // after close, rest the file write to nil
57 | fw.f = nil
58 |
59 | }
60 |
61 | func (fw *fileWriter) isOpen() bool {
62 | return nil == fw.f
63 | }
64 |
65 | // write data to file
66 | func (fw *fileWriter) write(buf []byte) (err error) {
67 | defer func() {
68 | if err := recover(); err != nil {
69 | log.Println(utiltools.PanicTrace())
70 | }
71 | }()
72 |
73 | if _, err = fw.f.Write(buf); err != nil {
74 | log.Println("write to file failed, file=", fw.file, ",err=", err)
75 | return
76 | }
77 |
78 | return
79 | }
80 |
--------------------------------------------------------------------------------
/hls/hls_mpegts_frame.go:
--------------------------------------------------------------------------------
1 | package hls
2 |
3 | type mpegTsFrame struct {
4 | pts int64
5 | dts int64
6 | pid int
7 | sid int
8 | cc int
9 | key bool
10 | }
11 |
12 | func newMpegTsFrame() *mpegTsFrame {
13 | return &mpegTsFrame{}
14 | }
15 |
--------------------------------------------------------------------------------
/hls/hls_mpegts_writer.go:
--------------------------------------------------------------------------------
1 | package hls
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/calabashdad/utiltools"
7 | )
8 |
9 | // @see: ngx_rtmp_mpegts_header
10 | var mpegtsHeader = []uint8{
11 | /* TS */
12 | 0x47, 0x40, 0x00, 0x10, 0x00,
13 |
14 | /* PSI */
15 | 0x00, 0xb0, 0x0d, 0x00, 0x01, 0xc1, 0x00, 0x00,
16 |
17 | /* PAT */
18 | 0x00, 0x01, 0xf0, 0x01,
19 |
20 | /* CRC */
21 | 0x2e, 0x70, 0x19, 0x05,
22 |
23 | /* stuffing 167 bytes */
24 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
25 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
26 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
27 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
28 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
29 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
30 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
31 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
32 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
33 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
34 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
35 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
36 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
37 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
38 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
39 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
40 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
41 |
42 | /* TS */
43 | 0x47, 0x50, 0x01, 0x10, 0x00,
44 |
45 | /* PSI */
46 | 0x02, 0xb0, 0x17, 0x00, 0x01, 0xc1, 0x00, 0x00,
47 |
48 | /* PMT */
49 | 0xe1, 0x00,
50 | 0xf0, 0x00,
51 | 0x1b, 0xe1, 0x00, 0xf0, 0x00, /* h264, pid=0x100=256 */
52 | 0x0f, 0xe1, 0x01, 0xf0, 0x00, /* aac, pid=0x101=257 */
53 |
54 | /*0x03, 0xe1, 0x01, 0xf0, 0x00,*/ /* mp3 */
55 | /* CRC */
56 | 0x2f, 0x44, 0xb9, 0x9b, /* crc for aac */
57 | /*0x4e, 0x59, 0x3d, 0x1e,*/ /* crc for mp3 */
58 |
59 | /* stuffing 157 bytes */
60 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
61 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
62 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
63 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
64 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
65 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
66 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
67 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
68 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
69 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
70 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
71 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
72 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
73 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
74 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
75 | 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
76 | }
77 |
78 | func mpegtsWriteHeader(writer *fileWriter) (err error) {
79 | defer func() {
80 | if err := recover(); err != nil {
81 | log.Println(utiltools.PanicTrace())
82 | }
83 | }()
84 |
85 | if err = writer.write(mpegtsHeader); err != nil {
86 | log.Println("write ts file header failed, err=", err)
87 | return
88 | }
89 |
90 | return
91 | }
92 |
93 | func mpegtsWriteFrame(writer *fileWriter, frame *mpegTsFrame, buffer []byte) (err error) {
94 | defer func() {
95 | if err := recover(); err != nil {
96 | log.Println(utiltools.PanicTrace())
97 | }
98 | }()
99 |
100 | if nil == buffer || len(buffer) <= 0 {
101 | return
102 | }
103 |
104 | last := len(buffer)
105 | pos := 0
106 |
107 | first := true
108 | pkt := [188]byte{}
109 |
110 | for {
111 | if pos >= last {
112 | break
113 | }
114 |
115 | // position of pkt
116 | p := 0
117 |
118 | frame.cc++
119 |
120 | // sync_byte; //8bits
121 | pkt[p] = 0x47
122 | p++
123 |
124 | // pid; //13bits
125 | pkt[p] = byte((frame.pid >> 8) & 0x1f)
126 | p++
127 |
128 | // payload_unit_start_indicator; //1bit
129 | if first {
130 | pkt[p-1] |= 0x40
131 | }
132 |
133 | pkt[p] = byte(frame.pid)
134 | p++
135 |
136 | // transport_scrambling_control; //2bits
137 | // adaption_field_control; //2bits, 0x01: PayloadOnly
138 | // continuity_counter; //4bits
139 | pkt[p] = byte(0x10 | (frame.cc & 0x0f))
140 | p++
141 |
142 | if first {
143 | first = false
144 | if frame.key {
145 | pkt[p-1] |= 0x20 // Both Adaption and Payload
146 |
147 | pkt[p] = 7 //size
148 | p++
149 |
150 | pkt[p] = 0x50 // random access + PCR
151 | p++
152 |
153 | writePcr(&pkt, &p, frame.dts)
154 | }
155 |
156 | // PES header
157 | // packet_start_code_prefix; //24bits, '00 00 01'
158 | pkt[p] = 0x00
159 | p++
160 | pkt[p] = 0x00
161 | p++
162 | pkt[p] = 0x01
163 | p++
164 |
165 | //8bits
166 | pkt[p] = byte(frame.sid)
167 | p++
168 |
169 | // pts(33bits) need 5bytes.
170 | var headerSize uint8 = 5
171 | var flags uint8 = 0x80 // pts
172 |
173 | // dts(33bits) need 5bytes also
174 | if frame.dts != frame.pts {
175 | headerSize += 5
176 | flags |= 0x40 // dts
177 | }
178 |
179 | // 3bytes: flag fields from PES_packet_length to PES_header_data_length
180 | pesSize := (last - pos) + int(headerSize) + 3
181 | if pesSize > 0xffff {
182 | // when actual packet length > 0xffff(65535),
183 | // which exceed the max u_int16_t packet length,
184 | // use 0 packet length, the next unit start indicates the end of packet.
185 | pesSize = 0
186 | }
187 |
188 | // PES_packet_length; //16bits
189 | pkt[p] = byte(pesSize >> 8)
190 | p++
191 | pkt[p] = byte(pesSize)
192 | p++
193 |
194 | // PES_scrambling_control; //2bits, '10'
195 | // PES_priority; //1bit
196 | // data_alignment_indicator; //1bit
197 | // copyright; //1bit
198 | // original_or_copy; //1bit
199 | pkt[p] = 0x80 /* H222 */
200 | p++
201 |
202 | // PTS_DTS_flags; //2bits
203 | // ESCR_flag; //1bit
204 | // ES_rate_flag; //1bit
205 | // DSM_trick_mode_flag; //1bit
206 | // additional_copy_info_flag; //1bit
207 | // PES_CRC_flag; //1bit
208 | // PES_extension_flag; //1bit
209 | pkt[p] = flags
210 | p++
211 |
212 | // PES_header_data_length; //8bits
213 | pkt[p] = headerSize
214 | p++
215 |
216 | // pts; // 33bits
217 | // p = write_pts(p, flags >> 6, frame->pts + SRS_AUTO_HLS_DELAY);
218 | writePts(&pkt, &p, flags>>6, frame.pts+hlsAutoDelay)
219 |
220 | // dts; // 33bits
221 | if frame.dts != frame.pts {
222 | writePts(&pkt, &p, 1, frame.dts+hlsAutoDelay)
223 | }
224 | } // end of first
225 |
226 | bodySize := 188 - p
227 | inSize := last - pos
228 |
229 | if bodySize <= inSize {
230 | copy(pkt[p:], buffer[pos:pos+bodySize])
231 | pos += bodySize
232 | } else {
233 | fillStuff(&pkt, &p, bodySize, inSize)
234 | copy(pkt[p:], buffer[pos:pos+inSize])
235 | pos = last
236 | }
237 |
238 | // write ts packet
239 | if err = writer.write(pkt[:]); err != nil {
240 | log.Println("write ts file failed, err=", err)
241 | return
242 | }
243 | }
244 |
245 | return
246 | }
247 |
248 | func writePcr(pkt *[188]byte, pos *int, pcr int64) {
249 |
250 | v := pcr
251 |
252 | pkt[*pos] = byte(v >> 25)
253 | *pos++
254 |
255 | pkt[*pos] = byte(v >> 17)
256 | *pos++
257 |
258 | pkt[*pos] = byte(v >> 9)
259 | *pos++
260 |
261 | pkt[*pos] = byte(v >> 1)
262 | *pos++
263 |
264 | pkt[*pos] = byte(v<<7 | 0x7e)
265 | *pos++
266 |
267 | pkt[*pos] = 0
268 | *pos++
269 | }
270 |
271 | func writePts(pkt *[188]byte, pos *int, fb uint8, pts int64) {
272 | val := 0
273 |
274 | val = int(int(fb)<<4 | int(((pts>>30)&0x07)<<1) | 1)
275 |
276 | pkt[*pos] = byte(val)
277 | *pos++
278 |
279 | val = ((int(pts>>15) & 0x7fff) << 1) | 1
280 | pkt[*pos] = byte(val >> 8)
281 | *pos++
282 | pkt[*pos] = byte(val)
283 | *pos++
284 |
285 | val = ((int(pts) & 0x7fff) << 1) | 1
286 | pkt[*pos] = byte(val >> 8)
287 | *pos++
288 | pkt[*pos] = byte(val)
289 | *pos++
290 |
291 | }
292 |
293 | func fillStuff(pkt *[188]byte, pos *int, bodySize int, inSize int) {
294 |
295 | // insert the stuff bytes before PES body
296 | stuffSize := bodySize - inSize
297 |
298 | // adaption_field_control; //2bits
299 | if v := pkt[3] & 0x20; v != 0 {
300 | // has adaptation
301 | // packet[4]: adaption_field_length
302 | // packet[5]: adaption field data
303 | // base: start of PES body
304 |
305 | base := 5 + int(pkt[4])
306 |
307 | len := *pos - base
308 | copy(pkt[base+stuffSize:], pkt[base:base+len])
309 | // increase the adaption field size.
310 | pkt[4] += byte(stuffSize)
311 |
312 | *pos = base + stuffSize + len
313 |
314 | return
315 | }
316 |
317 | // create adaption field.
318 | // adaption_field_control; //2bits
319 | pkt[3] |= 0x20
320 | // base: start of PES body
321 | base := 4
322 | len := *pos - base
323 | copy(pkt[base+stuffSize:], pkt[base:base+len])
324 | *pos = base + stuffSize + len
325 |
326 | // adaption_field_length; //8bits
327 | pkt[4] = byte(stuffSize - 1)
328 | if stuffSize >= 2 {
329 | // adaption field flags.
330 | pkt[5] = 0
331 |
332 | // adaption data.
333 | if stuffSize > 2 {
334 | utiltools.MemsetByte(pkt[6:6+stuffSize-2], 0xff)
335 | }
336 | }
337 | }
338 |
--------------------------------------------------------------------------------
/hls/hls_muxer.go:
--------------------------------------------------------------------------------
1 | package hls
2 |
3 | import (
4 | "log"
5 | "os"
6 | "strconv"
7 | "syscall"
8 |
9 | "github.com/calabashdad/utiltools"
10 | )
11 |
12 | // hlsMuxer the HLS stream(m3u8 and ts files).
13 | // generally, the m3u8 muxer only provides methods to open/close segments,
14 | // to flush video/audio, without any mechenisms.
15 | //
16 | // that is, user must use HlsCache, which will control the methods of muxer,
17 | // and provides HLS mechenisms.
18 | type hlsMuxer struct {
19 | app string
20 | stream string
21 |
22 | hlsPath string
23 | hlsFragment int
24 | hlsWindow int
25 |
26 | sequenceNo int
27 | m3u8 string
28 |
29 | // m3u8 segments
30 | segments []*hlsSegment
31 |
32 | //current segment
33 | current *hlsSegment
34 | }
35 |
36 | func newHlsMuxer() *hlsMuxer {
37 | return &hlsMuxer{}
38 | }
39 |
40 | func (hm *hlsMuxer) getSequenceNo() int {
41 | return hm.sequenceNo
42 | }
43 |
44 | func (hm *hlsMuxer) updateConfig(app string, stream string, path string, fragment int, window int) (err error) {
45 |
46 | hm.app = app
47 | hm.stream = stream
48 | hm.hlsPath = path
49 | hm.hlsFragment = fragment
50 | hm.hlsWindow = window
51 |
52 | return
53 | }
54 |
55 | // open a new segment, a new ts file
56 | // segmentStartDts use to calc the segment duration, use 0 for the first segment of hls
57 | func (hm *hlsMuxer) segmentOpen(segmentStartDts int64) (err error) {
58 | defer func() {
59 | if err := recover(); err != nil {
60 | log.Println(utiltools.PanicTrace())
61 | }
62 | }()
63 |
64 | if nil != hm.current {
65 | // has already opened, ignore segment open
66 | return
67 | }
68 |
69 | // create dir for app
70 | if err = hm.createDir(); err != nil {
71 | log.Println("create dir faile,err=", err)
72 | return
73 | }
74 |
75 | // new segment
76 | hm.current = newHlsSegment()
77 | hm.current.sequenceNo = hm.sequenceNo
78 | hm.sequenceNo++
79 | hm.current.segmentStartDts = segmentStartDts
80 |
81 | // generate filename
82 | filename := hm.stream + "-" + strconv.Itoa(hm.current.sequenceNo) + ".ts"
83 |
84 | hm.current.fullPath = hm.hlsPath + "/" + hm.app + "/" + filename
85 | hm.current.uri = filename
86 |
87 | tmpFile := hm.current.fullPath + ".tmp"
88 | if err = hm.current.muxer.open(tmpFile); err != nil {
89 | log.Println("open hls muxer failed, err=", err)
90 | return
91 | }
92 |
93 | return
94 | }
95 |
96 | func (hm *hlsMuxer) onSequenceHeader() (err error) {
97 | defer func() {
98 | if err := recover(); err != nil {
99 | log.Println(utiltools.PanicTrace())
100 | }
101 | }()
102 |
103 | // set the current segment to sequence header,
104 | // when close the segement, it will write a discontinuity to m3u8 file.
105 | hm.current.isSequenceHeader = true
106 |
107 | return
108 | }
109 |
110 | // whether segment overflow,
111 | // that is whether the current segment duration>=(the segment in config)
112 | func (hm *hlsMuxer) isSegmentOverflow() bool {
113 | defer func() {
114 | if err := recover(); err != nil {
115 | log.Println(utiltools.PanicTrace())
116 | }
117 | }()
118 |
119 | return hm.current.duration >= float64(hm.hlsFragment)
120 | }
121 |
122 | // whether segment absolutely overflow, for pure audio to reap segment,
123 | // that is whether the current segment duration>=2*(the segment in config)
124 | func (hm *hlsMuxer) isSegmentAbsolutelyOverflow() bool {
125 | defer func() {
126 | if err := recover(); err != nil {
127 | log.Println(utiltools.PanicTrace())
128 | }
129 | }()
130 |
131 | if nil == hm.current {
132 | log.Println("current is null is impossible, there must be a mistake")
133 | return true
134 | }
135 |
136 | res := hm.current.duration >= float64(2*hm.hlsFragment)
137 |
138 | return res
139 | }
140 |
141 | func (hm *hlsMuxer) flushAudio(af *mpegTsFrame, ab *[]byte) (err error) {
142 | defer func() {
143 | if err := recover(); err != nil {
144 | log.Println(utiltools.PanicTrace())
145 | }
146 | }()
147 |
148 | // if current is NULL, segment is not open, ignore the flush event.
149 | if nil == hm.current {
150 | log.Println("hls segment is not open, ignore the flush event.")
151 | return
152 | }
153 |
154 | if len(*ab) <= 0 {
155 | return
156 | }
157 |
158 | hm.current.updateDuration(af.pts)
159 |
160 | if err = hm.current.muxer.writeAudio(af, *ab); err != nil {
161 | log.Println("current muxer write audio faile, err=", err)
162 | return
163 | }
164 |
165 | // write success, clear the buffer
166 | *ab = nil
167 |
168 | return
169 | }
170 |
171 | func (hm *hlsMuxer) flushVideo(af *mpegTsFrame, ab []byte, vf *mpegTsFrame, vb *[]byte) (err error) {
172 | defer func() {
173 | if err := recover(); err != nil {
174 | log.Println(utiltools.PanicTrace())
175 | }
176 | }()
177 |
178 | if nil == hm.current {
179 | return
180 | }
181 |
182 | // update the duration of segment.
183 | hm.current.updateDuration(vf.dts)
184 |
185 | if err = hm.current.muxer.writeVideo(vf, vb); err != nil {
186 | return
187 | }
188 |
189 | *vb = nil
190 |
191 | return
192 | }
193 |
194 | // close segment(ts)
195 | // logDesc is the description for log
196 | func (hm *hlsMuxer) segmentClose(logDesc string) (err error) {
197 | defer func() {
198 | if err := recover(); err != nil {
199 | log.Println(utiltools.PanicTrace())
200 | }
201 | }()
202 |
203 | if nil == hm.current {
204 | log.Println("ignore the segment close, for segment is not open.")
205 | return
206 | }
207 |
208 | // valid, add to segments if segment duration is ok
209 | if hm.current.duration*1000 >= hlsSegmentMinDurationMs {
210 | hm.segments = append(hm.segments, hm.current)
211 |
212 | // close the muxer of finished segment
213 | fullPath := hm.current.fullPath
214 | hm.current = nil
215 |
216 | // rename from tmp to real path
217 | tmpFile := fullPath + ".tmp"
218 | if err = os.Rename(tmpFile, fullPath); err != nil {
219 | log.Println("rename file failed, err=", err)
220 | return
221 | }
222 | } else {
223 | // reuse current segment index
224 | hm.sequenceNo--
225 |
226 | // rename from tmp to real path
227 | tmpFile := hm.current.fullPath + ".tmp"
228 | if err = syscall.Unlink(tmpFile); err != nil {
229 | log.Println("syscall unlink tmpfile=", tmpFile, " failed, err=", err)
230 | }
231 | }
232 |
233 | // the segment to remove
234 | var segmentToRemove []*hlsSegment
235 |
236 | // shrink the segments
237 | var duration float64
238 | removeIndex := -1
239 | for i := len(hm.segments) - 1; i >= 0; i-- {
240 | seg := hm.segments[i]
241 | duration += seg.duration
242 |
243 | if int(duration) > hm.hlsWindow {
244 | removeIndex = i
245 | break
246 | }
247 | }
248 |
249 | for i := 0; i < removeIndex && len(hm.segments) > 0; i++ {
250 | segmentToRemove = append(segmentToRemove, hm.segments[i])
251 | }
252 |
253 | // refresh the m3u8, do not contains the removed ts
254 | if err = hm.refreshM3u8(); err != nil {
255 | log.Println("refresh m3u8 failed, err=", err)
256 | }
257 |
258 | // remove the ts file
259 | for i := 0; i < len(segmentToRemove); i++ {
260 | s := segmentToRemove[i]
261 | syscall.Unlink(s.fullPath)
262 | }
263 | segmentToRemove = nil
264 |
265 | return
266 | }
267 |
268 | func (hm *hlsMuxer) refreshM3u8() (err error) {
269 | defer func() {
270 | if err := recover(); err != nil {
271 | log.Println(utiltools.PanicTrace())
272 | }
273 | }()
274 |
275 | m3u8File := hm.hlsPath
276 | m3u8File += "/"
277 | m3u8File += hm.app
278 | m3u8File += "/"
279 | m3u8File += hm.stream
280 | m3u8File += ".m3u8"
281 |
282 | hm.m3u8 = m3u8File
283 | m3u8File += ".temp"
284 |
285 | var f *os.File
286 | if f, err = hm._refreshM3u8(m3u8File); err != nil {
287 | log.Println("refresh m3u8 file faile, err=", err)
288 | return
289 | }
290 | if nil != f {
291 | f.Close()
292 | if err = os.Rename(m3u8File, hm.m3u8); err != nil {
293 | log.Println("rename m3u8 file failed, old file=", m3u8File, ",new file=", hm.m3u8)
294 | return
295 | }
296 | }
297 |
298 | syscall.Unlink(m3u8File)
299 |
300 | return
301 | }
302 |
303 | func (hm *hlsMuxer) _refreshM3u8(m3u8File string) (f *os.File, err error) {
304 | defer func() {
305 | if err := recover(); err != nil {
306 | log.Println(utiltools.PanicTrace())
307 | }
308 | }()
309 |
310 | // no segments, return
311 | if 0 == len(hm.segments) {
312 | return
313 | }
314 |
315 | f, err = os.OpenFile(m3u8File, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
316 | if err != nil {
317 | log.Println("_refreshM3u8: open file error, file=", m3u8File)
318 | return
319 | }
320 |
321 | // #EXTM3U\n#EXT-X-VERSION:3\n
322 | header := []byte{
323 | // #EXTM3U\n
324 | 0x23, 0x45, 0x58, 0x54, 0x4d, 0x33, 0x55, 0xa,
325 | // #EXT-X-VERSION:3\n
326 | 0x23, 0x45, 0x58, 0x54, 0x2d, 0x58, 0x2d, 0x56, 0x45, 0x52,
327 | 0x53, 0x49, 0x4f, 0x4e, 0x3a, 0x33, 0xa,
328 | // #EXT-X-ALLOW-CACHE:NO
329 | 0x23, 0x45, 0x58, 0x54, 0x2d, 0x58, 0x2d, 0x41, 0x4c, 0x4c,
330 | 0x4f, 0x57, 0x2d, 0x43, 0x41, 0x43, 0x48, 0x45, 0x3a, 0x4e, 0x4f, 0x0a,
331 | }
332 |
333 | if _, err = f.Write(header); err != nil {
334 | log.Println("write m3u8 header failed, err=", err)
335 | return
336 | }
337 |
338 | targetDuration := 0
339 | for i := 0; i < len(hm.segments); i++ {
340 | if int(hm.segments[i].duration) > targetDuration {
341 | targetDuration = int(hm.segments[i].duration)
342 | }
343 | }
344 |
345 | targetDuration++
346 | var duration string
347 | duration = "#EXT-X-TARGETDURATION:" + strconv.Itoa(targetDuration) + "\n"
348 | if _, err = f.Write([]byte(duration)); err != nil {
349 | log.Println("write m3u8 duration failed, err=", err)
350 | return
351 | }
352 |
353 | // write all segments
354 | for i := 0; i < len(hm.segments); i++ {
355 | s := hm.segments[i]
356 |
357 | if s.isSequenceHeader {
358 | // #EXT-X-DISCONTINUITY\n
359 | extDiscon := "#EXT-X-DISCONTINUITY\n"
360 | if _, err = f.Write([]byte(extDiscon)); err != nil {
361 | log.Println("write m3u8 segment discontinuity failed, err=", err)
362 | return
363 | }
364 | }
365 |
366 | // "#EXTINF:4294967295.208,\n"
367 | extInfo := "#EXTINF:" + strconv.FormatFloat(s.duration, 'f', 3, 64) + "\n"
368 | if _, err = f.Write([]byte(extInfo)); err != nil {
369 | log.Println("write m3u8 segment info failed, err=", err)
370 | return
371 | }
372 |
373 | // file name
374 | filename := s.uri + "\n"
375 | if _, err = f.Write([]byte(filename)); err != nil {
376 | log.Println("write m3u8 uri failed, err=", err)
377 | return
378 | }
379 |
380 | }
381 |
382 | return
383 | }
384 |
385 | func (hm *hlsMuxer) createDir() (err error) {
386 | defer func() {
387 | if err := recover(); err != nil {
388 | log.Println(utiltools.PanicTrace())
389 | }
390 | }()
391 |
392 | appDir := hm.hlsPath
393 | appDir += "/"
394 | appDir += hm.app
395 |
396 | _, errLocal := os.Stat(appDir)
397 | if os.IsNotExist(errLocal) {
398 | if err = os.Mkdir(appDir, os.ModePerm); err != nil {
399 | return
400 | }
401 | }
402 |
403 | return
404 | }
405 |
--------------------------------------------------------------------------------
/hls/hls_segment.go:
--------------------------------------------------------------------------------
1 | package hls
2 |
3 | // the wrapper of m3u8 segment from specification:
4 | // 3.3.2. EXTINF
5 | // The EXTINF tag specifies the duration of a media segment.
6 | type hlsSegment struct {
7 | // duration in seconds in m3u8.
8 | duration float64
9 | // sequence number in m3u8.
10 | sequenceNo int
11 | // ts uri in m3u8.
12 | uri string
13 | // ts full file to write.
14 | fullPath string
15 | // the muxer to write ts.
16 | muxer *tsMuxer
17 | // current segment start dts for m3u8
18 | segmentStartDts int64
19 | // whether current segement is sequence header.
20 | isSequenceHeader bool
21 | }
22 |
23 | func newHlsSegment() *hlsSegment {
24 | return &hlsSegment{
25 | muxer: newTsMuxer(),
26 | }
27 | }
28 |
29 | func (hs *hlsSegment) updateDuration(currentFrameDts int64) {
30 |
31 | // we use video/audio to update segment duration,
32 | // so when reap segment, some previous audio frame will
33 | // update the segment duration, which is nagetive,
34 | // just ignore it.
35 | if currentFrameDts < hs.segmentStartDts {
36 | return
37 | }
38 |
39 | hs.duration = float64(currentFrameDts-hs.segmentStartDts) / 90000.0
40 | }
41 |
--------------------------------------------------------------------------------
/hls/hls_ts_muxer.go:
--------------------------------------------------------------------------------
1 | package hls
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/calabashdad/utiltools"
7 | )
8 |
9 | // write data from frame(header info) and buffer(data) to ts file.
10 | type tsMuxer struct {
11 | writer *fileWriter
12 | path string
13 | }
14 |
15 | func newTsMuxer() *tsMuxer {
16 | return &tsMuxer{
17 | writer: newFileWriter(),
18 | }
19 | }
20 |
21 | func (tm *tsMuxer) open(path string) (err error) {
22 | defer func() {
23 | if err := recover(); err != nil {
24 | log.Println(utiltools.PanicTrace())
25 | }
26 | }()
27 |
28 | tm.path = path
29 |
30 | tm.close()
31 |
32 | if err = tm.writer.open(tm.path); err != nil {
33 | log.Println("opem ts muxer path failed, err=", err)
34 | return
35 | }
36 |
37 | // write mpegts header
38 | if err = mpegtsWriteHeader(tm.writer); err != nil {
39 | log.Println("write mpegts header failed, err=", err)
40 | return
41 | }
42 |
43 | return
44 | }
45 |
46 | func (tm *tsMuxer) writeAudio(af *mpegTsFrame, ab []byte) (err error) {
47 | defer func() {
48 | if err := recover(); err != nil {
49 | log.Println(utiltools.PanicTrace())
50 | }
51 | }()
52 |
53 | if err = mpegtsWriteFrame(tm.writer, af, ab); err != nil {
54 | log.Println("mpegts write frame faile, err=", err)
55 | return
56 | }
57 |
58 | return
59 | }
60 |
61 | func (tm *tsMuxer) writeVideo(vf *mpegTsFrame, vb *[]byte) (err error) {
62 | defer func() {
63 | if err := recover(); err != nil {
64 | log.Println(utiltools.PanicTrace())
65 | }
66 | }()
67 |
68 | if err = mpegtsWriteFrame(tm.writer, vf, *vb); err != nil {
69 | return
70 | }
71 |
72 | return
73 | }
74 |
75 | func (tm *tsMuxer) close() (err error) {
76 | defer func() {
77 | if err := recover(); err != nil {
78 | log.Println(utiltools.PanicTrace())
79 | }
80 | }()
81 |
82 | tm.writer.close()
83 |
84 | return
85 | }
86 |
--------------------------------------------------------------------------------
/hls_server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/binary"
5 | "github.com/calabashdad/utiltools"
6 | "io/ioutil"
7 | "log"
8 | "net/http"
9 | "os"
10 | "path"
11 | "seal/conf"
12 | "seal/rtmp/co"
13 | "strconv"
14 | "strings"
15 | "time"
16 | )
17 |
18 | type hlsServer struct {
19 | }
20 |
21 | func (hs *hlsServer) Start() {
22 | defer func() {
23 | if err := recover(); err != nil {
24 | log.Println(utiltools.PanicTrace())
25 | }
26 |
27 | gGuards.Done()
28 | }()
29 |
30 | if "false" == conf.GlobalConfInfo.Hls.Enable {
31 | log.Println("hls server disabled")
32 | return
33 | }
34 | log.Println("start hls server, listen at :", conf.GlobalConfInfo.Hls.HttpListen)
35 |
36 | http.HandleFunc("/live/", handleLive)
37 |
38 | if err := http.ListenAndServe(":"+conf.GlobalConfInfo.Hls.HttpListen, nil); err != nil {
39 | log.Println("start hls server failed, err=", err)
40 | }
41 | }
42 |
43 | var crossdomainxml = []byte(
44 | `
45 |
46 |
47 | `)
48 |
49 | func handleLive(w http.ResponseWriter, r *http.Request) {
50 | defer func() {
51 | if err := recover(); err != nil {
52 | log.Println(utiltools.PanicTrace())
53 | }
54 | }()
55 |
56 | if path.Base(r.URL.Path) == "crossdomain.xml" {
57 |
58 | w.Header().Set("Content-Type", "application/xml")
59 | w.Write(crossdomainxml)
60 | return
61 | }
62 |
63 | ext := path.Ext(r.URL.Path)
64 | switch ext {
65 | case ".m3u8":
66 | app, m3u8 := parseM3u8File(r.URL.Path)
67 | m3u8 = conf.GlobalConfInfo.Hls.HlsPath + "/" + app + "/" + m3u8
68 | if data, err := loadFile(m3u8); nil == err {
69 | w.Header().Set("Access-Control-Allow-Origin", "*")
70 | w.Header().Set("Cache-Control", "no-cache")
71 | w.Header().Set("Content-Type", "application/x-mpegURL")
72 | w.Header().Set("Content-Length", strconv.Itoa(len(data)))
73 | if _, err = w.Write(data); err != nil {
74 | log.Println("write m3u8 file err=", err)
75 | }
76 | }
77 | case ".ts":
78 | app, ts := parseTsFile(r.URL.Path)
79 | ts = conf.GlobalConfInfo.Hls.HlsPath + "/" + app + "/" + ts
80 | if data, err := loadFile(ts); nil == err {
81 | w.Header().Set("Access-Control-Allow-Origin", "*")
82 | w.Header().Set("Content-Type", "video/mp2ts")
83 | w.Header().Set("Content-Length", strconv.Itoa(len(data)))
84 | if _, err = w.Write(data); err != nil {
85 | log.Println("write ts file err=", err)
86 | }
87 | }
88 | case ".flv":
89 | u := r.URL.Path
90 |
91 | path := strings.TrimSuffix(strings.TrimLeft(u, "/"), ".flv")
92 | paths := strings.SplitN(path, "/", 2)
93 |
94 | if len(paths) != 2 {
95 | http.Error(w, "http-flv path error, should be /live/stream.flv", http.StatusBadRequest)
96 | return
97 | }
98 | log.Println("url:", u, "path:", path, "paths:", paths)
99 |
100 | key := paths[0] + "/" + paths[1]
101 | w.Header().Set("Access-Control-Allow-Origin", "*")
102 |
103 | log.Println("http flv request, remote=", r.RemoteAddr)
104 |
105 | httpFlvStreamCycle(key, r.RemoteAddr, w)
106 | default:
107 | log.Println("unknown hls request file, type=", ext)
108 | }
109 | }
110 |
111 | func parseM3u8File(p string) (app string, m3u8File string) {
112 | if i := strings.Index(p, "/"); i >= 0 {
113 | if j := strings.LastIndex(p, "/"); j > 0 {
114 | app = p[i+1 : j]
115 | }
116 | }
117 |
118 | if i := strings.LastIndex(p, "/"); i > 0 {
119 | m3u8File = p[i+1:]
120 | }
121 |
122 | return
123 | }
124 |
125 | func parseTsFile(p string) (app string, tsFile string) {
126 | if i := strings.Index(p, "/"); i >= 0 {
127 | if j := strings.LastIndex(p, "/"); j > 0 {
128 | app = p[i+1 : j]
129 | }
130 | }
131 |
132 | if i := strings.LastIndex(p, "/"); i > 0 {
133 | tsFile = p[i+1:]
134 | }
135 |
136 | return
137 | }
138 |
139 | func loadFile(filename string) (data []byte, err error) {
140 | defer func() {
141 | if err := recover(); err != nil {
142 | log.Println(utiltools.PanicTrace())
143 | }
144 | }()
145 |
146 | var f *os.File
147 | if f, err = os.Open(filename); err != nil {
148 | log.Println("Open file ", filename, " failed, err is", err)
149 | return
150 | }
151 | defer f.Close()
152 |
153 | if data, err = ioutil.ReadAll(f); err != nil {
154 | log.Println("read file ", filename, " failed, err is", err)
155 | return
156 | }
157 |
158 | return
159 | }
160 |
161 | func httpFlvStreamCycle(key string, addr string, w http.ResponseWriter) {
162 | defer func() {
163 | if err := recover(); err != nil {
164 | log.Println(utiltools.PanicTrace())
165 | }
166 | }()
167 |
168 | var err error
169 |
170 | source := co.GlobalSources.FindSourceToPlay(key)
171 | if nil == source {
172 | log.Printf("httpFlvStreamCycle, stream=%s can not play because has not published\n", key)
173 | http.Error(w, "this stream has not published", http.StatusBadRequest)
174 | return
175 | }
176 |
177 | consumer := co.NewConsumer("http-flv/" + key)
178 | source.CreateConsumer(consumer)
179 |
180 | if source.Atc && !source.GopCache.Empty() {
181 | if nil != source.CacheMetaData {
182 | source.CacheMetaData.Header.Timestamp = source.GopCache.StartTime()
183 | }
184 | if nil != source.CacheVideoSequenceHeader {
185 | source.CacheVideoSequenceHeader.Header.Timestamp = source.GopCache.StartTime()
186 | }
187 | if nil != source.CacheAudioSequenceHeader {
188 | source.CacheAudioSequenceHeader.Header.Timestamp = source.GopCache.StartTime()
189 | }
190 | }
191 |
192 | //cache meta data
193 | if nil != source.CacheMetaData {
194 | consumer.Enquene(source.CacheMetaData, source.Atc, source.SampleRate, source.FrameRate, source.TimeJitter)
195 | log.Printf("http-flv,key=%s, cache metadata, msg time=%d, payload size=%d\n", key, source.CacheMetaData.Header.Timestamp, source.CacheMetaData.Header.PayloadLength)
196 | }
197 |
198 | //cache video data
199 | if nil != source.CacheVideoSequenceHeader {
200 | consumer.Enquene(source.CacheVideoSequenceHeader, source.Atc, source.SampleRate, source.FrameRate, source.TimeJitter)
201 | log.Printf("http-flv,key=%s, cache video sequence, msg time=%d, payload size=%d\n", key, source.CacheVideoSequenceHeader.Header.Timestamp, source.CacheVideoSequenceHeader.Header.PayloadLength)
202 | }
203 |
204 | //cache audio data
205 | if nil != source.CacheAudioSequenceHeader {
206 | consumer.Enquene(source.CacheAudioSequenceHeader, source.Atc, source.SampleRate, source.FrameRate, source.TimeJitter)
207 | log.Printf("http-flv,key=%s, cache audio sequence, msg time=%d, payload size=%d\n", key, source.CacheAudioSequenceHeader.Header.Timestamp, source.CacheAudioSequenceHeader.Header.PayloadLength)
208 | }
209 |
210 | //dump gop cache to client.
211 | source.GopCache.Dump(consumer, source.Atc, source.SampleRate, source.FrameRate, source.TimeJitter)
212 |
213 | log.Printf("httpFlvStreamCycle now playing, key=%s, remote=%s", key, addr)
214 |
215 | //f, err := os.OpenFile("/Users/yangkai/go/src/seal/test.flv", os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
216 |
217 | // send flv header
218 | flvHeader := []byte{0x46, 0x4c, 0x56, 0x01, 0x05, 0x00, 0x00, 0x00, 0x09}
219 | if _, err = w.Write(flvHeader); err != nil {
220 | log.Println("httpFlvStreamCycle send flv header to remote success.")
221 | return
222 | }
223 | //f.Write(flvHeader)
224 | log.Printf("httpFlv,key=%s, send flv header to remote sucess\n", key)
225 |
226 | timeLast := time.Now().Unix()
227 |
228 | var previousTagLen uint32
229 | for {
230 | msg := consumer.Dump()
231 | if nil == msg {
232 | // wait and try again.
233 |
234 | timeCurrent := time.Now().Unix()
235 | if timeCurrent-timeLast > 30 {
236 | log.Println("httpFlvStreamCycle time out > 30, break. key=", key)
237 | break
238 | }
239 |
240 | continue
241 | } else {
242 |
243 | timeLast = time.Now().Unix()
244 |
245 | // previous tag len c4B. 11 + payload data size
246 | // type 1B
247 | // data size 3B
248 | // timestamp 3B
249 | // timestampEx 1B
250 | // streamID 3B always is 0
251 | // total is 4 + 1 +3 + 3 + 1 + 3 = 15B
252 | var tagHeader [15]uint8
253 | var offset uint32
254 |
255 | // previous tag len
256 | binary.BigEndian.PutUint32(tagHeader[offset:], previousTagLen)
257 | offset += 4
258 |
259 | // type
260 | tagHeader[offset] = msg.Header.MessageType
261 | offset++
262 |
263 | // payload data size
264 | var sizebuf [4]uint8
265 | binary.BigEndian.PutUint32(sizebuf[:], msg.Header.PayloadLength)
266 | copy(tagHeader[offset:], sizebuf[1:])
267 | offset += 3
268 |
269 | // timestamp
270 | var timebuf [4]uint8
271 | binary.BigEndian.PutUint32(timebuf[:], uint32(msg.Header.Timestamp))
272 | copy(tagHeader[offset:], timebuf[1:])
273 | offset += 3
274 |
275 | // timestamp ex, generally not used
276 | tagHeader[offset] = 0
277 | offset++
278 |
279 | // stream id
280 | tagHeader[offset] = 0
281 | offset++
282 | tagHeader[offset] = 0
283 | offset++
284 | tagHeader[offset] = 0
285 | offset++
286 |
287 | if _, err = w.Write(tagHeader[:]); err != nil {
288 | log.Println("httpFlvStreamCycle: playing... send tag header to remote failed.err=", err)
289 | break
290 | }
291 | //f.Write(tagHeader[:])
292 |
293 | if _, err = w.Write(msg.Payload.Payload); err != nil {
294 | log.Println("httpFlvStreamCycle: playing... send tag payload to remote failed.err=", err)
295 | break
296 | }
297 | //f.Write(msg.Payload.Payload)
298 |
299 | previousTagLen = 11 + msg.Header.PayloadLength
300 | }
301 | }
302 |
303 | source.DestroyConsumer(consumer)
304 | log.Printf("httpFlvStreamCycle: playing over, key=%s, consumer has destroyed\n", key)
305 |
306 | }
307 |
--------------------------------------------------------------------------------
/kernel/mem_pool.go:
--------------------------------------------------------------------------------
1 | package kernel
2 |
3 | import (
4 | "log"
5 | )
6 |
7 | // MemPool memory pool
8 | type MemPool struct {
9 | pos uint32
10 | buf []uint8
11 | }
12 |
13 | const maxPoolSize = 500 * 1024
14 |
15 | // GetMem get memory space from pool
16 | func (pool *MemPool) GetMem(size uint32) []byte {
17 |
18 | if size >= maxPoolSize {
19 | pool.buf = make([]uint8, size)
20 | pool.pos = size
21 |
22 | log.Println("warning: Gem Memory, too large, size=", size)
23 |
24 | return pool.buf
25 | }
26 |
27 | if maxPoolSize-pool.pos < size {
28 | pool.pos = 0
29 | pool.buf = make([]uint8, maxPoolSize)
30 | }
31 | b := pool.buf[pool.pos : pool.pos+size]
32 | pool.pos += size
33 | return b
34 | }
35 |
36 | // NewMemPool create a new memory pool
37 | func NewMemPool() *MemPool {
38 | return &MemPool{
39 | buf: make([]uint8, maxPoolSize),
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/kernel/tcp_socket.go:
--------------------------------------------------------------------------------
1 | package kernel
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "net"
7 | "time"
8 | )
9 |
10 | // TCPSock socket
11 | type TCPSock struct {
12 | net.Conn
13 | recvTimeOut uint32
14 | sendTimeOut uint32
15 | recvBytesSum uint64
16 | }
17 |
18 | // GetRecvBytesSum return the recv bytes of conn
19 | func (conn *TCPSock) GetRecvBytesSum() uint64 {
20 | return conn.recvBytesSum
21 | }
22 |
23 | // SetRecvTimeout set tcp socket recv timeout
24 | func (conn *TCPSock) SetRecvTimeout(timeoutUs uint32) {
25 | conn.recvTimeOut = timeoutUs
26 | }
27 |
28 | // SetSendTimeout set tcp socket send timeout
29 | func (conn *TCPSock) SetSendTimeout(timeoutUs uint32) {
30 | conn.sendTimeOut = timeoutUs
31 | }
32 |
33 | // ExpectBytesFull recv exactly size or error
34 | func (conn *TCPSock) ExpectBytesFull(buf []uint8, size uint32) (err error) {
35 |
36 | if err = conn.SetDeadline(time.Now().Add(time.Duration(conn.recvTimeOut) * time.Microsecond)); err != nil {
37 | return
38 | }
39 |
40 | var n int
41 | if n, err = io.ReadFull(conn.Conn, buf[:size]); err != nil {
42 | return
43 | }
44 |
45 | conn.recvBytesSum += uint64(n)
46 |
47 | return
48 | }
49 |
50 | // SendBytes send buf
51 | func (conn *TCPSock) SendBytes(buf []uint8) (err error) {
52 |
53 | if err = conn.SetDeadline(time.Now().Add(time.Duration(conn.sendTimeOut) * time.Microsecond)); err != nil {
54 | return
55 | }
56 |
57 | var n int
58 | if n, err = conn.Conn.Write(buf); err != nil {
59 | return
60 | }
61 |
62 | if n != len(buf) {
63 | err = fmt.Errorf("tcp sock, send bytes error, need send %d, actually send %d", len(buf), n)
64 | return
65 | }
66 |
67 | return
68 | }
69 |
--------------------------------------------------------------------------------
/rtmp/co/const.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 |
--------------------------------------------------------------------------------
/rtmp/co/consumer.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "log"
5 | "seal/conf"
6 | "seal/rtmp/pt"
7 | "time"
8 | )
9 |
10 | //Consumer is the consumer of source
11 | type Consumer struct {
12 | stream string
13 | queueSizeMills uint32
14 | avStartTime int64
15 | avEndTime int64
16 | msgQuene chan *pt.Message
17 | jitter *pt.TimeJitter
18 | paused bool
19 | duration float64
20 | }
21 |
22 | func NewConsumer(key string) *Consumer {
23 | return &Consumer{
24 | stream: key,
25 | queueSizeMills: conf.GlobalConfInfo.Rtmp.ConsumerQueueSize * 1000,
26 | avStartTime: -1,
27 | avEndTime: -1,
28 | msgQuene: make(chan *pt.Message, 4096),
29 | jitter: &pt.TimeJitter{},
30 | }
31 | }
32 |
33 | func (c *Consumer) Clean() {
34 | close(c.msgQuene)
35 | }
36 |
37 | // Atc whether Atc, donot use jitter correct if true
38 | // tba timebase of audio. used to calc the audio time delta if time-jitter detected.
39 | // tbv timebase of video. used to calc the video time delta if time-jitter detected.
40 | func (c *Consumer) Enquene(msg *pt.Message, atc bool, tba float64, tbv float64, timeJitter uint32) {
41 |
42 | if nil == msg {
43 | return
44 | }
45 |
46 | if !atc {
47 | //c.jitter.Correct(msg, tba, tbv, timeJitter)
48 | }
49 |
50 | if msg.Header.IsVideo() || msg.Header.IsAudio() {
51 | if -1 == c.avStartTime {
52 | c.avStartTime = int64(msg.Header.Timestamp)
53 | }
54 |
55 | c.avEndTime = int64(msg.Header.Timestamp)
56 | }
57 |
58 | select {
59 | // incase block, and influence others.
60 | case <-time.After(time.Duration(3) * time.Millisecond):
61 | log.Println("enquene to channel timeout, channel may be full, key=", c.stream)
62 | break
63 | case c.msgQuene <- msg:
64 | break
65 | }
66 | }
67 |
68 | func (c *Consumer) Dump() (msg *pt.Message) {
69 |
70 | if c.paused {
71 | log.Println("client paused now")
72 | return
73 | }
74 |
75 | select {
76 | case <-time.After(time.Duration(10) * time.Millisecond):
77 | // in case block
78 | return
79 | case msg = <-c.msgQuene:
80 | break
81 | }
82 |
83 | //for {
84 | // var msgLocal *pt.Message
85 | //
86 | // select {
87 | // case <-time.After(time.Duration(10) * time.Millisecond):
88 | // // in case block
89 | // return
90 | // case msgLocal = <-c.msgQuene:
91 | // break
92 | // }
93 | //
94 | // c.avStartTime = int64(msgLocal.Header.Timestamp)
95 | //
96 | // if uint32(c.avEndTime-c.avStartTime) > c.queueSizeMills {
97 | //
98 | // // for metadata, key frame, audio sequence header, we do not shrink it.
99 | // if msgLocal.Header.IsAmf0Data() ||
100 | // flv.VideoH264IsKeyframe(msgLocal.Payload.Payload) ||
101 | // flv.AudioIsSequenceHeader(msgLocal.Payload.Payload) {
102 | //
103 | // msg = msgLocal
104 | // log.Printf("key=%s dump a frame even it's timestamp is too old. msg type=%d, msg time=%d, avStatrtTime=%d, queue len=%d\n",
105 | // c.stream, msg.Header.MessageType, msgLocal.Header.Timestamp, c.avStartTime, c.queueSizeMills)
106 | //
107 | // return
108 | // } else {
109 | // // msg is too old, drop it directly, we store the latest i frame into cache already
110 | // continue
111 | // }
112 | // } else {
113 | // msg = msgLocal
114 | // return
115 | // }
116 | //
117 | //}
118 |
119 | return
120 | }
121 |
122 | func (c *Consumer) onPlayPause(isPause bool) (err error) {
123 | c.paused = isPause
124 | log.Println("consumer changed pause status to ", isPause)
125 | return
126 | }
127 |
--------------------------------------------------------------------------------
/rtmp/co/cycle.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "log"
5 | "net"
6 | "seal/conf"
7 | "seal/kernel"
8 | "seal/rtmp/pt"
9 |
10 | "github.com/calabashdad/utiltools"
11 | )
12 |
13 | type ackWindowSize struct {
14 | ackWindowSize uint32
15 | hasAckedSize uint64
16 | }
17 |
18 | type connectInfo struct {
19 | tcURL string
20 | pageURL string
21 | swfURL string
22 | app string
23 | objectEncoding float64
24 | }
25 |
26 | // RtmpConn rtmp connection info
27 | type RtmpConn struct {
28 | tcpConn *kernel.TCPSock
29 | chunkStreams map[uint32]*pt.ChunkStream //key:cs id
30 | inChunkSize uint32 //default 128, set by peer
31 | outChunkSize uint32 //default 128, set by config file.
32 | ack ackWindowSize
33 | cmdRequests map[float64]string //command requests.key: transactin id, value:command name
34 | role uint8 //publisher or player.
35 | streamName string
36 | tokenStr string //token str for authentication. it's optional.
37 | duration float64 //for player.used to specified the stop when exceed the duration.
38 | defaultStreamID float64 //default stream id for request.
39 | connInfo *connectInfo //connect info.
40 | source *SourceStream //data source info.
41 | consumer *Consumer //for consumer, like player.
42 | }
43 |
44 | // NewRtmpConnection create rtmp conncetion
45 | func NewRtmpConnection(c net.Conn) *RtmpConn {
46 | return &RtmpConn{
47 | tcpConn: &kernel.TCPSock{
48 | Conn: c,
49 | },
50 | chunkStreams: make(map[uint32]*pt.ChunkStream),
51 | inChunkSize: pt.RtmpDefalutChunkSize,
52 | outChunkSize: pt.RtmpDefalutChunkSize,
53 | ack: ackWindowSize{
54 | ackWindowSize: 250000,
55 | },
56 | cmdRequests: make(map[float64]string),
57 | role: pt.RtmpRoleUnknown,
58 | defaultStreamID: 1.0,
59 | connInfo: &connectInfo{
60 | objectEncoding: pt.RtmpSigAmf0Ver,
61 | },
62 | }
63 | }
64 |
65 | // Cycle rtmp service cycle
66 | func (rc *RtmpConn) Cycle() {
67 | defer func() {
68 | if err := recover(); err != nil {
69 | log.Println(utiltools.PanicTrace())
70 | }
71 | }()
72 |
73 | rc.tcpConn.SetRecvTimeout(conf.GlobalConfInfo.Rtmp.TimeOut * 1000 * 1000)
74 | rc.tcpConn.SetSendTimeout(conf.GlobalConfInfo.Rtmp.TimeOut * 1000 * 1000)
75 |
76 | var err error
77 |
78 | if err = rc.handShake(); err != nil {
79 | log.Println("rtmp handshake failed.err=", err)
80 | return
81 | }
82 | log.Println("rtmp handshake success.")
83 |
84 | for {
85 | // notice that the payload has not alloced at init.
86 | // one msg alloc once, and do not copy to improve performance.
87 | msg := &pt.Message{}
88 |
89 | if err = rc.recvMsg(&msg.Header, &msg.Payload); err != nil {
90 | break
91 | }
92 |
93 | if err = rc.onRecvMsg(msg); err != nil {
94 | break
95 | }
96 |
97 | }
98 |
99 | log.Println("rtmp cycle finished, begin clean.err=", err)
100 |
101 | rc.clean()
102 |
103 | log.Println("rtmp clean finished, remote=", rc.tcpConn.Conn.RemoteAddr())
104 | }
105 |
106 | func (rc *RtmpConn) getSourceKey() string {
107 | // e.g. rtmp://127.0.0.1/live/test
108 | // key is live/test
109 | return rc.connInfo.app + "/" + rc.streamName
110 | }
111 |
112 | func (rc *RtmpConn) clean() {
113 |
114 | if err := rc.tcpConn.Close(); err != nil {
115 | log.Println("close socket err=", err)
116 | }
117 |
118 | if pt.RtmpRoleFlashPublisher == rc.role || pt.RtmpRoleFMLEPublisher == rc.role {
119 | if nil != rc.source {
120 | key := rc.getSourceKey()
121 | rc.deletePublishStream(key)
122 | log.Println("delete publisher stream=", key)
123 | }
124 | }
125 |
126 | if pt.RtmpRolePlayer == rc.role {
127 | if nil != rc.source {
128 | rc.consumer.Clean()
129 | rc.source.DestroyConsumer(rc.consumer)
130 | log.Println("player clean over")
131 | }
132 | }
133 | }
134 |
135 | func (rc *RtmpConn) deletePublishStream(key string) {
136 | GlobalSources.deleteSource(key)
137 | }
138 |
--------------------------------------------------------------------------------
/rtmp/co/gop_cache.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "log"
5 | "seal/rtmp/flv"
6 | "seal/rtmp/pt"
7 | "sync"
8 | )
9 |
10 | // GopCache cache gop of video/audio to enable players fast start
11 | type GopCache struct {
12 | mu sync.RWMutex
13 |
14 | // cachedVideoCount the video frame count, avoid cache for pure audio stream.
15 | cachedVideoCount uint32
16 | // when user disabled video when publishing, and gop cache enalbed,
17 | // we will cache the audio/video for we already got video, but we never
18 | // know when to clear the gop cache, for there is no video in future,
19 | // so we must guess whether user disabled the video.
20 | // when we got some audios after laster video, for instance, 600 audio packets,
21 | // about 3s(26ms per packet) 115 audio packets, clear gop cache.
22 | //
23 | // it is ok for performance, for when we clear the gop cache,
24 | // gop cache is disabled for pure audio stream.
25 | audioAfterLastVideoCount uint32
26 |
27 | msgs []*pt.Message
28 | }
29 |
30 | func (g *GopCache) cache(msg *pt.Message) {
31 |
32 | if nil == msg {
33 | return
34 | }
35 |
36 | g.mu.Lock()
37 | defer g.mu.Unlock()
38 |
39 | if msg.Header.IsVideo() {
40 | g.cachedVideoCount++
41 | g.audioAfterLastVideoCount = 0
42 | }
43 |
44 | if g.pureAudio() {
45 | return
46 | }
47 |
48 | if msg.Header.IsAudio() {
49 | g.audioAfterLastVideoCount++
50 | }
51 |
52 | if g.audioAfterLastVideoCount > pt.PureAudioGuessCount {
53 | g.clear()
54 | log.Printf("gop cahce pure audio more than %d seconds, clear old caches.\n", pt.PureAudioGuessCount)
55 | return
56 | }
57 |
58 | // clear gop cache when got key frame
59 | if msg.Header.IsVideo() && flv.VideoIsH264(msg.Payload.Payload) && flv.VideoH264IsKeyframe(msg.Payload.Payload) {
60 | g.clear()
61 |
62 | // curent msg is video frame, so we set to 1.
63 | g.cachedVideoCount = 1
64 | }
65 |
66 | g.msgs = append(g.msgs, msg)
67 | }
68 |
69 | func (g *GopCache) pureAudio() bool {
70 | return 0 == g.cachedVideoCount
71 | }
72 |
73 | func (g *GopCache) clear() {
74 | g.msgs = nil
75 | g.cachedVideoCount = 0
76 | g.audioAfterLastVideoCount = 0
77 | }
78 |
79 | func (g *GopCache) Empty() bool {
80 | return nil == g.msgs
81 | }
82 |
83 | func (g *GopCache) StartTime() uint64 {
84 | if nil != g.msgs && nil != g.msgs[0] {
85 | return g.msgs[0].Header.Timestamp
86 | }
87 |
88 | return 0
89 | }
90 |
91 | func (g *GopCache) Dump(c *Consumer, atc bool, tba float64, tbv float64, timeJitter uint32) {
92 |
93 | g.mu.Lock()
94 | defer g.mu.Unlock()
95 |
96 | for _, v := range g.msgs {
97 |
98 | if nil == v {
99 | continue
100 | }
101 |
102 | c.Enquene(v, atc, tba, tbv, timeJitter)
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/rtmp/co/handshake.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | "log"
7 | "seal/rtmp/pt"
8 |
9 | "github.com/calabashdad/utiltools"
10 | )
11 |
12 | func (rc *RtmpConn) handShake() (err error) {
13 | defer func() {
14 | if err := recover(); err != nil {
15 | log.Println(utiltools.PanicTrace())
16 | }
17 | }()
18 |
19 | var handshakeData [6146]uint8 // c0(1) + c1(1536) + c2(1536) + s0(1) + s1(1536) + s2(1536)
20 |
21 | c0 := handshakeData[:1]
22 | c1 := handshakeData[1:1537]
23 | c2 := handshakeData[1537:3073]
24 |
25 | s0 := handshakeData[3073:3074]
26 | s1 := handshakeData[3074:4610]
27 | s2 := handshakeData[4610:6146]
28 |
29 | c0c1 := handshakeData[0:1537]
30 | s0s1s2 := handshakeData[3073:6146]
31 |
32 | //recv c0c1
33 | if err = rc.tcpConn.ExpectBytesFull(c0c1, 1537); err != nil {
34 | return
35 | }
36 |
37 | //parse c0
38 | if c0[0] != 3 {
39 | err = fmt.Errorf("client c0 is not 3")
40 | return
41 | }
42 |
43 | //use complex handshake, if complex handshake failed, try use simple handshake
44 | //parse c1
45 | clientVer := binary.BigEndian.Uint32(c1[4:8])
46 | if 0 != clientVer {
47 | if err = pt.ComplexHandShake(c1, s0, s1, s2); err != nil {
48 | return
49 | }
50 | log.Println("complex handshake success.")
51 |
52 | } else {
53 | //use simple handshake
54 | log.Println("0 == clientVer, client use simple handshake.")
55 | s0[0] = 3
56 | copy(s1, c2)
57 | copy(s2, c1)
58 | }
59 |
60 | //send s0s1s2
61 | if err = rc.tcpConn.SendBytes(s0s1s2); err != nil {
62 | return
63 | }
64 |
65 | //recv c2
66 | if err = rc.tcpConn.ExpectBytesFull(c2, uint32(len(c2))); err != nil {
67 | return
68 | }
69 |
70 | //c2 do not need verify.
71 |
72 | return
73 | }
74 |
--------------------------------------------------------------------------------
/rtmp/co/msg_abort.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "log"
5 | "seal/rtmp/pt"
6 | "github.com/calabashdad/utiltools"
7 | )
8 |
9 | func (rc *RtmpConn) msgAbort(msg *pt.Message) (err error) {
10 | defer func() {
11 | if err := recover(); err != nil {
12 | log.Println(utiltools.PanicTrace())
13 | }
14 | }()
15 |
16 | log.Println("MsgAbort")
17 |
18 | if nil == msg {
19 | return
20 | }
21 |
22 | return
23 | }
24 |
--------------------------------------------------------------------------------
/rtmp/co/msg_ack.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "log"
5 | "seal/rtmp/pt"
6 | "github.com/calabashdad/utiltools"
7 | )
8 |
9 | func (rc *RtmpConn) msgAck(msg *pt.Message) (err error) {
10 | defer func() {
11 | if err := recover(); err != nil {
12 | log.Println(utiltools.PanicTrace())
13 | }
14 | }()
15 |
16 | log.Println("MsgAck")
17 |
18 | if nil == msg {
19 | return
20 | }
21 |
22 | p := pt.AcknowlegementPacket{}
23 | if err = p.Decode(msg.Payload.Payload); err != nil {
24 | return
25 | }
26 |
27 | return
28 | }
29 |
--------------------------------------------------------------------------------
/rtmp/co/msg_aggre.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | "log"
7 | "seal/rtmp/pt"
8 |
9 | "github.com/calabashdad/utiltools"
10 | )
11 |
12 | func (rc *RtmpConn) msgAggregate(msg *pt.Message) (err error) {
13 | defer func() {
14 | if err := recover(); err != nil {
15 | log.Println(utiltools.PanicTrace())
16 | }
17 | }()
18 |
19 | log.Println("aggregate")
20 | if nil == msg {
21 | return
22 | }
23 |
24 | var offset uint32
25 | maxOffset := len(msg.Payload.Payload) - 1
26 | if maxOffset < 0 {
27 | return
28 | }
29 | for {
30 | if offset > uint32(maxOffset) {
31 | break
32 | }
33 |
34 | dataType := msg.Payload.Payload[offset]
35 | offset++
36 |
37 | _ = dataType
38 |
39 | if maxOffset-int(offset) < 3 {
40 | err = fmt.Errorf("aggregate msg size invalid")
41 | break
42 | }
43 | // data size, 3 bytes, big endian
44 | dataSize := uint32(msg.Payload.Payload[offset])<<16 + uint32(msg.Payload.Payload[offset+1])<<8 + uint32(msg.Payload.Payload[offset+2])
45 | if dataSize <= 0 {
46 | err = fmt.Errorf("aggregate msg size invalid, 0")
47 | break
48 | }
49 | offset += 3
50 |
51 | if maxOffset-int(offset) < 3 {
52 | err = fmt.Errorf("aggregate msg timestamp invalid")
53 | break
54 | }
55 | timeStamp := uint32(msg.Payload.Payload[offset])<<16 + uint32(msg.Payload.Payload[offset+1])<<8 + uint32(msg.Payload.Payload[offset+2])
56 | offset += 3
57 |
58 | if maxOffset-int(offset) < 1 {
59 | err = fmt.Errorf("aggregate msg timeH invalid")
60 | break
61 | }
62 | timeH := uint32(msg.Payload.Payload[offset])
63 | offset++
64 |
65 | timeStamp |= timeH << 24
66 | timeStamp &= 0x7FFFFFFF
67 |
68 | if maxOffset-int(offset) < 3 {
69 | err = fmt.Errorf("aggregate msg stream id invalid")
70 | break
71 | }
72 | streamID := uint32(msg.Payload.Payload[offset])<<16 + uint32(msg.Payload.Payload[offset+1])<<8 + uint32(msg.Payload.Payload[offset+2])
73 | offset += 3
74 |
75 | if maxOffset-int(offset) < int(dataSize) {
76 | err = fmt.Errorf("aggregate msg data size, not enough")
77 | break
78 | }
79 |
80 | var o pt.Message
81 | o.Header.MessageType = dataType
82 | o.Header.PayloadLength = dataSize
83 | o.Header.TimestampDelta = timeStamp
84 | o.Header.Timestamp = uint64(timeStamp)
85 | o.Header.StreamID = streamID
86 | o.Header.PerferCsid = msg.Header.PerferCsid
87 |
88 | o.Payload.Payload = make([]uint8, dataSize)
89 | copy(o.Payload.Payload, msg.Payload.Payload[offset:offset+dataSize])
90 | offset += dataSize
91 |
92 | if maxOffset-int(offset) < 4 {
93 | err = fmt.Errorf("aggregate msg previous tag size")
94 | break
95 | }
96 | _ = binary.BigEndian.Uint32(msg.Payload.Payload[offset : offset+4])
97 | offset += 4
98 |
99 | // process has parsed message
100 | if o.Header.IsAudio() {
101 | log.Println("aggregate audio msg")
102 | if err = rc.msgAudio(&o); err != nil {
103 | break
104 | }
105 | } else if o.Header.IsVideo() {
106 | log.Println("aggregate video msg")
107 | if err = rc.msgVideo(&o); err != nil {
108 | break
109 | }
110 | }
111 | }
112 |
113 | return
114 | }
115 |
--------------------------------------------------------------------------------
/rtmp/co/msg_audio.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "log"
5 | "seal/rtmp/flv"
6 | "seal/rtmp/pt"
7 |
8 | "github.com/calabashdad/utiltools"
9 | )
10 |
11 | func (rc *RtmpConn) msgAudio(msg *pt.Message) (err error) {
12 | defer func() {
13 | if err := recover(); err != nil {
14 | log.Println(utiltools.PanicTrace())
15 | }
16 | }()
17 |
18 | if nil == msg {
19 | return
20 | }
21 |
22 | // hls
23 | if nil != rc.source.hls {
24 | if err = rc.source.hls.OnAudio(msg); err != nil {
25 | log.Println("hls process audio data failed, err=", err)
26 | return
27 | }
28 | }
29 |
30 | //copy to all consumers
31 | rc.source.copyToAllConsumers(msg)
32 |
33 | //cache the sequence.
34 | // do not cache the sequence header to gop cache, return here
35 | if flv.AudioIsSequenceHeader(msg.Payload.Payload) {
36 | rc.source.CacheAudioSequenceHeader = msg
37 | log.Println("cache audio data sequence")
38 | return
39 | }
40 |
41 | rc.source.GopCache.cache(msg)
42 |
43 | //if rc.source.Atc {
44 | // if nil != rc.source.CacheAudioSequenceHeader {
45 | // rc.source.CacheAudioSequenceHeader.Header.Timestamp = msg.Header.Timestamp
46 | // }
47 | //
48 | // if nil != rc.source.CacheMetaData {
49 | // rc.source.CacheMetaData.Header.Timestamp = msg.Header.Timestamp
50 | // }
51 | //}
52 |
53 | return
54 | }
55 |
--------------------------------------------------------------------------------
/rtmp/co/msg_set_ack.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "log"
5 | "seal/rtmp/pt"
6 |
7 | "github.com/calabashdad/utiltools"
8 | )
9 |
10 | func (rc *RtmpConn) msgSetAck(msg *pt.Message) (err error) {
11 | defer func() {
12 | if err := recover(); err != nil {
13 | log.Println(utiltools.PanicTrace())
14 | }
15 | }()
16 |
17 | log.Println("MsgSetChunk")
18 |
19 | if nil == msg {
20 | return
21 | }
22 |
23 | p := pt.SetWindowAckSizePacket{}
24 | if err = p.Decode(msg.Payload.Payload); err != nil {
25 | return
26 | }
27 |
28 | if p.AckowledgementWindowSize > 0 {
29 | rc.ack.ackWindowSize = p.AckowledgementWindowSize
30 | log.Println("set ack window size=", p.AckowledgementWindowSize)
31 | }
32 |
33 | return
34 | }
35 |
--------------------------------------------------------------------------------
/rtmp/co/msg_set_band.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "log"
5 | "seal/rtmp/pt"
6 |
7 | "github.com/calabashdad/utiltools"
8 | )
9 |
10 | func (rc *RtmpConn) msgSetBand(msg *pt.Message) (err error) {
11 | defer func() {
12 | if err := recover(); err != nil {
13 | log.Println(utiltools.PanicTrace())
14 | }
15 | }()
16 |
17 | log.Println("MsgSetBand")
18 |
19 | if nil == msg {
20 | return
21 | }
22 |
23 | p := pt.SetPeerBandWidthPacket{}
24 | if err = p.Decode(msg.Payload.Payload); err != nil {
25 | return
26 | }
27 |
28 | return
29 | }
30 |
--------------------------------------------------------------------------------
/rtmp/co/msg_set_chunk.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "log"
5 | "seal/rtmp/pt"
6 |
7 | "github.com/calabashdad/utiltools"
8 | )
9 |
10 | func (rc *RtmpConn) msgSetChunk(msg *pt.Message) (err error) {
11 | defer func() {
12 | if err := recover(); err != nil {
13 | log.Println(utiltools.PanicTrace())
14 | }
15 | }()
16 |
17 | log.Println("set chunk size")
18 |
19 | if nil == msg {
20 | return
21 | }
22 |
23 | p := pt.SetChunkSizePacket{}
24 | if err = p.Decode(msg.Payload.Payload); err != nil {
25 | return
26 | }
27 |
28 | if p.ChunkSize >= pt.RtmpChunkSizeMin && p.ChunkSize <= pt.RtmpChunkSizeMax {
29 | rc.inChunkSize = p.ChunkSize
30 | log.Println("peer set chunk size to ", p.ChunkSize)
31 | }
32 | log.Println("remote set chunk size to ", rc.inChunkSize)
33 |
34 | return
35 | }
36 |
--------------------------------------------------------------------------------
/rtmp/co/msg_user_ctrl.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "log"
5 | "seal/rtmp/pt"
6 |
7 | "github.com/calabashdad/utiltools"
8 | )
9 |
10 | func (rc *RtmpConn) msgUserCtrl(msg *pt.Message) (err error) {
11 | defer func() {
12 | if err := recover(); err != nil {
13 | log.Println(utiltools.PanicTrace())
14 | }
15 | }()
16 |
17 | log.Println("MsgUserCtrl")
18 |
19 | if nil == msg {
20 | return
21 | }
22 |
23 | p := pt.UserControlPacket{}
24 | if err = p.Decode(msg.Payload.Payload); err != nil {
25 | return
26 | }
27 |
28 | log.Println("MsgUserCtrl event type=", p.EventType)
29 |
30 | switch p.EventType {
31 | case pt.SrcPCUCStreamBegin:
32 | case pt.SrcPCUCStreamEOF:
33 | case pt.SrcPCUCStreamDry:
34 | case pt.SrcPCUCSetBufferLength:
35 | case pt.SrcPCUCStreamIsRecorded:
36 | case pt.SrcPCUCPingRequest:
37 | err = rc.ctrlPingRequest(&p)
38 | case pt.SrcPCUCPingResponse:
39 | default:
40 | log.Println("msg user ctrl unknown event type.type=", p.EventType)
41 | }
42 |
43 | if err != nil {
44 | return
45 | }
46 |
47 | return
48 | }
49 |
50 | func (rc *RtmpConn) ctrlPingRequest(p *pt.UserControlPacket) (err error) {
51 |
52 | log.Println("ctrl ping request.")
53 |
54 | if pt.SrcPCUCSetBufferLength == p.EventType {
55 |
56 | } else if pt.SrcPCUCPingRequest == p.EventType {
57 | var pp pt.UserControlPacket
58 | pp.EventType = pt.SrcPCUCPingResponse
59 | pp.EventData = p.EventData
60 | if err = rc.sendPacket(&pp, 0); err != nil {
61 | return
62 | }
63 |
64 | log.Println("send ping response success.")
65 |
66 | }
67 |
68 | return
69 | }
70 |
--------------------------------------------------------------------------------
/rtmp/co/msg_video.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "log"
5 | "seal/rtmp/flv"
6 | "seal/rtmp/pt"
7 |
8 | "github.com/calabashdad/utiltools"
9 | )
10 |
11 | func (rc *RtmpConn) msgVideo(msg *pt.Message) (err error) {
12 | defer func() {
13 | if err := recover(); err != nil {
14 | log.Println(utiltools.PanicTrace())
15 | }
16 | }()
17 |
18 | if nil == msg {
19 | return
20 | }
21 |
22 | //if flv.VideoH264IsKeyframe(msg.Payload.Payload) {
23 | // log.Printf("+++++recv i frame msg, type=%d, time=%d, len=%d\n", msg.Header.MessageType, msg.Header.Timestamp, msg.Header.PayloadLength)
24 | //}
25 |
26 | // hls
27 | if nil != rc.source.hls {
28 | if err = rc.source.hls.OnVideo(msg); err != nil {
29 | log.Println("hls process video data failed, err=", err)
30 | return
31 | }
32 | }
33 |
34 | //copy to all consumers
35 | rc.source.copyToAllConsumers(msg)
36 |
37 | //cache the key frame
38 | // do not cache the sequence header to gop cache, return here
39 | if flv.VideoH264IsKeyFrameAndSequenceHeader(msg.Payload.Payload) {
40 | rc.source.CacheVideoSequenceHeader = msg
41 | log.Println("cache video sequence")
42 | return
43 | }
44 |
45 | rc.source.GopCache.cache(msg)
46 |
47 | if rc.source.Atc {
48 | if nil != rc.source.CacheVideoSequenceHeader {
49 | rc.source.CacheVideoSequenceHeader.Header.Timestamp = msg.Header.Timestamp
50 | }
51 |
52 | if nil != rc.source.CacheMetaData {
53 | rc.source.CacheMetaData.Header.Timestamp = msg.Header.Timestamp
54 | }
55 | }
56 |
57 | return
58 | }
59 |
--------------------------------------------------------------------------------
/rtmp/co/on_recv_msg.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "log"
5 | "seal/rtmp/pt"
6 |
7 | "github.com/calabashdad/utiltools"
8 | )
9 |
10 | func (rc *RtmpConn) onRecvMsg(msg *pt.Message) (err error) {
11 | defer func() {
12 | if err := recover(); err != nil {
13 | log.Println(utiltools.PanicTrace())
14 | }
15 | }()
16 |
17 | if nil == msg {
18 | return
19 | }
20 |
21 | if err = rc.estimateNeedSendAcknowlegement(); err != nil {
22 | return
23 | }
24 |
25 | switch msg.Header.MessageType {
26 | case pt.RtmpMsgAmf3CommandMessage, pt.RtmpMsgAmf0CommandMessage,
27 | pt.RtmpMsgAmf0DataMessage, pt.RtmpMsgAmf3DataMessage:
28 | err = rc.msgAmf(msg)
29 | case pt.RtmpMsgUserControlMessage:
30 | err = rc.msgUserCtrl(msg)
31 | case pt.RtmpMsgWindowAcknowledgementSize:
32 | err = rc.msgSetAck(msg)
33 | case pt.RtmpMsgSetChunkSize:
34 | err = rc.msgSetChunk(msg)
35 | case pt.RtmpMsgSetPeerBandwidth:
36 | err = rc.msgSetBand(msg)
37 | case pt.RtmpMsgAcknowledgement:
38 | err = rc.msgAck(msg)
39 | case pt.RtmpMsgAbortMessage:
40 | err = rc.msgAbort(msg)
41 | case pt.RtmpMsgEdgeAndOriginServerCommand:
42 | case pt.RtmpMsgAmf3SharedObject:
43 | case pt.RtmpMsgAmf0SharedObject:
44 | case pt.RtmpMsgAudioMessage:
45 | err = rc.msgAudio(msg)
46 | case pt.RtmpMsgVideoMessage:
47 | err = rc.msgVideo(msg)
48 | case pt.RtmpMsgAggregateMessage:
49 | err = rc.msgAggregate(msg)
50 | default:
51 | log.Println("on recv msg unknown msg typeid=", msg.Header.MessageType)
52 | }
53 |
54 | if err != nil {
55 | return
56 | }
57 |
58 | return
59 | }
60 |
--------------------------------------------------------------------------------
/rtmp/co/playing.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "seal/conf"
7 | "seal/rtmp/pt"
8 | "time"
9 | )
10 |
11 | func (rc *RtmpConn) playing(p *pt.PlayPacket) (err error) {
12 |
13 | userSpecifiedDurationToStop := p.Duration > 0
14 | var startTime int64 = -1
15 |
16 | timeLast := time.Now().Unix()
17 |
18 | for {
19 | //read from client. use short time out.
20 | //if recv failed, it's ok, not an error.
21 | if true {
22 | const timeOutUs = 10 * 1000 //ms
23 | rc.tcpConn.SetRecvTimeout(timeOutUs)
24 |
25 | var msg pt.Message
26 | if localErr := rc.recvMsg(&msg.Header, &msg.Payload); localErr != nil {
27 | // do nothing, it's ok
28 | }
29 | if len(msg.Payload.Payload) > 0 {
30 | //has recved play control.
31 | err = rc.handlePlayData(&msg)
32 | if err != nil {
33 | log.Println("playing... handle play data faield.err=", err)
34 | return
35 | }
36 | }
37 | }
38 |
39 | // reset the socket send and recv timeout
40 | rc.tcpConn.SetRecvTimeout(conf.GlobalConfInfo.Rtmp.TimeOut * 1000 * 1000)
41 | rc.tcpConn.SetSendTimeout(conf.GlobalConfInfo.Rtmp.TimeOut * 1000 * 1000)
42 |
43 | msg := rc.consumer.Dump()
44 | if nil == msg {
45 | // wait and try again.
46 | timeCurrent := time.Now().Unix()
47 | if timeCurrent-timeLast > 5 {
48 | log.Println("rtmp playing time out > 5 seconds, break. key=", rc.streamName)
49 | break
50 | }
51 |
52 | continue
53 | } else {
54 | timeLast = time.Now().Unix()
55 |
56 | //send to remote
57 | // only when user specifies the duration,
58 | // we start to collect the durations for each message.
59 | if userSpecifiedDurationToStop {
60 | if startTime < 0 || startTime > int64(msg.Header.Timestamp) {
61 | startTime = int64(msg.Header.Timestamp)
62 | }
63 |
64 | rc.consumer.duration += (float64(msg.Header.Timestamp) - float64(startTime))
65 | startTime = int64(msg.Header.Timestamp)
66 | }
67 |
68 | if err = rc.sendMsg(msg); err != nil {
69 | log.Println("playing... send to remote failed.err=", err)
70 | break
71 | }
72 | }
73 | }
74 |
75 | return
76 | }
77 |
78 | func (rc *RtmpConn) handlePlayData(msg *pt.Message) (err error) {
79 |
80 | if nil == msg {
81 | return
82 | }
83 |
84 | if msg.Header.IsAmf0Data() || msg.Header.IsAmf3Data() {
85 | log.Println("play data: recv handled play amf data")
86 | } else {
87 | //process user control
88 | rc.handlePlayUserControl(msg)
89 | }
90 |
91 | return
92 | }
93 |
94 | func (rc *RtmpConn) handlePlayUserControl(msg *pt.Message) (err error) {
95 |
96 | if nil == msg {
97 | return
98 | }
99 |
100 | if msg.Header.IsAmf0Command() || msg.Header.IsAmf3Command() {
101 | //ignore
102 | log.Println("ignore amf cmd when handle play user control.")
103 | return
104 | }
105 |
106 | // for jwplayer/flowplayer, which send close as pause message.
107 | if true {
108 | p := pt.CloseStreamPacket{}
109 | if localError := p.Decode(msg.Payload.Payload); localError != nil {
110 | //it's ok
111 | } else {
112 | err = fmt.Errorf("player ask to close stream,remote=%s", rc.tcpConn.RemoteAddr())
113 | return
114 | }
115 | }
116 |
117 | // call msg,
118 | // support response null first
119 | if true {
120 | p := pt.CallPacket{}
121 | if localErr := p.Decode(msg.Payload.Payload); localErr != nil {
122 | // it's ok
123 | } else {
124 | pRes := pt.CallResPacket{}
125 | pRes.CommandName = pt.RtmpAmf0CommandResult
126 | pRes.TransactionID = p.TransactionID
127 | pRes.CommandObjectMarker = pt.RtmpAMF0Null
128 | pRes.ResponseMarker = pt.RtmpAMF0Null
129 |
130 | if err = rc.sendPacket(&pRes, 0); err != nil {
131 | return
132 | }
133 | }
134 | }
135 |
136 | //pause or other msg
137 | if true {
138 | p := pt.PausePacket{}
139 | if localErr := p.Decode(msg.Payload.Payload); localErr != nil {
140 | // it's ok
141 | } else {
142 | if err = rc.onPlayClientPause(uint32(rc.defaultStreamID), p.IsPause); err != nil {
143 | log.Println("play client pause error.err=", err)
144 | return
145 | }
146 |
147 | if err = rc.consumer.onPlayPause(p.IsPause); err != nil {
148 | log.Println("consumer on play pause error.err=", err)
149 | return
150 | }
151 | }
152 | }
153 |
154 | return
155 | }
156 |
157 | func (rc *RtmpConn) onPlayClientPause(streamID uint32, isPause bool) (err error) {
158 |
159 | if isPause {
160 | // onStatus(NetStream.Pause.Notify)
161 | p := pt.OnStatusCallPacket{}
162 | p.CommandName = pt.RtmpAmf0CommandOnStatus
163 |
164 | p.AddObj(pt.NewAmf0Object(pt.StatusLevel, pt.StatusLevelStatus, pt.RtmpAmf0String))
165 | p.AddObj(pt.NewAmf0Object(pt.StatusCode, pt.StatusCodeStreamPause, pt.RtmpAmf0String))
166 | p.AddObj(pt.NewAmf0Object(pt.StatusDescription, "Paused stream.", pt.RtmpAmf0String))
167 |
168 | if err = rc.sendPacket(&p, streamID); err != nil {
169 | return
170 | }
171 |
172 | // StreamEOF
173 | if true {
174 | p := pt.UserControlPacket{}
175 | p.EventType = pt.SrcPCUCStreamEOF
176 | p.EventData = streamID
177 |
178 | if err = rc.sendPacket(&p, streamID); err != nil {
179 | log.Println("send PCUC(StreamEOF) message failed.")
180 | return
181 | }
182 | }
183 |
184 | } else {
185 | // onStatus(NetStream.Unpause.Notify)
186 | p := pt.OnStatusCallPacket{}
187 | p.CommandName = pt.RtmpAmf0CommandOnStatus
188 |
189 | p.AddObj(pt.NewAmf0Object(pt.StatusLevel, pt.StatusLevelStatus, pt.RtmpAmf0String))
190 | p.AddObj(pt.NewAmf0Object(pt.StatusCode, pt.StatusCodeStreamUnpause, pt.RtmpAmf0String))
191 | p.AddObj(pt.NewAmf0Object(pt.StatusDescription, "UnPaused stream.", pt.RtmpAmf0String))
192 |
193 | if err = rc.sendPacket(&p, streamID); err != nil {
194 | return
195 | }
196 |
197 | // StreanBegin
198 | if true {
199 | p := pt.UserControlPacket{}
200 | p.EventType = pt.SrcPCUCStreamBegin
201 | p.EventData = streamID
202 |
203 | if err = rc.sendPacket(&p, streamID); err != nil {
204 | log.Println("send PCUC(StreanBegin) message failed.")
205 | return
206 | }
207 | }
208 | }
209 |
210 | return
211 | }
212 |
--------------------------------------------------------------------------------
/rtmp/co/recv_msg.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | "log"
7 | "seal/rtmp/pt"
8 |
9 | "github.com/calabashdad/utiltools"
10 | )
11 |
12 | // recvMsg recv whole msg and quit when got an entire msg, not handle it at all.
13 | func (rc *RtmpConn) recvMsg(header *pt.MessageHeader, payload *pt.MessagePayload) (err error) {
14 | defer func() {
15 | if err := recover(); err != nil {
16 | log.Println(utiltools.PanicTrace())
17 | }
18 | }()
19 |
20 | for {
21 | // read basic header
22 | var buf [3]uint8
23 | if err = rc.tcpConn.ExpectBytesFull(buf[:1], 1); err != nil {
24 | return
25 | }
26 |
27 | chunkFmt := (buf[0] & 0xc0) >> 6
28 | csid := uint32(buf[0] & 0x3f)
29 |
30 | switch csid {
31 | case 0:
32 | //csId 2 bytes. 64-319
33 | err = rc.tcpConn.ExpectBytesFull(buf[1:2], 1)
34 | csid = uint32(64 + buf[1])
35 | case 1:
36 | //csId 3 bytes. 64-65599
37 | err = rc.tcpConn.ExpectBytesFull(buf[1:3], 2)
38 | csid = uint32(64) + uint32(buf[1]) + uint32(buf[2])*256
39 | }
40 |
41 | if err != nil {
42 | break
43 | }
44 |
45 | chunk, ok := rc.chunkStreams[csid]
46 | if !ok {
47 | chunk = &pt.ChunkStream{}
48 | rc.chunkStreams[csid] = chunk
49 | }
50 |
51 | chunk.Fmt = chunkFmt
52 | chunk.CsID = csid
53 | chunk.MsgHeader.PerferCsid = csid
54 |
55 | // read message header
56 | if 0 == chunk.MsgCount && chunk.Fmt != pt.RtmpFmtType0 {
57 | if pt.RtmpCidProtocolControl == chunk.CsID && pt.RtmpFmtType1 == chunk.Fmt {
58 | // for librtmp, if ping, it will send a fresh stream with fmt=1,
59 | // 0x42 where: fmt=1, cid=2, protocol contorl user-control message
60 | // 0x00 0x00 0x00 where: timestamp=0
61 | // 0x00 0x00 0x06 where: payload_length=6
62 | // 0x04 where: message_type=4(protocol control user-control message)
63 | // 0x00 0x06 where: event Ping(0x06)
64 | // 0x00 0x00 0x0d 0x0f where: event data 4bytes ping timestamp.
65 | log.Println("rtmp session, accept cid=2, chunkFmt=1 , it's a valid chunk format, for librtmp.")
66 | } else {
67 | err = fmt.Errorf("chunk start error, must be RTMP_FMT_TYPE0")
68 | break
69 | }
70 | }
71 |
72 | if payload.SizeTmp > 0 && pt.RtmpFmtType0 == chunk.Fmt {
73 | err = fmt.Errorf("when msg count > 0, chunk fmt is not allowed to be RTMP_FMT_TYPE0")
74 | break
75 | }
76 |
77 | var msgHeaderSize uint32
78 |
79 | switch chunk.Fmt {
80 | case pt.RtmpFmtType0:
81 | msgHeaderSize = 11
82 | case pt.RtmpFmtType1:
83 | msgHeaderSize = 7
84 | case pt.RtmpFmtType2:
85 | msgHeaderSize = 3
86 | case pt.RtmpFmtType3:
87 | msgHeaderSize = 0
88 | }
89 |
90 | var msgHeader [11]uint8 //max is 11
91 | err = rc.tcpConn.ExpectBytesFull(msgHeader[:], msgHeaderSize)
92 | if err != nil {
93 | break
94 | }
95 |
96 | // parse msg header
97 | // 3bytes: timestamp delta, fmt=0,1,2
98 | // 3bytes: payload length, fmt=0,1
99 | // 1bytes: message type, fmt=0,1
100 | // 4bytes: stream id, fmt=0
101 | switch chunk.Fmt {
102 | case pt.RtmpFmtType0:
103 | chunk.MsgHeader.TimestampDelta = uint32(msgHeader[0])<<16 + uint32(msgHeader[1])<<8 + uint32(msgHeader[2])
104 | if chunk.MsgHeader.TimestampDelta >= pt.RtmpExtendTimeStamp {
105 | chunk.HasExtendedTimestamp = true
106 | } else {
107 | chunk.HasExtendedTimestamp = false
108 | // For a type-0 chunk, the absolute timestamp of the message is sent here.
109 | chunk.MsgHeader.Timestamp = uint64(chunk.MsgHeader.TimestampDelta)
110 | }
111 |
112 | payloadLength := uint32(msgHeader[3])<<16 + uint32(msgHeader[4])<<8 + uint32(msgHeader[5])
113 | if payload.SizeTmp > 0 && payloadLength != chunk.MsgHeader.PayloadLength {
114 | err = fmt.Errorf("RTMP_FMT_TYPE0: msg has in chunk, msg size can not change")
115 | break
116 | }
117 |
118 | chunk.MsgHeader.PayloadLength = payloadLength
119 | chunk.MsgHeader.MessageType = msgHeader[6]
120 | chunk.MsgHeader.StreamID = binary.LittleEndian.Uint32(msgHeader[7:11])
121 |
122 | case pt.RtmpFmtType1:
123 | chunk.MsgHeader.TimestampDelta = uint32(msgHeader[0])<<16 + uint32(msgHeader[1])<<8 + uint32(msgHeader[2])
124 | if chunk.MsgHeader.TimestampDelta >= pt.RtmpExtendTimeStamp {
125 | chunk.HasExtendedTimestamp = true
126 | } else {
127 | chunk.HasExtendedTimestamp = false
128 | chunk.MsgHeader.Timestamp += uint64(chunk.MsgHeader.TimestampDelta)
129 | }
130 |
131 | payloadLength := uint32(msgHeader[3])<<16 + uint32(msgHeader[4])<<8 + uint32(msgHeader[5])
132 | if payload.SizeTmp > 0 && payloadLength != chunk.MsgHeader.PayloadLength {
133 | err = fmt.Errorf("RTMP_FMT_TYPE1: msg has in chunk, msg size can not change")
134 | break
135 | }
136 |
137 | chunk.MsgHeader.PayloadLength = payloadLength
138 | chunk.MsgHeader.MessageType = msgHeader[6]
139 |
140 | case pt.RtmpFmtType2:
141 | chunk.MsgHeader.TimestampDelta = uint32(msgHeader[0])<<16 + uint32(msgHeader[1])<<8 + uint32(msgHeader[2])
142 | if chunk.MsgHeader.TimestampDelta >= pt.RtmpExtendTimeStamp {
143 | chunk.HasExtendedTimestamp = true
144 | } else {
145 | chunk.HasExtendedTimestamp = false
146 | chunk.MsgHeader.Timestamp += uint64(chunk.MsgHeader.TimestampDelta)
147 | }
148 | case pt.RtmpFmtType3:
149 | // update the timestamp even fmt=3 for first chunk packet. the same with previous.
150 | if 0 == payload.SizeTmp && !chunk.HasExtendedTimestamp {
151 | chunk.MsgHeader.Timestamp += uint64(chunk.MsgHeader.TimestampDelta)
152 | }
153 | }
154 |
155 | if err != nil {
156 | break
157 | }
158 |
159 | // read extend timestamp
160 | if chunk.HasExtendedTimestamp {
161 | var buf [4]uint8
162 | if err = rc.tcpConn.ExpectBytesFull(buf[:], 4); err != nil {
163 | break
164 | }
165 |
166 | extendTimeStamp := binary.BigEndian.Uint32(buf[0:4])
167 |
168 | // always use 31bits timestamp, for some server may use 32bits extended timestamp.
169 | extendTimeStamp &= 0x7fffffff
170 |
171 | chunkTimeStamp := chunk.MsgHeader.Timestamp
172 | if 0 == payload.SizeTmp || 0 == chunkTimeStamp {
173 | chunk.MsgHeader.Timestamp = uint64(extendTimeStamp)
174 | }
175 |
176 | // because of the flv file format is lower 24bits, and higher 8bit is SI32, so timestamp is 31bit.
177 | chunk.MsgHeader.Timestamp &= 0x7fffffff
178 | }
179 |
180 | chunk.MsgCount++
181 |
182 | // make cache of msg
183 | if uint32(len(payload.Payload)) < chunk.MsgHeader.PayloadLength {
184 | payload.Payload = make([]uint8, chunk.MsgHeader.PayloadLength)
185 | }
186 |
187 | // read chunk data
188 | remainPayloadSize := chunk.MsgHeader.PayloadLength - payload.SizeTmp
189 |
190 | if remainPayloadSize >= rc.inChunkSize {
191 | remainPayloadSize = rc.inChunkSize
192 | }
193 |
194 | if err = rc.tcpConn.ExpectBytesFull(payload.Payload[payload.SizeTmp:payload.SizeTmp+remainPayloadSize], remainPayloadSize); err != nil {
195 | break
196 | } else {
197 | payload.SizeTmp += remainPayloadSize
198 | if payload.SizeTmp == chunk.MsgHeader.PayloadLength {
199 |
200 | *header = chunk.MsgHeader
201 |
202 | // has recv entire rtmp message.
203 | // reset the payload size this time, the message actually size is header length, this chunk can reuse by a new csid.
204 | payload.SizeTmp = 0
205 |
206 | break
207 | }
208 | }
209 |
210 | }
211 |
212 | if err != nil {
213 | return
214 | }
215 |
216 | return
217 | }
218 |
--------------------------------------------------------------------------------
/rtmp/co/res_ack.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "log"
5 | "seal/rtmp/pt"
6 |
7 | "github.com/calabashdad/utiltools"
8 | )
9 |
10 | func (rc *RtmpConn) responseAcknowlegementMsg() (err error) {
11 | defer func() {
12 | if err := recover(); err != nil {
13 | log.Println(utiltools.PanicTrace())
14 | }
15 | }()
16 |
17 | var pkt pt.AcknowlegementPacket
18 |
19 | pkt.SequenceNumber = uint32(rc.tcpConn.GetRecvBytesSum())
20 |
21 | if err = rc.sendPacket(&pkt, 0); err != nil {
22 | return
23 | }
24 |
25 | rc.ack.hasAckedSize = rc.tcpConn.GetRecvBytesSum()
26 |
27 | return
28 | }
29 |
30 | func (rc *RtmpConn) estimateNeedSendAcknowlegement() (err error) {
31 |
32 | if rc.ack.ackWindowSize > 0 &&
33 | ((rc.tcpConn.GetRecvBytesSum() - rc.ack.hasAckedSize) > uint64(rc.ack.ackWindowSize)) {
34 | // response a acknowlegement to peer.
35 | if err = rc.responseAcknowlegementMsg(); err != nil {
36 | log.Println("response acknowlegement msg failed to peer.")
37 | return
38 | }
39 | }
40 |
41 | return
42 | }
43 |
--------------------------------------------------------------------------------
/rtmp/co/send_msg.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "encoding/binary"
5 | "log"
6 | "seal/rtmp/pt"
7 |
8 | "github.com/calabashdad/utiltools"
9 | )
10 |
11 | func (rc *RtmpConn) sendMsg(msg *pt.Message) (err error) {
12 | defer func() {
13 | if err := recover(); err != nil {
14 | log.Println(utiltools.PanicTrace())
15 | }
16 | }()
17 |
18 | if nil == msg {
19 | return
20 | }
21 |
22 | // ensure the basic header is 1bytes. make simple.
23 | if msg.Header.PerferCsid < 2 {
24 | msg.Header.PerferCsid = pt.RtmpCidProtocolControl
25 | }
26 |
27 | // current position of payload send.
28 | var payloadOffset uint32
29 |
30 | // always write the header event payload is Empty.
31 | for {
32 | if payloadOffset >= msg.Header.PayloadLength {
33 | break
34 | }
35 |
36 | var headerOffset uint32
37 | var header [pt.RtmpMaxFmt0HeaderSize]uint8
38 |
39 | if 0 == payloadOffset {
40 | // write new chunk stream header, fmt is 0
41 | header[headerOffset] = 0x00 | uint8(msg.Header.PerferCsid&0x3f)
42 | headerOffset++
43 |
44 | // chunk message header, 11 bytes
45 | // timestamp, 3bytes, big-endian
46 | timestamp := msg.Header.Timestamp
47 | if timestamp < pt.RtmpExtendTimeStamp {
48 | header[headerOffset] = uint8((timestamp & 0x00ff0000) >> 16)
49 | headerOffset++
50 | header[headerOffset] = uint8((timestamp & 0x0000ff00) >> 8)
51 | headerOffset++
52 | header[headerOffset] = uint8(timestamp & 0x000000ff)
53 | headerOffset++
54 | } else {
55 | header[headerOffset] = 0xff
56 | headerOffset++
57 | header[headerOffset] = 0xff
58 | headerOffset++
59 | header[headerOffset] = 0xff
60 | headerOffset++
61 | }
62 |
63 | // message_length, 3bytes, big-endian
64 | payloadLengh := msg.Header.PayloadLength
65 | header[headerOffset] = uint8((payloadLengh & 0x00ff0000) >> 16)
66 | headerOffset++
67 | header[headerOffset] = uint8((payloadLengh & 0x0000ff00) >> 8)
68 | headerOffset++
69 | header[headerOffset] = uint8((payloadLengh & 0x000000ff))
70 | headerOffset++
71 |
72 | // message_type, 1bytes
73 | header[headerOffset] = msg.Header.MessageType
74 | headerOffset++
75 |
76 | // stream id, 4 bytes, little-endian
77 | binary.LittleEndian.PutUint32(header[headerOffset:headerOffset+4], msg.Header.StreamID)
78 | headerOffset += 4
79 |
80 | // chunk extended timestamp header, 0 or 4 bytes, big-endian
81 | if timestamp >= pt.RtmpExtendTimeStamp {
82 | binary.BigEndian.PutUint32(header[headerOffset:headerOffset+4], uint32(timestamp))
83 | headerOffset += 4
84 | }
85 |
86 | } else {
87 | // write no message header chunk stream, fmt is 3
88 | // @remark, if perfer_cid > 0x3F, that is, use 2B/3B chunk header,
89 | // rollback to 1B chunk header.
90 |
91 | // fmt is 3
92 | header[headerOffset] = 0xc0 | uint8(msg.Header.PerferCsid&0x3f)
93 | headerOffset++
94 |
95 | // chunk extended timestamp header, 0 or 4 bytes, big-endian
96 | // 6.1.3. Extended Timestamp
97 | // This field is transmitted only when the normal time stamp in the
98 | // chunk message header is set to 0x00ffffff. If normal time stamp is
99 | // set to any value less than 0x00ffffff, this field MUST NOT be
100 | // present. This field MUST NOT be present if the timestamp field is not
101 | // present. Type 3 chunks MUST NOT have this field.
102 | // adobe changed for Type3 chunk:
103 | // FMLE always sendout the extended-timestamp,
104 | // must send the extended-timestamp to FMS,
105 | // must send the extended-timestamp to flash-player.
106 | timestamp := msg.Header.Timestamp
107 | if timestamp >= pt.RtmpExtendTimeStamp {
108 | binary.BigEndian.PutUint32(header[headerOffset:headerOffset+4], uint32(timestamp))
109 | headerOffset += 4
110 | }
111 | }
112 |
113 | // not use writev method, we use net.Buffers to mock writev in c/c++, socket disconnected qickly
114 | if true {
115 | // send header
116 | if err = rc.tcpConn.SendBytes(header[:headerOffset]); err != nil {
117 | log.Println("send msg header failed.")
118 | return
119 | }
120 |
121 | //payload
122 | payloadSize := msg.Header.PayloadLength - payloadOffset
123 | if payloadSize > rc.outChunkSize {
124 | payloadSize = rc.outChunkSize
125 | }
126 |
127 | if err = rc.tcpConn.SendBytes(msg.Payload.Payload[payloadOffset : payloadOffset+payloadSize]); err != nil {
128 | log.Println("send msg payload failed.")
129 | return
130 | }
131 |
132 | payloadOffset += payloadSize
133 | }
134 | }
135 |
136 | return
137 | }
138 |
--------------------------------------------------------------------------------
/rtmp/co/send_pkt.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "log"
5 | "seal/rtmp/pt"
6 |
7 | "github.com/calabashdad/utiltools"
8 | )
9 |
10 | // sendPacket the function has call the message
11 | func (rc *RtmpConn) sendPacket(pkt pt.Packet, streamID uint32) (err error) {
12 | defer func() {
13 | if err := recover(); err != nil {
14 | log.Println(utiltools.PanicTrace())
15 | }
16 | }()
17 |
18 | var msg pt.Message
19 |
20 | msg.Payload.Payload = pkt.Encode()
21 | msg.Payload.SizeTmp = uint32(len(msg.Payload.Payload))
22 |
23 | if uint32(len(msg.Payload.Payload)) <= 0 {
24 | //ignore Empty msg.
25 | return
26 | }
27 |
28 | msg.Header.PayloadLength = uint32(len(msg.Payload.Payload))
29 | msg.Header.MessageType = pkt.GetMessageType()
30 | msg.Header.PerferCsid = pkt.GetPreferCsID()
31 | msg.Header.StreamID = streamID
32 |
33 | if err = rc.sendMsg(&msg); err != nil {
34 | return
35 | }
36 |
37 | return
38 | }
39 |
--------------------------------------------------------------------------------
/rtmp/co/source.go:
--------------------------------------------------------------------------------
1 | package co
2 |
3 | import (
4 | "log"
5 | "seal/conf"
6 | "seal/hls"
7 | "seal/rtmp/pt"
8 | "sync"
9 | )
10 |
11 | // stream data source hub
12 | type sourceHub struct {
13 | //key: app/streamName, e.g. rtmp://127.0.0.1/live/test, the key is [live/app]
14 | hub map[string]*SourceStream
15 | lock sync.RWMutex
16 | }
17 |
18 | var GlobalSources = &sourceHub{
19 | hub: make(map[string]*SourceStream),
20 | }
21 |
22 | // SourceStream rtmp stream data source
23 | type SourceStream struct {
24 | // the sample rate of audio in metadata
25 | SampleRate float64
26 | // the video frame rate in metadata
27 | FrameRate float64
28 | // Atc whether Atc(use absolute time and donot adjust time),
29 | // directly use msg time and donot adjust if Atc is true,
30 | // otherwise, adjust msg time to start from 0 to make flash happy.
31 | Atc bool
32 | // time jitter algrithem
33 | TimeJitter uint32
34 | // cached meta data
35 | CacheMetaData *pt.Message
36 | // cached video sequence header
37 | CacheVideoSequenceHeader *pt.Message
38 | // cached aideo sequence header
39 | CacheAudioSequenceHeader *pt.Message
40 |
41 | // consumers
42 | consumers map[*Consumer]interface{}
43 | // lock for consumers.
44 | consumerLock sync.RWMutex
45 |
46 | // gop cache
47 | GopCache *GopCache
48 |
49 | // hls stream
50 | hls *hls.SourceStream
51 | }
52 |
53 | func (s *SourceStream) CreateConsumer(c *Consumer) {
54 | if nil == c {
55 | log.Println("when registe consumer, nil == consumer")
56 | return
57 | }
58 |
59 | s.consumerLock.Lock()
60 | defer s.consumerLock.Unlock()
61 |
62 | s.consumers[c] = struct{}{}
63 | log.Println("a consumer created.consumer=", c)
64 |
65 | }
66 |
67 | func (s *SourceStream) DestroyConsumer(c *Consumer) {
68 | if nil == c {
69 | log.Println("when destroy consumer, nil == consummer")
70 | return
71 | }
72 |
73 | s.consumerLock.Lock()
74 | defer s.consumerLock.Unlock()
75 |
76 | delete(s.consumers, c)
77 | log.Println("a consumer destroyed.consumer=", c)
78 | }
79 |
80 | func (s *SourceStream) copyToAllConsumers(msg *pt.Message) {
81 |
82 | if nil == msg {
83 | return
84 | }
85 |
86 | s.consumerLock.Lock()
87 | defer s.consumerLock.Unlock()
88 |
89 | for k, v := range s.consumers {
90 | _ = v
91 | k.Enquene(msg, s.Atc, s.SampleRate, s.FrameRate, s.TimeJitter)
92 | }
93 | }
94 |
95 | func (s *sourceHub) findSourceToPublish(k string) *SourceStream {
96 |
97 | if 0 == len(k) {
98 | log.Println("find source to publish, nil == k")
99 | return nil
100 | }
101 |
102 | s.lock.Lock()
103 | defer s.lock.Unlock()
104 |
105 | if res := s.hub[k]; nil != res {
106 | log.Println("stream ", k, " can not publish, because has already publishing....")
107 | return nil
108 | }
109 |
110 | //can publish. new a source
111 | s.hub[k] = &SourceStream{
112 | TimeJitter: conf.GlobalConfInfo.Rtmp.TimeJitter,
113 | GopCache: &GopCache{},
114 | consumers: make(map[*Consumer]interface{}),
115 | }
116 |
117 | if "true" == conf.GlobalConfInfo.Hls.Enable {
118 | s.hub[k].hls = hls.NewSourceStream()
119 | } else {
120 | // make sure is nil when hls is closed
121 | s.hub[k].hls = nil
122 | }
123 |
124 | return s.hub[k]
125 | }
126 |
127 | func (s *sourceHub) FindSourceToPlay(k string) *SourceStream {
128 | s.lock.Lock()
129 | defer s.lock.Unlock()
130 |
131 | if res := s.hub[k]; nil != res {
132 | return res
133 | }
134 |
135 | log.Println("stream ", k, " can not play, because has not published.")
136 |
137 | return nil
138 | }
139 |
140 | func (s *sourceHub) deleteSource(key string) {
141 | s.lock.Lock()
142 | defer s.lock.Unlock()
143 |
144 | stream := s.hub[key]
145 | if nil != stream && nil != stream.hls {
146 | stream.hls.OnUnPublish()
147 | }
148 |
149 | delete(s.hub, key)
150 | }
151 |
--------------------------------------------------------------------------------
/rtmp/flv/flv_codec.go:
--------------------------------------------------------------------------------
1 | package flv
2 |
3 | import (
4 | "seal/rtmp/pt"
5 | )
6 |
7 | // AudioIsSequenceHeader judge audio is aac sequence header
8 | func AudioIsSequenceHeader(data []uint8) bool {
9 |
10 | if !audioIsAAC(data) {
11 | return false
12 | }
13 |
14 | if len(data) < 2 {
15 | return false
16 | }
17 |
18 | aacPacketType := data[1]
19 |
20 | return aacPacketType == pt.RtmpCodecAudioTypeSequenceHeader
21 | }
22 |
23 | func audioIsAAC(data []uint8) bool {
24 |
25 | if len(data) < 1 {
26 | return false
27 | }
28 |
29 | soundFormat := data[0]
30 | soundFormat = (soundFormat >> 4) & 0x0f
31 |
32 | return soundFormat == pt.RtmpCodecAudioAAC
33 | }
34 |
35 | // VideoIsH264 judge video is h264 sequence header
36 | func VideoIsH264(data []uint8) bool {
37 |
38 | if len(data) < 1 {
39 | return false
40 | }
41 |
42 | codecID := data[0]
43 | codecID &= 0x0f
44 |
45 | return pt.RtmpCodecVideoAVC == codecID
46 | }
47 |
48 | // VideoH264IsKeyframe judge video is h264 key frame
49 | func VideoH264IsKeyframe(data []uint8) bool {
50 | // 2bytes required.
51 | if len(data) < 2 {
52 | return false
53 | }
54 |
55 | frameType := data[0]
56 | frameType = (frameType >> 4) & 0x0F
57 |
58 | return frameType == pt.RtmpCodecVideoAVCFrameKeyFrame
59 | }
60 |
61 | // VideoH264IsKeyFrameAndSequenceHeader judge video is h264 sequence header and key frame
62 | // payload: 0x17 0x00
63 | func VideoH264IsKeyFrameAndSequenceHeader(data []uint8) bool {
64 | // sequence header only for h264
65 | if !VideoIsH264(data) {
66 | return false
67 | }
68 |
69 | // 2bytes required.
70 | if len(data) < 2 {
71 | return false
72 | }
73 |
74 | frameType := data[0]
75 | frameType = (frameType >> 4) & 0x0F
76 |
77 | avcPacketType := data[1]
78 |
79 | return frameType == pt.RtmpCodecVideoAVCFrameKeyFrame && avcPacketType == pt.RtmpCodecVideoAVCTypeSequenceHeader
80 | }
81 |
82 | // payload: 0x17 0x01
83 | func VideoH264IsKeyFrameAndAvcNalu(data []uint8) bool {
84 | if !VideoIsH264(data) {
85 | return false
86 | }
87 |
88 | // 2bytes required.
89 | if len(data) < 2 {
90 | return false
91 | }
92 |
93 | frameType := data[0]
94 | frameType = (frameType >> 4) & 0x0F
95 |
96 | avcPacketType := data[1]
97 |
98 | return frameType == pt.RtmpCodecVideoAVCFrameKeyFrame && avcPacketType == pt.RtmpCodecVideoAVCTypeNALU
99 | }
100 |
--------------------------------------------------------------------------------
/rtmp/pt/amf.go:
--------------------------------------------------------------------------------
1 | //Package pt is protocol for short. define the basic rtmp protocol consts
2 | package pt
3 |
4 | import (
5 | "encoding/binary"
6 | "fmt"
7 | "log"
8 | "math"
9 | )
10 |
11 | // Amf0Object amf0object
12 | type Amf0Object struct {
13 | propertyName string
14 | value interface{}
15 | valueType uint8 //just for help known type.
16 | }
17 |
18 | // NewAmf0Object create a new amf0Object
19 | func NewAmf0Object(propertyName string, value interface{}, valueType uint8) *Amf0Object {
20 | return &Amf0Object{
21 | propertyName: propertyName,
22 | value: value,
23 | valueType: valueType,
24 | }
25 | }
26 |
27 | type amf0EcmaArray struct {
28 | count uint32
29 | anyObject []Amf0Object
30 | }
31 |
32 | type amf0StrictArray struct {
33 | count uint32
34 | anyObject []interface{}
35 | }
36 |
37 | func (array *amf0EcmaArray) addObject(obj Amf0Object) {
38 | array.anyObject = append(array.anyObject, obj)
39 | array.count++
40 | }
41 |
42 | // this function do not affect the offset parsed in data.
43 | func amf0ObjectEOF(data []uint8, offset *uint32) (res bool) {
44 | if len(data) < 3 {
45 | res = false
46 | return
47 | }
48 |
49 | if 0x00 == data[*offset] &&
50 | 0x00 == data[*offset+1] &&
51 | RtmpAmf0ObjectEnd == data[*offset+2] {
52 | res = true
53 | *offset += 3
54 | } else {
55 | res = false
56 | }
57 |
58 | return
59 | }
60 |
61 | func amf0ReadUtf8(data []uint8, offset *uint32) (value string, err error) {
62 | if (uint32(len(data)) - *offset) < 2 {
63 | err = fmt.Errorf("Amf0ReadUtf8: 1, data len is not enough")
64 | return
65 | }
66 |
67 | dataLen := binary.BigEndian.Uint16(data[*offset : *offset+2])
68 | *offset += 2
69 |
70 | if (uint32(len(data)) - *offset) < uint32(dataLen) {
71 | err = fmt.Errorf("Amf0ReadUtf8: 2, data len is not enough")
72 | return
73 | }
74 |
75 | if 0 == dataLen {
76 | return
77 | }
78 |
79 | value = string(data[*offset : *offset+uint32(dataLen)])
80 | *offset += uint32(dataLen)
81 |
82 | return
83 | }
84 |
85 | func amf0WriteUtf8(value string) (data []uint8) {
86 |
87 | data = make([]uint8, 2+len(value))
88 |
89 | var offset uint32
90 |
91 | binary.BigEndian.PutUint16(data[offset:offset+2], uint16(len(value)))
92 | offset += 2
93 |
94 | copy(data[offset:], value)
95 | offset += uint32(len(value))
96 |
97 | return
98 | }
99 |
100 | func amf0ReadAny(data []uint8, marker *uint8, offset *uint32) (value interface{}, err error) {
101 |
102 | if amf0ObjectEOF(data, offset) {
103 | return
104 | }
105 |
106 | if (uint32(len(data)) - *offset) < 1 {
107 | err = fmt.Errorf("Amf0ReadAny: 0, data len is not enough")
108 | return
109 | }
110 |
111 | *marker = data[*offset]
112 |
113 | switch *marker {
114 | case RtmpAmf0String:
115 | value, err = Amf0ReadString(data, offset)
116 | case RtmpAmf0Boolean:
117 | value, err = amf0ReadBool(data, offset)
118 | case RtmpAmf0Number:
119 | value, err = Amf0ReadNumber(data, offset)
120 | case RtmpAMF0Null:
121 | err = amf0ReadNull(data, offset)
122 | case RtmpAmf0Undefined:
123 | err = amf0ReadUndefined(data, offset)
124 | case RtmpAmf0Object:
125 | value, err = amf0ReadObject(data, offset)
126 | case RtmpAmf0LongString:
127 | value, err = amf0ReadLongString(data, offset)
128 | case RtmpAmf0EcmaArray:
129 | value, err = amf0ReadEcmaArray(data, offset)
130 | case RtmpAmf0StrictArray:
131 | value, err = amf0ReadStrictArray(data, offset)
132 | default:
133 | err = fmt.Errorf("Amf0ReadAny: unknown marker Value, marker=%d", marker)
134 | }
135 |
136 | if err != nil {
137 | return
138 | }
139 |
140 | return
141 | }
142 |
143 | func amf0WriteAny(any Amf0Object) (data []uint8) {
144 | switch any.valueType {
145 | case RtmpAmf0String:
146 | data = amf0WriteString(any.value.(string))
147 | case RtmpAmf0Boolean:
148 | data = amf0WriteBool(any.value.(bool))
149 | case RtmpAmf0Number:
150 | data = amf0WriteNumber(any.value.(float64))
151 | case RtmpAMF0Null:
152 | data = amf0WriteNull()
153 | case RtmpAmf0Undefined:
154 | data = amf0WriteUndefined()
155 | case RtmpAmf0Object:
156 | data = amf0WriteObject(any.value.([]Amf0Object))
157 | case RtmpAmf0LongString:
158 | data = amf0WriteLongString(any.value.(string))
159 | case RtmpAmf0EcmaArray:
160 | data = amf0WriteEcmaArray(any.value.(amf0EcmaArray))
161 | case RtmpAmf0StrictArray:
162 | data = amf0WriteStrictArray(any.value.([]Amf0Object))
163 | default:
164 | log.Println("Amf0WriteAny: unknown type.", any.valueType)
165 | }
166 | return
167 | }
168 |
169 | // Amf0ReadString read amf0 string
170 | func Amf0ReadString(data []uint8, offset *uint32) (value string, err error) {
171 |
172 | if (uint32(len(data)) - *offset) < 1 {
173 | err = fmt.Errorf("Amf0ReadString: 0, data len is not enough")
174 | return
175 | }
176 |
177 | marker := data[*offset]
178 | *offset++
179 |
180 | if RtmpAmf0String != marker {
181 | err = fmt.Errorf("Amf0ReadString: RTMP_AMF0_String != marker")
182 | return
183 | }
184 |
185 | value, err = amf0ReadUtf8(data, offset)
186 |
187 | return
188 | }
189 |
190 | func amf0WriteString(value string) (data []uint8) {
191 |
192 | data = append(data, RtmpAmf0String)
193 | data = append(data, amf0WriteUtf8(value)...)
194 |
195 | return
196 | }
197 |
198 | // Amf0ReadNumber read amf0 number
199 | func Amf0ReadNumber(data []uint8, offset *uint32) (value float64, err error) {
200 |
201 | if (uint32(len(data)) - *offset) < 1 {
202 | err = fmt.Errorf("Amf0ReadNumber: 0, data len is not enough")
203 | return
204 | }
205 |
206 | marker := data[*offset]
207 | *offset++
208 |
209 | if RtmpAmf0Number != marker {
210 | err = fmt.Errorf("Amf0ReadNumber: RTMP_AMF0_Number != marker")
211 | return
212 | }
213 |
214 | if (uint32(len(data)) - *offset) < 8 {
215 | err = fmt.Errorf("Amf0ReadNumber: 1, data len is not enough")
216 | return
217 | }
218 |
219 | valueTmp := binary.BigEndian.Uint64(data[*offset : *offset+8])
220 | *offset += 8
221 |
222 | value = math.Float64frombits(valueTmp)
223 |
224 | return
225 | }
226 |
227 | func amf0WriteNumber(value float64) (data []uint8) {
228 | data = make([]uint8, 1+8)
229 |
230 | var offset uint32
231 |
232 | data[offset] = RtmpAmf0Number
233 | offset++
234 |
235 | v2 := math.Float64bits(value)
236 | binary.BigEndian.PutUint64(data[offset:offset+8], v2)
237 | offset += 8
238 |
239 | return
240 | }
241 |
242 | func amf0ReadObject(data []uint8, offset *uint32) (amf0objects []Amf0Object, err error) {
243 |
244 | if (uint32(len(data)) - *offset) < 1 {
245 | err = fmt.Errorf("Amf0ReadObject: 0, data len is not enough")
246 | return
247 | }
248 |
249 | marker := data[*offset]
250 | *offset++
251 |
252 | if RtmpAmf0Object != marker {
253 | err = fmt.Errorf("error: Amf0ReadObject:RTMP_AMF0_Object != marker")
254 | return
255 | }
256 |
257 | for {
258 | if *offset >= uint32(len(data)) {
259 | break
260 | }
261 |
262 | if amf0ObjectEOF(data, offset) {
263 | break
264 | }
265 |
266 | var amf0object Amf0Object
267 |
268 | amf0object.propertyName, err = amf0ReadUtf8(data, offset)
269 | if err != nil {
270 | break
271 | }
272 |
273 | amf0object.value, err = amf0ReadAny(data, &amf0object.valueType, offset)
274 | if err != nil {
275 | break
276 | }
277 |
278 | amf0objects = append(amf0objects, amf0object)
279 | }
280 |
281 | return
282 | }
283 |
284 | func amf0WriteObject(amf0objects []Amf0Object) (data []uint8) {
285 |
286 | data = append(data, RtmpAmf0Object)
287 |
288 | for _, v := range amf0objects {
289 | data = append(data, amf0WriteUtf8(v.propertyName)...)
290 | data = append(data, amf0WriteAny(v)...)
291 | }
292 |
293 | data = append(data, 0x00, 0x00, 0x09)
294 |
295 | return
296 | }
297 |
298 | func amf0ReadBool(data []uint8, offset *uint32) (value bool, err error) {
299 | if (uint32(len(data)) - *offset) < 1 {
300 | err = fmt.Errorf("Amf0ReadBool: 0, data len is not enough")
301 | return
302 | }
303 |
304 | marker := data[*offset]
305 | *offset++
306 |
307 | if RtmpAmf0Boolean != marker {
308 | err = fmt.Errorf("Amf0ReadBool: RTMP_AMF0_Boolean != marker")
309 | return
310 | }
311 |
312 | if (uint32(len(data)) - *offset) < 1 {
313 | err = fmt.Errorf("Amf0ReadBool: 1, data len is not enough")
314 | return
315 | }
316 |
317 | v := data[*offset]
318 | *offset++
319 |
320 | if v != 0 {
321 | value = true
322 | } else {
323 | value = false
324 | }
325 |
326 | return
327 | }
328 |
329 | func amf0WriteBool(value bool) (data []uint8) {
330 |
331 | data = make([]uint8, 1+1)
332 | data[0] = RtmpAmf0Boolean
333 | if value {
334 | data[1] = 1
335 | } else {
336 | data[1] = 0
337 | }
338 |
339 | return
340 | }
341 |
342 | func amf0ReadNull(data []uint8, offset *uint32) (err error) {
343 | if (uint32(len(data)) - *offset) < 1 {
344 | err = fmt.Errorf("Amf0ReadNull: 0, data len is not enough")
345 | return
346 | }
347 |
348 | marker := data[*offset]
349 | *offset++
350 |
351 | if RtmpAMF0Null != marker {
352 | err = fmt.Errorf("Amf0ReadNull: RTMP_AMF0_Null != marker")
353 | return
354 | }
355 |
356 | return
357 | }
358 |
359 | func amf0WriteNull() (data []uint8) {
360 |
361 | data = append(data, RtmpAMF0Null)
362 |
363 | return
364 | }
365 |
366 | func amf0ReadUndefined(data []uint8, offset *uint32) (err error) {
367 |
368 | if (uint32(len(data)) - *offset) < 1 {
369 | err = fmt.Errorf("Amf0ReadUndefined: 0, data len is not enough")
370 | return
371 | }
372 |
373 | marker := data[*offset]
374 | *offset++
375 |
376 | if RtmpAmf0Undefined != marker {
377 | err = fmt.Errorf("Amf0ReadUndefined: RTMP_AMF0_Undefined != marker")
378 | return
379 | }
380 |
381 | return
382 | }
383 |
384 | func amf0WriteUndefined() (data []uint8) {
385 |
386 | data = append(data, RtmpAmf0Undefined)
387 |
388 | return
389 | }
390 |
391 | func amf0ReadLongString(data []uint8, offset *uint32) (value string, err error) {
392 |
393 | if (uint32(len(data)) - *offset) < 1 {
394 | err = fmt.Errorf("Amf0ReadLongString: 0, data len is not enough")
395 | return
396 | }
397 |
398 | marker := data[*offset]
399 | *offset++
400 |
401 | if RtmpAmf0LongString != marker {
402 | err = fmt.Errorf("Amf0ReadLongString: RTMP_AMF0_LongString != marker")
403 | return
404 | }
405 |
406 | if (uint32(len(data)) - *offset) < 4 {
407 | err = fmt.Errorf("Amf0ReadLongString: 1, data len is not enough")
408 |
409 | return
410 | }
411 |
412 | dataLen := binary.BigEndian.Uint32(data[*offset : *offset+4])
413 | *offset += 4
414 | if dataLen <= 0 {
415 | err = fmt.Errorf("Amf0ReadLongString: data len is <= 0, dataLen=%d", dataLen)
416 | return
417 | }
418 |
419 | if (uint32(len(data)) - *offset) < dataLen {
420 | err = fmt.Errorf("Amf0ReadLongString: 2, data len is not enough")
421 | return
422 | }
423 |
424 | value = string(data[*offset : *offset+dataLen])
425 | *offset += dataLen
426 |
427 | return
428 | }
429 |
430 | func amf0WriteLongString(value string) (data []uint8) {
431 |
432 | data = make([]uint8, 1+4+len(value))
433 |
434 | var offset uint32
435 |
436 | data[offset] = RtmpAmf0LongString
437 | offset++
438 |
439 | dataLen := len(value)
440 | binary.BigEndian.PutUint32(data[offset:offset+4], uint32(dataLen))
441 | offset += 4
442 |
443 | copy(data[offset:], value)
444 | offset += uint32(dataLen)
445 |
446 | return
447 | }
448 |
449 | func amf0ReadEcmaArray(data []uint8, offset *uint32) (value amf0EcmaArray, err error) {
450 |
451 | if (uint32(len(data)) - *offset) < 1 {
452 | err = fmt.Errorf("Amf0ReadEcmaArray: 0, data len is not enough")
453 | return
454 | }
455 |
456 | marker := data[*offset]
457 | *offset++
458 |
459 | if RtmpAmf0EcmaArray != marker {
460 | err = fmt.Errorf("error: Amf0ReadEcmaArray: RTMP_AMF0_EcmaArray != marker")
461 | return
462 | }
463 |
464 | if (uint32(len(data)) - *offset) < 4 {
465 | err = fmt.Errorf("Amf0ReadEcmaArray: 1, data len is not enough")
466 | return
467 | }
468 |
469 | value.count = binary.BigEndian.Uint32(data[*offset : *offset+4])
470 | *offset += 4
471 |
472 | for {
473 | if *offset >= uint32(len(data)) {
474 | break
475 | }
476 |
477 | if amf0ObjectEOF(data, offset) {
478 | break
479 | }
480 |
481 | var amf Amf0Object
482 | amf.propertyName, err = amf0ReadUtf8(data, offset)
483 | if err != nil {
484 | break
485 | }
486 |
487 | amf.value, err = amf0ReadAny(data, &amf.valueType, offset)
488 | if err != nil {
489 | break
490 | }
491 |
492 | value.anyObject = append(value.anyObject, amf)
493 | }
494 |
495 | return
496 | }
497 |
498 | func amf0WriteEcmaArray(arr amf0EcmaArray) (data []uint8) {
499 | data = make([]uint8, 1+4)
500 |
501 | var offset uint32
502 |
503 | data[offset] = RtmpAmf0EcmaArray
504 | offset++
505 |
506 | binary.BigEndian.PutUint32(data[offset:offset+4], uint32(arr.count))
507 | offset += 4
508 |
509 | for _, v := range arr.anyObject {
510 | data = append(data, amf0WriteUtf8(v.propertyName)...)
511 | data = append(data, amf0WriteAny(v)...)
512 | }
513 |
514 | //eof
515 | data = append(data, 0x00, 0x00, 0x09)
516 |
517 | return
518 | }
519 |
520 | func amf0ReadStrictArray(data []uint8, offset *uint32) (value amf0StrictArray, err error) {
521 | if (uint32(len(data)) - *offset) < 1 {
522 | err = fmt.Errorf("Amf0ReadStrictArray: 0, data len is not enough")
523 | return
524 | }
525 |
526 | marker := data[*offset]
527 | *offset++
528 |
529 | if RtmpAmf0StrictArray != marker {
530 | err = fmt.Errorf("Amf0ReadStrictArray: error: RTMP_AMF0_StrictArray != marker")
531 | return
532 | }
533 |
534 | if (uint32(len(data)) - *offset) < 4 {
535 | err = fmt.Errorf("Amf0ReadStrictArray: 1, data len is not enough")
536 | return
537 | }
538 |
539 | value.count = binary.BigEndian.Uint32(data[*offset : *offset+4])
540 | *offset += 4
541 |
542 | for i := 0; uint32(i) < value.count; i++ {
543 | if *offset >= uint32(len(data)) {
544 | break
545 | }
546 |
547 | var obj interface{}
548 |
549 | var markerLocal uint8
550 | obj, err = amf0ReadAny(data, &markerLocal, offset)
551 | if err != nil {
552 | break
553 | }
554 |
555 | value.anyObject = append(value.anyObject, obj)
556 | }
557 |
558 | return
559 | }
560 |
561 | func amf0WriteStrictArray(objs []Amf0Object) (data []uint8) {
562 | data = make([]uint8, 1+4)
563 |
564 | var offset uint32
565 |
566 | data[offset] = RtmpAmf0StrictArray
567 | offset++
568 |
569 | count := len(objs)
570 | binary.BigEndian.PutUint32(data[offset:offset+4], uint32(count))
571 | offset += 4
572 |
573 | for _, v := range objs {
574 | data = append(data, amf0WriteAny(v)...)
575 | }
576 |
577 | //eof
578 | data = append(data, 0x00, 0x00, 0x09)
579 |
580 | return
581 | }
582 |
583 | func amf0Discovery(data []uint8, offset *uint32) (value interface{}, marker uint8, err error) {
584 |
585 | if amf0ObjectEOF(data, offset) {
586 | return
587 | }
588 |
589 | if (uint32(len(data)) - *offset) < 1 {
590 | err = fmt.Errorf("Amf0Discovery: 0, data len is not enough")
591 | return
592 | }
593 |
594 | marker = data[*offset]
595 |
596 | switch marker {
597 | case RtmpAmf0String:
598 | value, err = Amf0ReadString(data, offset)
599 | case RtmpAmf0Boolean:
600 | value, err = amf0ReadBool(data, offset)
601 | case RtmpAmf0Number:
602 | value, err = Amf0ReadNumber(data, offset)
603 | case RtmpAMF0Null:
604 | err = amf0ReadNull(data, offset)
605 | case RtmpAmf0Undefined:
606 | err = amf0ReadUndefined(data, offset)
607 | case RtmpAmf0Object:
608 | value, err = amf0ReadObject(data, offset)
609 | case RtmpAmf0LongString:
610 | value, err = amf0ReadLongString(data, offset)
611 | case RtmpAmf0EcmaArray:
612 | value, err = amf0ReadEcmaArray(data, offset)
613 | case RtmpAmf0StrictArray:
614 | value, err = amf0ReadStrictArray(data, offset)
615 | default:
616 | err = fmt.Errorf("Amf0Discovery: unknown marker type, marker=%d", marker)
617 | }
618 |
619 | if err != nil {
620 | return
621 | }
622 |
623 | return
624 | }
625 |
--------------------------------------------------------------------------------
/rtmp/pt/chunk.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | // ChunkStream incoming chunk stream maybe interlaced,
4 | // use the chunk stream to cache the input RTMP chunk streams.
5 | type ChunkStream struct {
6 | /**
7 | * represents the basic header fmt,
8 | * which used to identify the variant message header type.
9 | */
10 | Fmt uint8
11 | /**
12 | * represents the basic header cs_id,
13 | * which is the chunk stream id.
14 | */
15 | CsID uint32
16 | /**
17 | * cached message header
18 | */
19 | MsgHeader MessageHeader
20 | /**
21 | * whether the chunk message header has extended timestamp.
22 | */
23 | HasExtendedTimestamp bool
24 | /**
25 | * decoded msg count, to identify whether the chunk stream is fresh.
26 | */
27 | MsgCount uint64
28 | }
29 |
--------------------------------------------------------------------------------
/rtmp/pt/handshake.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | import (
4 | "bytes"
5 | "crypto/hmac"
6 | "crypto/rand"
7 | "crypto/sha256"
8 | "encoding/binary"
9 | "fmt"
10 | "time"
11 | )
12 |
13 | var (
14 | handshakeClientFullKey = []uint8{
15 | 'G', 'e', 'n', 'u', 'i', 'n', 'e', ' ', 'A', 'd', 'o', 'b', 'e', ' ',
16 | 'F', 'l', 'a', 's', 'h', ' ', 'P', 'l', 'a', 'y', 'e', 'r', ' ',
17 | '0', '0', '1',
18 | 0xF0, 0xEE, 0xC2, 0x4A, 0x80, 0x68, 0xBE, 0xE8, 0x2E, 0x00, 0xD0, 0xD1,
19 | 0x02, 0x9E, 0x7E, 0x57, 0x6E, 0xEC, 0x5D, 0x2D, 0x29, 0x80, 0x6F, 0xAB,
20 | 0x93, 0xB8, 0xE6, 0x36, 0xCF, 0xEB, 0x31, 0xAE,
21 | }
22 | handshakeServerFullKey = []uint8{
23 | 'G', 'e', 'n', 'u', 'i', 'n', 'e', ' ', 'A', 'd', 'o', 'b', 'e', ' ',
24 | 'F', 'l', 'a', 's', 'h', ' ', 'M', 'e', 'd', 'i', 'a', ' ',
25 | 'S', 'e', 'r', 'v', 'e', 'r', ' ',
26 | '0', '0', '1',
27 | 0xF0, 0xEE, 0xC2, 0x4A, 0x80, 0x68, 0xBE, 0xE8, 0x2E, 0x00, 0xD0, 0xD1,
28 | 0x02, 0x9E, 0x7E, 0x57, 0x6E, 0xEC, 0x5D, 0x2D, 0x29, 0x80, 0x6F, 0xAB,
29 | 0x93, 0xB8, 0xE6, 0x36, 0xCF, 0xEB, 0x31, 0xAE,
30 | }
31 |
32 | handshakeClientPartialKey = handshakeClientFullKey[:30]
33 | handshakeServerPartialKey = handshakeServerFullKey[:36]
34 | )
35 |
36 | // ComplexHandShake complex handshake method
37 | func ComplexHandShake(c1 []uint8, s0 []uint8, s1 []uint8, s2 []uint8) (err error) {
38 |
39 | clientTime := binary.BigEndian.Uint32(c1[0:4])
40 | clientVer := binary.BigEndian.Uint32(c1[4:8])
41 | _ = clientTime
42 |
43 | _ = clientVer
44 |
45 | //use digest-key scheme.
46 | c1Digest764 := c1[8 : 8+764]
47 | var serverDigestForS2 []uint8
48 | if ok, digest := isDigestKeyScheme(c1, c1Digest764); !ok {
49 | //failed try key-digest scheme
50 | c1Digest764_2 := c1[8+764 : 8+764+764]
51 | if ok2, digest2 := isKeyDigestScheme(c1, c1Digest764_2); !ok2 {
52 | err = fmt.Errorf("ComplexHandShake verify both digest-key scheme and key-digest failed")
53 | } else {
54 | serverDigestForS2 = digest2
55 | }
56 |
57 | if nil != err {
58 | return
59 | }
60 |
61 | } else {
62 | serverDigestForS2 = digest
63 | }
64 |
65 | //create s0
66 | s0[0] = 3
67 |
68 | //create s1
69 | createS1(s1, handshakeServerPartialKey)
70 |
71 | //create s2.
72 | createS2(s2, serverDigestForS2)
73 |
74 | return
75 | }
76 |
77 | func createS1(s1 []uint8, key []uint8) {
78 |
79 | //create s1. time(4B) version(4B) [digest]{random} [key]{random}
80 | var offset uint32
81 |
82 | serverTime := uint32(time.Now().Unix())
83 | serverVer := uint32(0x0a0b0c0d)
84 | binary.BigEndian.PutUint32(s1[offset:offset+4], serverTime)
85 | offset += 4
86 | binary.BigEndian.PutUint32(s1[offset:offset+4], serverVer)
87 | offset += 4
88 | //use digest-key scheme.
89 |
90 | var randomDataoffset uint32
91 | for {
92 | rand.Read(s1[offset:]) // time(4B)server version(4B)
93 | randomDataoffset = uint32(s1[offset]) + uint32(s1[offset+1]) + uint32(s1[offset+2]) + uint32(s1[offset+3])
94 | if randomDataoffset > 0 && randomDataoffset < 728 {
95 | offset += 4
96 | break
97 | } else {
98 | randomDataoffset = 0
99 | }
100 | }
101 |
102 | digestLoc := offset + randomDataoffset //time(4B) version(4B) + digest[offset(4B) + random1(offset B) + digest + random2] + key[]
103 |
104 | h := hmac.New(sha256.New, handshakeServerPartialKey)
105 | h.Write(s1[:digestLoc])
106 | h.Write(s1[digestLoc+32:])
107 | digestData := h.Sum(nil)
108 | copy(s1[digestLoc:], digestData)
109 |
110 | return
111 | }
112 |
113 | func createS2(s2 []uint8, key []uint8) {
114 | // 1536bytes c2s2. c2 and s2 has the same structure.
115 | //random-data: 1504bytes
116 | //digest-data: 32bytes
117 | rand.Read(s2[:])
118 | h := hmac.New(sha256.New, key)
119 | h.Write(s2[:len(s2)-32])
120 | digestS2 := h.Sum(nil)
121 | copy(s2[len(s2)-32:], digestS2)
122 |
123 | return
124 | }
125 |
126 | // just for c1 or s1
127 | func isDigestKeyScheme(buf []uint8, c1Digest764 []uint8) (ok bool, digest []uint8) {
128 |
129 | // 764bytes digest
130 | // offset: 4bytes (u[0] + u[1] + u[2] + u[3])
131 | // random-data: (offset)bytes
132 | // digest-data: 32bytes
133 | // random-data: (764-4-offset-32)bytes
134 |
135 | // 764bytes key
136 | // random-data: (offset)bytes
137 | // key-data: 128bytes
138 | // random-data: (764-offset-128-4)bytes
139 | // offset: 4bytes
140 |
141 | var digestOffset uint32
142 | for i := 0; i < 4; i++ {
143 | digestOffset += uint32(c1Digest764[i])
144 | }
145 |
146 | if digestOffset > (764 - 32) {
147 | ok = false
148 | return
149 | }
150 |
151 | digestLoc := 4 + digestOffset
152 | digestData := c1Digest764[digestLoc : digestLoc+32]
153 |
154 | // part1 and part2 is divided by digest data of c1 or s1.
155 | part1 := buf[:8+digestLoc]
156 | part2 := buf[8+digestLoc+32:]
157 |
158 | h := hmac.New(sha256.New, handshakeClientPartialKey)
159 | h.Write(part1)
160 | h.Write(part2)
161 | calcDigestData := h.Sum(nil)
162 |
163 | if 0 == bytes.Compare(digestData, calcDigestData) {
164 | ok = true
165 | h := hmac.New(sha256.New, handshakeServerFullKey)
166 | h.Write(digestData)
167 | digest = h.Sum(nil)
168 | } else {
169 | ok = false
170 | }
171 |
172 | return
173 | }
174 |
175 | func isKeyDigestScheme(buf []uint8, c1Digest764 []uint8) (ok bool, digest []uint8) {
176 | // 764bytes key
177 | // random-data: (offset)bytes
178 | // key-data: 128bytes
179 | // random-data: (764-offset-128-4)bytes
180 | // offset: 4bytes
181 |
182 | // 764bytes digest
183 | // offset: 4bytes (u[0] + u[1] + u[2] + u[3])
184 | // random-data: (offset)bytes
185 | // digest-data: 32bytes
186 | // random-data: (764-4-offset-32)bytes
187 |
188 | var digestOffset uint32
189 | for i := 0; i < 4; i++ {
190 | digestOffset += uint32(c1Digest764[i])
191 | }
192 |
193 | if digestOffset > (764 - 32) {
194 | ok = false
195 | return
196 | }
197 |
198 | digestLoc := 4 + digestOffset
199 | digestData := c1Digest764[digestLoc : digestLoc+32]
200 |
201 | // part1 and part2 is divided by digest data of c1 or s1.
202 | part1 := buf[:8+764+digestLoc]
203 | part2 := buf[8+764+digestLoc+32:]
204 |
205 | h := hmac.New(sha256.New, handshakeClientPartialKey)
206 | h.Write(part1)
207 | h.Write(part2)
208 | calcDigestData := h.Sum(nil)
209 |
210 | if 0 == bytes.Compare(digestData, calcDigestData) {
211 | ok = true
212 | h := hmac.New(sha256.New, handshakeServerFullKey)
213 | h.Write(digestData)
214 | digest = h.Sum(nil)
215 | } else {
216 | ok = false
217 | }
218 |
219 | return
220 | }
221 |
--------------------------------------------------------------------------------
/rtmp/pt/message.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | // MessageHeader message header.
4 | type MessageHeader struct {
5 | // 3bytes.
6 | // Three-byte field that contains a timestamp delta of the message.
7 | // The 4 bytes are packed in the big-endian order.
8 | // only used for decoding message from chunk stream.
9 | TimestampDelta uint32
10 |
11 | // 3bytes.
12 | // Three-byte field that represents the size of the payload in bytes.
13 | // It is set in big-endian format.
14 | PayloadLength uint32
15 |
16 | // 1byte.
17 | // One byte field to represent the message type. A range of type IDs
18 | // (1-7) are reserved for protocol control messages.
19 | MessageType uint8
20 |
21 | // 4bytes.
22 | // Four-byte field that identifies the stream of the message. These
23 | // bytes are set in big-endian format.
24 | StreamID uint32
25 |
26 | // Four-byte field that contains a Timestamp of the message.
27 | // The 4 bytes are packed in the big-endian order.
28 | // @remark, used as calc Timestamp when decode and encode time.
29 | // @remark, we use 64bits for large time for jitter detect and hls.
30 | Timestamp uint64
31 |
32 | // get the perfered cid(chunk stream id) which sendout over.
33 | // set at decoding, and canbe used for directly send message,
34 | // for example, dispatch to all connections.
35 | PerferCsid uint32
36 | }
37 |
38 | // IsAudio .
39 | func (h *MessageHeader) IsAudio() bool {
40 | return RtmpMsgAudioMessage == h.MessageType
41 | }
42 |
43 | // IsVideo .
44 | func (h *MessageHeader) IsVideo() bool {
45 | return RtmpMsgVideoMessage == h.MessageType
46 | }
47 |
48 | // IsAmf0Command .
49 | func (h *MessageHeader) IsAmf0Command() bool {
50 | return RtmpMsgAmf0CommandMessage == h.MessageType
51 | }
52 |
53 | // IsAmf0Data .
54 | func (h *MessageHeader) IsAmf0Data() bool {
55 | return RtmpMsgAmf0DataMessage == h.MessageType
56 | }
57 |
58 | // IsAmf3Command .
59 | func (h *MessageHeader) IsAmf3Command() bool {
60 | return RtmpMsgAmf3CommandMessage == h.MessageType
61 | }
62 |
63 | // IsAmf3Data .
64 | func (h *MessageHeader) IsAmf3Data() bool {
65 | return RtmpMsgAmf3DataMessage == h.MessageType
66 | }
67 |
68 | // IsWindowAckledgementSize .
69 | func (h *MessageHeader) IsWindowAckledgementSize() bool {
70 | return RtmpMsgWindowAcknowledgementSize == h.MessageType
71 | }
72 |
73 | // IsAckledgement .
74 | func (h *MessageHeader) IsAckledgement() bool {
75 | return RtmpMsgAcknowledgement == h.MessageType
76 | }
77 |
78 | // IsSetChunkSize .
79 | func (h *MessageHeader) IsSetChunkSize() bool {
80 | return RtmpMsgSetChunkSize == h.MessageType
81 | }
82 |
83 | // IsUserControlMessage .
84 | func (h *MessageHeader) IsUserControlMessage() bool {
85 | return RtmpMsgUserControlMessage == h.MessageType
86 | }
87 |
88 | // IsSetPeerBandwidth .
89 | func (h *MessageHeader) IsSetPeerBandwidth() bool {
90 | return RtmpMsgSetPeerBandwidth == h.MessageType
91 | }
92 |
93 | // IsAggregate .
94 | func (h *MessageHeader) IsAggregate() bool {
95 | return RtmpMsgAggregateMessage == h.MessageType
96 | }
97 |
98 | // InitializeAmf0Script create a amf0 script header, set the size and stream_id.
99 | func (h *MessageHeader) InitializeAmf0Script(payloadLen uint32, streamID uint32) {
100 | h.MessageType = RtmpMsgAmf0DataMessage
101 | h.PayloadLength = payloadLen
102 | h.TimestampDelta = 0
103 | h.Timestamp = 0
104 | h.StreamID = streamID
105 |
106 | // amf0 script use connection2 chunk-id
107 | h.PerferCsid = RtmpCidOverConnection2
108 | }
109 |
110 | // InitializeAudio create a audio header, set the size, timestamp and stream_id.
111 | func (h *MessageHeader) InitializeAudio(payloadSize uint32, time uint32, streamID uint32) {
112 | h.MessageType = RtmpMsgAudioMessage
113 | h.PayloadLength = payloadSize
114 | h.TimestampDelta = time
115 | h.Timestamp = uint64(time)
116 | h.StreamID = streamID
117 |
118 | // audio chunk-id
119 | h.PerferCsid = RtmpCidAudio
120 | }
121 |
122 | // InitializeVideo create a video header, set the size, timestamp and stream_id.
123 | func (h *MessageHeader) InitializeVideo(payloadSize uint32, time uint32, streamID uint32) {
124 | h.MessageType = RtmpMsgVideoMessage
125 | h.PayloadLength = payloadSize
126 | h.TimestampDelta = time
127 | h.Timestamp = uint64(time)
128 | h.StreamID = streamID
129 |
130 | // video chunk-id
131 | h.PerferCsid = RtmpCidVideo
132 | }
133 |
134 | // MessagePayload message payload
135 | type MessagePayload struct {
136 | // current message parsed SizeTmp,
137 | // SizeTmp <= header.payload_length
138 | // for the payload maybe sent in multiple chunks.
139 | // when finish recv whole msg, it will be reset to 0.
140 | SizeTmp uint32
141 |
142 | // the Payload of message, can not know about the detail of Payload,
143 | // user must use decode_message to get concrete packet.
144 | // not all message Payload can be decoded to packet. for example,
145 | // video/audio packet use raw bytes, no video/audio packet.
146 | Payload []uint8
147 | }
148 |
149 | // Message message is raw data RTMP message, bytes oriented
150 | type Message struct {
151 | Header MessageHeader
152 | Payload MessagePayload
153 | }
154 |
--------------------------------------------------------------------------------
/rtmp/pt/packet.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | //Packet is the interface of the packet.
4 | type Packet interface {
5 | Decode([]uint8) error
6 | Encode() []uint8
7 | GetMessageType() uint8
8 | GetPreferCsID() uint32
9 | }
10 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_acknowlegement.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | import (
4 | "encoding/binary"
5 | )
6 |
7 | // AcknowlegementPacket The client or the server sends the acknowledgment to the peer after
8 | // receiving bytes equal to the window size.
9 | type AcknowlegementPacket struct {
10 | SequenceNumber uint32
11 | }
12 |
13 | // Decode .
14 | func (pkt *AcknowlegementPacket) Decode(data []uint8) (err error) {
15 | //nothing
16 | return
17 | }
18 |
19 | // Encode .
20 | func (pkt *AcknowlegementPacket) Encode() (data []uint8) {
21 |
22 | data = make([]uint8, 4)
23 | binary.BigEndian.PutUint32(data[:], pkt.SequenceNumber)
24 |
25 | return
26 | }
27 |
28 | // GetMessageType .
29 | func (pkt *AcknowlegementPacket) GetMessageType() uint8 {
30 | return RtmpMsgAcknowledgement
31 | }
32 |
33 | // GetPreferCsID .
34 | func (pkt *AcknowlegementPacket) GetPreferCsID() uint32 {
35 | return RtmpCidProtocolControl
36 | }
37 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_band_width.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | // BandWidthPacket the special packet for the bandwidth test.
4 | // actually, it's a OnStatusCallPacket, but
5 | // 1. encode with data field, to send data to client.
6 | // 2. decode ignore the data field, donot care.
7 | type BandWidthPacket struct {
8 | // Name of command.
9 | CommandName string
10 |
11 | // Transaction ID set to 0.
12 | TransactionID float64
13 |
14 | // Args Command information does not exist. Set to null type.
15 | Args Amf0Object // null
16 |
17 | // Data Name-value pairs that describe the response from the server.
18 | // ‘code’,‘level’, ‘description’ are names of few among such information.
19 | Data []Amf0Object
20 | }
21 |
22 | // Decode .
23 | func (pkt *BandWidthPacket) Decode(data []uint8) (err error) {
24 | var offset uint32
25 |
26 | if pkt.CommandName, err = Amf0ReadString(data, &offset); err != nil {
27 | return
28 | }
29 |
30 | if pkt.TransactionID, err = Amf0ReadNumber(data, &offset); err != nil {
31 | return
32 | }
33 |
34 | if err = amf0ReadNull(data, &offset); err != nil {
35 | return
36 | }
37 |
38 | if pkt.isStopPlay() || pkt.isStopPublish() || pkt.isFinish() {
39 | pkt.Data, err = amf0ReadObject(data, &offset)
40 | if err != nil {
41 | return
42 | }
43 | }
44 |
45 | return
46 | }
47 |
48 | func (pkt *BandWidthPacket) isStopPlay() bool {
49 | return RtmpBwCheckStopPlay == pkt.CommandName
50 | }
51 |
52 | func (pkt *BandWidthPacket) isStopPublish() bool {
53 | return RtmpBwCheckStartPublish == pkt.CommandName
54 | }
55 |
56 | func (pkt *BandWidthPacket) isFinish() bool {
57 | return RtmpBwCheckFinished == pkt.CommandName
58 | }
59 |
60 | // Encode .
61 | func (pkt *BandWidthPacket) Encode() (data []uint8) {
62 | data = append(data, amf0WriteString(pkt.CommandName)...)
63 | data = append(data, amf0WriteNumber(pkt.TransactionID)...)
64 | data = append(data, amf0WriteNull()...)
65 | data = append(data, amf0WriteObject(pkt.Data)...)
66 |
67 | return
68 | }
69 |
70 | // GetMessageType .
71 | func (pkt *BandWidthPacket) GetMessageType() uint8 {
72 | return RtmpMsgAmf0CommandMessage
73 | }
74 |
75 | // GetPreferCsID .
76 | func (pkt *BandWidthPacket) GetPreferCsID() uint32 {
77 | return RtmpCidOverStream
78 | }
79 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_call.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | // CallPacket the call method of the NetConnection object runs remote procedure
4 | // calls (RPC) at the receiving end. The called RPC name is passed as a parameter to the
5 | // call command
6 | type CallPacket struct {
7 |
8 | // CommandName Name of the remote procedure that is called.
9 | CommandName string
10 |
11 | // TransactionID If a response is expected we give a transaction Id. Else we pass a value of 0
12 | TransactionID float64
13 |
14 | // CommandObject If there exists any command info this
15 | // is set, else this is set to null type.
16 | CommandObject interface{}
17 |
18 | // CmdObjectType object type marker
19 | CmdObjectType uint8
20 |
21 | // Arguments Any optional arguments to be provided
22 | Arguments interface{}
23 |
24 | // ArgumentsType type of Arguments
25 | ArgumentsType uint8
26 | }
27 |
28 | // Decode .
29 | func (pkt *CallPacket) Decode(data []uint8) (err error) {
30 | var offset uint32
31 |
32 | if pkt.CommandName, err = Amf0ReadString(data, &offset); err != nil {
33 | return
34 | }
35 |
36 | if pkt.TransactionID, err = Amf0ReadNumber(data, &offset); err != nil {
37 | return
38 | }
39 |
40 | if pkt.CommandObject, err = amf0ReadAny(data, &pkt.CmdObjectType, &offset); err != nil {
41 | return
42 | }
43 |
44 | if uint32(len(data))-offset > 0 {
45 | pkt.Arguments, err = amf0ReadAny(data, &pkt.ArgumentsType, &offset)
46 | if err != nil {
47 | return
48 | }
49 | }
50 |
51 | return
52 | }
53 |
54 | // Encode .
55 | func (pkt *CallPacket) Encode() (data []uint8) {
56 | data = append(data, amf0WriteString(pkt.CommandName)...)
57 | data = append(data, amf0WriteNumber(pkt.TransactionID)...)
58 |
59 | if nil != pkt.CommandObject {
60 | data = append(data, amf0WriteAny(pkt.CommandObject.(Amf0Object))...)
61 | }
62 |
63 | if nil != pkt.Arguments {
64 | data = append(data, amf0WriteAny(pkt.Arguments.(Amf0Object))...)
65 | }
66 |
67 | return
68 | }
69 |
70 | // GetMessageType .
71 | func (pkt *CallPacket) GetMessageType() uint8 {
72 | return RtmpMsgAmf0CommandMessage
73 | }
74 |
75 | // GetPreferCsID .
76 | func (pkt *CallPacket) GetPreferCsID() uint32 {
77 | return RtmpCidOverConnection
78 | }
79 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_call_res.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | // CallResPacket response for CallPacket
4 | type CallResPacket struct {
5 | // CommandName Name of the command.
6 | CommandName string
7 |
8 | // TransactionID ID of the command, to which the response belongs to
9 | TransactionID float64
10 |
11 | // CommandObject If there exists any command info this is set, else this is set to null type.
12 | CommandObject interface{}
13 |
14 | // CommandObjectMarker object type marker
15 | CommandObjectMarker uint8
16 |
17 | // Response from the method that was called.
18 | Response interface{}
19 |
20 | // ResponseMarker response type marker
21 | ResponseMarker uint8
22 | }
23 |
24 | // Decode .
25 | func (pkt *CallResPacket) Decode(data []uint8) (err error) {
26 | var offset uint32
27 |
28 | if pkt.CommandName, err = Amf0ReadString(data, &offset); err != nil {
29 | return
30 | }
31 |
32 | if pkt.TransactionID, err = Amf0ReadNumber(data, &offset); err != nil {
33 | return
34 | }
35 |
36 | if pkt.CommandObject, err = amf0ReadAny(data, &pkt.CommandObjectMarker, &offset); err != nil {
37 | return
38 | }
39 |
40 | maxOffset := uint32(len(data)) - 1
41 | if maxOffset-offset > 0 {
42 | pkt.Response, err = amf0ReadAny(data, &pkt.ResponseMarker, &offset)
43 | if err != nil {
44 | return
45 | }
46 |
47 | }
48 |
49 | return
50 | }
51 |
52 | // Encode .
53 | func (pkt *CallResPacket) Encode() (data []uint8) {
54 | data = append(data, amf0WriteString(pkt.CommandName)...)
55 | data = append(data, amf0WriteNumber(pkt.TransactionID)...)
56 | if nil != pkt.CommandObject {
57 | data = append(data, amf0WriteAny(pkt.CommandObject.(Amf0Object))...)
58 | }
59 |
60 | if nil != pkt.Response {
61 | data = append(data, amf0WriteAny(pkt.Response.(Amf0Object))...)
62 | }
63 |
64 | return
65 | }
66 |
67 | // GetMessageType .
68 | func (pkt *CallResPacket) GetMessageType() uint8 {
69 | return RtmpMsgAmf0CommandMessage
70 | }
71 |
72 | // GetPreferCsID .
73 | func (pkt *CallResPacket) GetPreferCsID() uint32 {
74 | return RtmpCidOverConnection
75 | }
76 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_close_stream.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | // CloseStreamPacket client close stream packet.
4 | type CloseStreamPacket struct {
5 | // CommandName Name of the command, set to “closeStream”.
6 | CommandName string
7 |
8 | // Transaction ID set to 0.
9 | TransactionID float64
10 |
11 | // CommandObject Command information object does not exist. Set to null type.
12 | CommandObject Amf0Object // null
13 | }
14 |
15 | // Decode .
16 | func (pkt *CloseStreamPacket) Decode(data []uint8) (err error) {
17 | var offset uint32
18 |
19 | if pkt.CommandName, err = Amf0ReadString(data, &offset); err != nil {
20 | return
21 | }
22 |
23 | if pkt.TransactionID, err = Amf0ReadNumber(data, &offset); err != nil {
24 | return
25 | }
26 |
27 | if err = amf0ReadNull(data, &offset); err != nil {
28 | return
29 | }
30 |
31 | return
32 | }
33 |
34 | // Encode .
35 | func (pkt *CloseStreamPacket) Encode() (data []uint8) {
36 | //nothing
37 | return
38 | }
39 |
40 | // GetMessageType .
41 | func (pkt *CloseStreamPacket) GetMessageType() uint8 {
42 | //no method for this pakcet
43 | return 0
44 | }
45 |
46 | // GetPreferCsID .
47 | func (pkt *CloseStreamPacket) GetPreferCsID() uint32 {
48 | //no method for this pakcet
49 | return 0
50 | }
51 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_connect.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | import "fmt"
4 |
5 | // ConnectPacket The client sends the connect command to the server to request
6 | // connection to a server application instance.
7 | type ConnectPacket struct {
8 |
9 | // CommandName Name of the command. Set to “connect”.
10 | CommandName string
11 |
12 | // TransactionID Always set to 1.
13 | TransactionID float64
14 |
15 | // CommandObject Command information object which has the name-value pairs.
16 | CommandObject []Amf0Object
17 |
18 | // Args Any optional information
19 | Args []Amf0Object
20 | }
21 |
22 | // GetObjectProperty get object property in connect packet
23 | func (pkt *ConnectPacket) GetObjectProperty(name string) (value interface{}) {
24 |
25 | for _, v := range pkt.CommandObject {
26 | if name == v.propertyName {
27 | return v.value
28 | }
29 | }
30 |
31 | return
32 | }
33 |
34 | // Decode .
35 | func (pkt *ConnectPacket) Decode(data []uint8) (err error) {
36 | var offset uint32
37 |
38 | if pkt.CommandName, err = Amf0ReadString(data, &offset); err != nil {
39 | return
40 | }
41 |
42 | if RtmpAmf0CommandConnect != pkt.CommandName {
43 | err = fmt.Errorf("decode connect packet, command name is not connect")
44 | return
45 | }
46 |
47 | if pkt.TransactionID, err = Amf0ReadNumber(data, &offset); err != nil {
48 | return
49 | }
50 |
51 | if 1.0 != pkt.TransactionID {
52 | err = fmt.Errorf("decode conenct packet, transaction id is not 1.0")
53 | return
54 | }
55 |
56 | if pkt.CommandObject, err = amf0ReadObject(data, &offset); err != nil {
57 | return
58 | }
59 |
60 | if uint32(len(data))-offset > 0 {
61 | var marker uint8
62 | var v interface{}
63 | v, err = amf0ReadAny(data, &marker, &offset)
64 | if err != nil {
65 | return
66 | }
67 |
68 | if RtmpAmf0Object == marker {
69 | pkt.Args = v.([]Amf0Object)
70 | }
71 | }
72 |
73 | return
74 | }
75 |
76 | // Encode .
77 | func (pkt *ConnectPacket) Encode() (data []uint8) {
78 | data = append(data, amf0WriteString(pkt.CommandName)...)
79 | data = append(data, amf0WriteNumber(pkt.TransactionID)...)
80 | data = append(data, amf0WriteObject(pkt.CommandObject)...)
81 | if len(pkt.Args) > 0 {
82 | data = append(data, amf0WriteObject(pkt.Args)...)
83 | }
84 |
85 | return
86 | }
87 |
88 | // GetMessageType .
89 | func (pkt *ConnectPacket) GetMessageType() uint8 {
90 | return RtmpMsgAmf0CommandMessage
91 | }
92 |
93 | // GetPreferCsID .
94 | func (pkt *ConnectPacket) GetPreferCsID() uint32 {
95 | return RtmpCidOverConnection
96 | }
97 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_connect_res.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // ConnectResPacket response for SrsConnectAppPacket.
8 | type ConnectResPacket struct {
9 |
10 | // CommandName _result or _error; indicates whether the response is result or error.
11 | CommandName string
12 |
13 | // Transaction ID is 1 for call connect responses
14 | TransactionID float64
15 |
16 | // Props Name-value pairs that describe the properties(fmsver etc.) of the connection.
17 | Props []Amf0Object
18 |
19 | // Info Name-value pairs that describe the response from|the server. ‘code’,
20 | // ‘level’, ‘description’ are names of few among such information.
21 | Info []Amf0Object
22 | }
23 |
24 | // Decode .
25 | func (pkt *ConnectResPacket) Decode(data []uint8) (err error) {
26 | var offset uint32
27 |
28 | if pkt.CommandName, err = Amf0ReadString(data, &offset); err != nil {
29 | return
30 | }
31 |
32 | if pkt.CommandName != RtmpAmf0CommandResult {
33 | err = fmt.Errorf("decode connect res packet command name is error. actuall name=%s, should be %s",
34 | pkt.CommandName, RtmpAmf0CommandResult)
35 | return
36 | }
37 |
38 | if pkt.TransactionID, err = Amf0ReadNumber(data, &offset); err != nil {
39 | return
40 | }
41 |
42 | if pkt.TransactionID != 1.0 {
43 | err = fmt.Errorf("decode connect res packet transaction id != 1.0")
44 | return
45 | }
46 |
47 | if pkt.Props, err = amf0ReadObject(data, &offset); err != nil {
48 | return
49 | }
50 |
51 | if pkt.Info, err = amf0ReadObject(data, &offset); err != nil {
52 | return
53 | }
54 |
55 | return
56 | }
57 |
58 | // Encode .
59 | func (pkt *ConnectResPacket) Encode() (data []uint8) {
60 |
61 | data = append(data, amf0WriteString(pkt.CommandName)...)
62 | data = append(data, amf0WriteNumber(pkt.TransactionID)...)
63 | data = append(data, amf0WriteObject(pkt.Props)...)
64 | data = append(data, amf0WriteObject(pkt.Info)...)
65 |
66 | return
67 | }
68 |
69 | // GetMessageType .
70 | func (pkt *ConnectResPacket) GetMessageType() uint8 {
71 | return RtmpMsgAmf0CommandMessage
72 | }
73 |
74 | // GetPreferCsID .
75 | func (pkt *ConnectResPacket) GetPreferCsID() uint32 {
76 | return RtmpCidOverConnection
77 | }
78 |
79 | // AddProsObj add object to pros
80 | func (pkt *ConnectResPacket) AddProsObj(obj *Amf0Object) {
81 | pkt.Props = append(pkt.Props, *obj)
82 | }
83 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_create_stream.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // CreateStreamPacket The client sends this command to the server to create a logical
8 | // channel for message communication The publishing of audio, video, and
9 | // metadata is carried out over stream channel created using the
10 | // createStream command.
11 | type CreateStreamPacket struct {
12 | // CommandName Name of the command. Set to “createStream”.
13 | CommandName string
14 |
15 | // TransactionID Transaction ID of the command.
16 | TransactionID float64
17 |
18 | // CommandObject If there exists any command info this is set, else this is set to null type.
19 | CommandObject Amf0Object // null
20 | }
21 |
22 | // Decode .
23 | func (pkt *CreateStreamPacket) Decode(data []uint8) (err error) {
24 | var offset uint32
25 |
26 | if pkt.CommandName, err = Amf0ReadString(data, &offset); err != nil {
27 | return
28 | }
29 |
30 | if RtmpAmf0CommandCreateStream != pkt.CommandName {
31 | err = fmt.Errorf("decode create stream packet, command name is wrong. actully=%s", pkt.CommandName)
32 | return
33 | }
34 |
35 | if pkt.TransactionID, err = Amf0ReadNumber(data, &offset); err != nil {
36 | return
37 | }
38 |
39 | if err = amf0ReadNull(data, &offset); err != nil {
40 | return
41 | }
42 |
43 | return
44 | }
45 |
46 | // Encode .
47 | func (pkt *CreateStreamPacket) Encode() (data []uint8) {
48 | data = append(data, amf0WriteString(pkt.CommandName)...)
49 | data = append(data, amf0WriteNumber(pkt.TransactionID)...)
50 | data = append(data, amf0WriteNull()...)
51 |
52 | return
53 | }
54 |
55 | // GetMessageType .
56 | func (pkt *CreateStreamPacket) GetMessageType() uint8 {
57 | return RtmpMsgAmf0CommandMessage
58 | }
59 |
60 | // GetPreferCsID .
61 | func (pkt *CreateStreamPacket) GetPreferCsID() uint32 {
62 | return RtmpCidOverConnection
63 | }
64 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_create_stream_res.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // CreateStreamResPacket response for CreateStreamPacket
8 | type CreateStreamResPacket struct {
9 |
10 | // CommandName _result or _error; indicates whether the response is result or error.
11 | CommandName string
12 |
13 | // TransactionID ID of the command that response belongs to.
14 | TransactionID float64
15 |
16 | // CommandObject If there exists any command info this is set, else this is set to null type.
17 | CommandObject Amf0Object // null
18 |
19 | // StreamID The return value is either a stream ID or an error information object.
20 | StreamID float64
21 | }
22 |
23 | // Encode .
24 | func (pkt *CreateStreamResPacket) Encode() (data []uint8) {
25 |
26 | data = append(data, amf0WriteString(pkt.CommandName)...)
27 | data = append(data, amf0WriteNumber(pkt.TransactionID)...)
28 | data = append(data, amf0WriteNull()...)
29 | data = append(data, amf0WriteNumber(pkt.StreamID)...)
30 |
31 | return
32 | }
33 |
34 | // Decode .
35 | func (pkt *CreateStreamResPacket) Decode(data []uint8) (err error) {
36 |
37 | var offset uint32
38 |
39 | if pkt.CommandName, err = Amf0ReadString(data, &offset); err != nil {
40 | return
41 | }
42 |
43 | if RtmpAmf0CommandResult != pkt.CommandName {
44 | err = fmt.Errorf("decode create stream res packet, command name is not result")
45 | return
46 | }
47 |
48 | if pkt.TransactionID, err = Amf0ReadNumber(data, &offset); err != nil {
49 | return
50 | }
51 |
52 | if err = amf0ReadNull(data, &offset); err != nil {
53 | return
54 | }
55 |
56 | if pkt.TransactionID, err = Amf0ReadNumber(data, &offset); err != nil {
57 | return
58 | }
59 |
60 | return
61 | }
62 |
63 | // GetMessageType .
64 | func (pkt *CreateStreamResPacket) GetMessageType() uint8 {
65 | return RtmpMsgAmf0CommandMessage
66 | }
67 |
68 | // GetPreferCsID .
69 | func (pkt *CreateStreamResPacket) GetPreferCsID() uint32 {
70 | return RtmpCidOverConnection
71 | }
72 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_fmle_start.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | // FmleStartPacket FMLE start publish: ReleaseStream/PublishStream
9 | type FmleStartPacket struct {
10 | // CommandName Name of the command
11 | CommandName string
12 |
13 | // TransactionID the transaction ID to get the response.
14 | TransactionID float64
15 |
16 | // CommandObject If there exists any command info this is set, else this is set to null type.
17 | CommandObject Amf0Object // null
18 |
19 | // StreamName the stream name to start publish or release.
20 | StreamName string
21 |
22 | // TokenStrToken value, for authentication. it's optional.
23 | TokenStr string
24 | }
25 |
26 | // Decode .
27 | func (pkt *FmleStartPacket) Decode(data []uint8) (err error) {
28 | var offset uint32
29 |
30 | if pkt.CommandName, err = Amf0ReadString(data, &offset); err != nil {
31 | return
32 | }
33 |
34 | if RtmpAmf0CommandReleaseStream != pkt.CommandName &&
35 | RtmpAmf0CommandFcPublish != pkt.CommandName &&
36 | RtmpAmf0CommandUnpublish != pkt.CommandName {
37 | err = fmt.Errorf("decode fmle start packet error, command name is error.actully=%s", pkt.CommandName)
38 | return
39 | }
40 |
41 | if pkt.TransactionID, err = Amf0ReadNumber(data, &offset); err != nil {
42 | return
43 | }
44 |
45 | if err = amf0ReadNull(data, &offset); err != nil {
46 | return
47 | }
48 |
49 | var streamNameLocal string
50 | if streamNameLocal, err = Amf0ReadString(data, &offset); err != nil {
51 | return
52 | }
53 |
54 | i := strings.Index(streamNameLocal, TokenStr)
55 | if i < 0 {
56 | pkt.StreamName = streamNameLocal
57 | } else {
58 | pkt.StreamName = streamNameLocal[0:i]
59 | pkt.TokenStr = streamNameLocal[i+len(TokenStr):]
60 | }
61 |
62 | return
63 | }
64 |
65 | // Encode .
66 | func (pkt *FmleStartPacket) Encode() (data []uint8) {
67 | data = append(data, amf0WriteString(pkt.CommandName)...)
68 | data = append(data, amf0WriteNumber(pkt.TransactionID)...)
69 | data = append(data, amf0WriteNull()...)
70 | data = append(data, amf0WriteString(pkt.StreamName)...)
71 |
72 | return
73 | }
74 |
75 | // GetMessageType .
76 | func (pkt *FmleStartPacket) GetMessageType() uint8 {
77 | return RtmpMsgAmf0CommandMessage
78 | }
79 |
80 | // GetPreferCsID .
81 | func (pkt *FmleStartPacket) GetPreferCsID() uint32 {
82 | return RtmpCidOverConnection
83 | }
84 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_fmle_start_res.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | import "fmt"
4 |
5 | // FmleStartResPacket response for FmleStartPacket
6 | type FmleStartResPacket struct {
7 |
8 | // CommandName Name of the command
9 | CommandName string
10 |
11 | // TransactionID the transaction ID to get the response.
12 | TransactionID float64
13 |
14 | // CommandObject If there exists any command info this is set, else this is set to null type.
15 | CommandObject Amf0Object // null
16 |
17 | // Args the optional args, set to undefined.
18 | Args Amf0Object // undefined
19 | }
20 |
21 | // Decode .
22 | func (pkt *FmleStartResPacket) Decode(data []uint8) (err error) {
23 | var offset uint32
24 |
25 | if pkt.CommandName, err = Amf0ReadString(data, &offset); err != nil {
26 | return
27 | }
28 |
29 | if RtmpAmf0CommandResult != pkt.CommandName {
30 | err = fmt.Errorf("decode fmle start res packet, command name is not result")
31 | return
32 | }
33 |
34 | if pkt.TransactionID, err = Amf0ReadNumber(data, &offset); err != nil {
35 | return
36 | }
37 |
38 | if err = amf0ReadNull(data, &offset); err != nil {
39 | return
40 | }
41 |
42 | if err = amf0ReadUndefined(data, &offset); err != nil {
43 | return
44 | }
45 |
46 | return
47 | }
48 |
49 | // Encode .
50 | func (pkt *FmleStartResPacket) Encode() (data []uint8) {
51 |
52 | data = append(data, amf0WriteString(pkt.CommandName)...)
53 | data = append(data, amf0WriteNumber(pkt.TransactionID)...)
54 | data = append(data, amf0WriteNull()...)
55 | data = append(data, amf0WriteUndefined()...)
56 |
57 | return
58 | }
59 |
60 | // GetMessageType .
61 | func (pkt *FmleStartResPacket) GetMessageType() uint8 {
62 | return RtmpMsgAmf0CommandMessage
63 | }
64 |
65 | // GetPreferCsID .
66 | func (pkt *FmleStartResPacket) GetPreferCsID() uint32 {
67 | return RtmpCidOverConnection
68 | }
69 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_on_custom_data.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | // OnCustomDataPakcet the stream custom data.
4 | type OnCustomDataPakcet struct {
5 |
6 | // Name of custom data. Set to "onCustomData"
7 | Name string
8 |
9 | // Customdata Custom data of stream.
10 | Customdata interface{}
11 |
12 | // Marker type of CustomData
13 | Marker uint8
14 | }
15 |
16 | // Decode .
17 | func (pkt *OnCustomDataPakcet) Decode(data []uint8) (err error) {
18 | var offset uint32
19 |
20 | if pkt.Name, err = Amf0ReadString(data, &offset); err != nil {
21 | return
22 | }
23 |
24 | if pkt.Customdata, err = amf0ReadAny(data, &pkt.Marker, &offset); err != nil {
25 | return
26 | }
27 |
28 | return
29 | }
30 |
31 | // Encode .
32 | func (pkt *OnCustomDataPakcet) Encode() (data []uint8) {
33 | data = append(data, amf0WriteString(pkt.Name)...)
34 | if RtmpAmf0Object == pkt.Marker {
35 | data = append(data, amf0WriteObject(pkt.Customdata.([]Amf0Object))...)
36 | } else if RtmpAmf0EcmaArray == pkt.Marker {
37 | data = append(data, amf0WriteEcmaArray(pkt.Customdata.(amf0EcmaArray))...)
38 | }
39 |
40 | return
41 | }
42 |
43 | // GetMessageType .
44 | func (pkt *OnCustomDataPakcet) GetMessageType() uint8 {
45 | return RtmpMsgAmf0DataMessage
46 | }
47 |
48 | // GetPreferCsID .
49 | func (pkt *OnCustomDataPakcet) GetPreferCsID() uint32 {
50 | return RtmpCidOverConnection2
51 | }
52 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_on_meta_data.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | // OnMetaDataPacket the stream metadata.
4 | // @setDataFrame
5 | type OnMetaDataPacket struct {
6 |
7 | // Name Name of metadata. Set to "onMetaData"
8 | Name string
9 |
10 | // Metadata Metadata of stream.
11 | Metadata interface{}
12 |
13 | // Marker type of Metadata object or ecma
14 | Marker uint8
15 | }
16 |
17 | // Decode .
18 | func (pkt *OnMetaDataPacket) Decode(data []uint8) (err error) {
19 | var offset uint32
20 |
21 | if pkt.Name, err = Amf0ReadString(data, &offset); err != nil {
22 | return
23 | }
24 |
25 | if RtmpAmf0DataSetDataFrame == pkt.Name {
26 | if pkt.Name, err = Amf0ReadString(data, &offset); err != nil {
27 | return
28 | }
29 | }
30 |
31 | if pkt.Metadata, err = amf0ReadAny(data, &pkt.Marker, &offset); err != nil {
32 | return
33 | }
34 |
35 | return
36 | }
37 |
38 | // Encode .
39 | func (pkt *OnMetaDataPacket) Encode() (data []uint8) {
40 | data = append(data, amf0WriteString(pkt.Name)...)
41 | if RtmpAmf0Object == pkt.Marker {
42 | data = append(data, amf0WriteObject(pkt.Metadata.([]Amf0Object))...)
43 | } else if RtmpAmf0EcmaArray == pkt.Marker {
44 | data = append(data, amf0WriteEcmaArray(pkt.Metadata.(amf0EcmaArray))...)
45 | }
46 |
47 | return
48 | }
49 |
50 | // GetMessageType .
51 | func (pkt *OnMetaDataPacket) GetMessageType() uint8 {
52 | return RtmpMsgAmf0DataMessage
53 | }
54 |
55 | // GetPreferCsID .
56 | func (pkt *OnMetaDataPacket) GetPreferCsID() uint32 {
57 | return RtmpCidOverConnection2
58 | }
59 |
60 | // AddObject add object to objs
61 | func (pkt *OnMetaDataPacket) AddObject(obj Amf0Object) {
62 | if RtmpAmf0Object == pkt.Marker {
63 | pkt.Metadata = append(pkt.Metadata.([]Amf0Object), obj)
64 | } else if RtmpAmf0EcmaArray == pkt.Marker {
65 | v := pkt.Metadata.(amf0EcmaArray)
66 | v.addObject(obj)
67 |
68 | pkt.Metadata = v
69 | }
70 | }
71 |
72 | // GetProperty get object property name
73 | func (pkt *OnMetaDataPacket) GetProperty(name string) interface{} {
74 |
75 | if RtmpAmf0Object == pkt.Marker {
76 | for _, v := range pkt.Metadata.([]Amf0Object) {
77 | if name == v.propertyName {
78 | return v.value
79 | }
80 | }
81 | } else if RtmpAmf0EcmaArray == pkt.Marker {
82 | for _, v := range (pkt.Metadata.(amf0EcmaArray)).anyObject {
83 | if name == v.propertyName {
84 | return v.value
85 | }
86 | }
87 | }
88 |
89 | return nil
90 | }
91 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_on_status_call.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | // OnStatusCallPacket onStatus command, AMF0 Call
4 | // user must set the stream id
5 | type OnStatusCallPacket struct {
6 |
7 | // CommandName Name of command. Set to "onStatus"
8 | CommandName string
9 |
10 | // TransactionID set to 0
11 | TransactionID float64
12 |
13 | // Args Command information does not exist. Set to null type.
14 | Args Amf0Object
15 |
16 | // Data Name-value pairs that describe the response from the server.
17 | // ‘code’,‘level’, ‘description’ are names of few among such information.
18 | Data []Amf0Object
19 | }
20 |
21 | // Decode .
22 | func (pkt *OnStatusCallPacket) Decode(data []uint8) (err error) {
23 | //nothing
24 | return
25 | }
26 |
27 | // Encode .
28 | func (pkt *OnStatusCallPacket) Encode() (data []uint8) {
29 | data = append(data, amf0WriteString(pkt.CommandName)...)
30 | data = append(data, amf0WriteNumber(pkt.TransactionID)...)
31 | data = append(data, amf0WriteNull()...)
32 | data = append(data, amf0WriteObject(pkt.Data)...)
33 |
34 | return
35 | }
36 |
37 | // GetMessageType .
38 | func (pkt *OnStatusCallPacket) GetMessageType() uint8 {
39 | return RtmpMsgAmf0CommandMessage
40 | }
41 |
42 | // GetPreferCsID .
43 | func (pkt *OnStatusCallPacket) GetPreferCsID() uint32 {
44 | return RtmpCidOverStream
45 | }
46 |
47 | // AddObj add object to data
48 | func (pkt *OnStatusCallPacket) AddObj(obj *Amf0Object) {
49 | pkt.Data = append(pkt.Data, *obj)
50 | }
51 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_on_status_data.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | // OnStatusDataPacket onStatus data, AMF0 Data
4 | // user must set the stream id
5 | type OnStatusDataPacket struct {
6 |
7 | // CommandName Name of command. Set to "onStatus"
8 | CommandName string
9 |
10 | // Data Name-value pairs that describe the response from the server.
11 | // ‘code’, are names of few among such information.
12 | Data []Amf0Object
13 | }
14 |
15 | // Decode .
16 | func (pkt *OnStatusDataPacket) Decode(data []uint8) (err error) {
17 | //nothing
18 | return
19 | }
20 |
21 | // Encode .
22 | func (pkt *OnStatusDataPacket) Encode() (data []uint8) {
23 | data = append(data, amf0WriteString(pkt.CommandName)...)
24 | data = append(data, amf0WriteObject(pkt.Data)...)
25 |
26 | return
27 | }
28 |
29 | // GetMessageType .
30 | func (pkt *OnStatusDataPacket) GetMessageType() uint8 {
31 | return RtmpMsgAmf0DataMessage
32 | }
33 |
34 | // GetPreferCsID .
35 | func (pkt *OnStatusDataPacket) GetPreferCsID() uint32 {
36 | return RtmpCidOverStream
37 | }
38 |
39 | // AddObj add object to Data
40 | func (pkt *OnStatusDataPacket) AddObj(obj *Amf0Object) {
41 | pkt.Data = append(pkt.Data, *obj)
42 | }
43 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_onbw_done.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | // OnBwDonePacket when bandwidth test done, notice client
4 | type OnBwDonePacket struct {
5 |
6 | // CommandName Name of command. Set to "onBWDone"
7 | CommandName string
8 |
9 | // TransactionID Transaction ID set to 0.
10 | TransactionID float64
11 |
12 | // Args Command information does not exist. Set to null type.
13 | Args Amf0Object
14 | }
15 |
16 | // Decode .
17 | func (pkt *OnBwDonePacket) Decode(data []uint8) (err error) {
18 | //nothing
19 | return
20 | }
21 |
22 | // Encode .
23 | func (pkt *OnBwDonePacket) Encode() (data []uint8) {
24 | data = append(data, amf0WriteString(pkt.CommandName)...)
25 | data = append(data, amf0WriteNumber(pkt.TransactionID)...)
26 | data = append(data, amf0WriteNull()...)
27 |
28 | return
29 | }
30 |
31 | // GetMessageType .
32 | func (pkt *OnBwDonePacket) GetMessageType() uint8 {
33 | return RtmpMsgAmf0CommandMessage
34 | }
35 |
36 | // GetPreferCsID .
37 | func (pkt *OnBwDonePacket) GetPreferCsID() uint32 {
38 | return RtmpCidOverConnection
39 | }
40 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_pause.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | import (
4 | "fmt"
5 | )
6 |
7 | // PausePacket The client sends the pause command to tell the server to pause or start playing.
8 | type PausePacket struct {
9 |
10 | // CommandName Name of the command, set to “pause”.
11 | CommandName string
12 |
13 | // TransactionID There is no transaction ID for this command. Set to 0.
14 | TransactionID float64
15 |
16 | // CommandObject Command information object does not exist. Set to null type.
17 | CommandObject Amf0Object
18 |
19 | // IsPause true or false, to indicate pausing or resuming play
20 | IsPause bool
21 |
22 | // TimeMs Number of milliseconds at which the the stream is paused or play resumed.
23 | // This is the current stream time at the Client when stream was paused. When the
24 | // playback is resumed, the server will only send messages with timestamps
25 | // greater than this value.
26 | TimeMs float64
27 | }
28 |
29 | // Decode .
30 | func (pkt *PausePacket) Decode(data []uint8) (err error) {
31 |
32 | var offset uint32
33 |
34 | if pkt.CommandName, err = Amf0ReadString(data, &offset); err != nil {
35 | return
36 | }
37 |
38 | if RtmpAmf0CommandPause == pkt.CommandName {
39 | err = fmt.Errorf("decode pause packet command name is error.actully=%s", pkt.CommandName)
40 | return
41 | }
42 |
43 | if pkt.TransactionID, err = Amf0ReadNumber(data, &offset); err != nil {
44 | return
45 | }
46 |
47 | if err = amf0ReadNull(data, &offset); err != nil {
48 | return
49 | }
50 |
51 | if pkt.IsPause, err = amf0ReadBool(data, &offset); err != nil {
52 | return
53 | }
54 |
55 | if pkt.TimeMs, err = Amf0ReadNumber(data, &offset); err != nil {
56 | return
57 | }
58 |
59 | return
60 | }
61 |
62 | // Encode .
63 | func (pkt *PausePacket) Encode() (data []uint8) {
64 | //no this method
65 | return
66 | }
67 |
68 | // GetMessageType .
69 | func (pkt *PausePacket) GetMessageType() uint8 {
70 | //no this method
71 | return 0
72 | }
73 |
74 | // GetPreferCsID .
75 | func (pkt *PausePacket) GetPreferCsID() uint32 {
76 | //no this method
77 | return 0
78 | }
79 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_play.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | // PlayPacket The client sends this command to the server to play a stream.
9 | type PlayPacket struct {
10 |
11 | // CommandName Name of the command. Set to “play”.
12 | CommandName string
13 |
14 | // Transaction ID set to 0.
15 | TransactionID float64
16 |
17 | // CommandObject Command information does not exist. Set to null type.
18 | CommandObject Amf0Object
19 |
20 | // StreamName Name of the stream to play.
21 | // To play video (FLV) files, specify the name of the stream without a file
22 | // extension (for example, "sample").
23 | // To play back MP3 or ID3 tags, you must precede the stream name with mp3:
24 | // (for example, "mp3:sample".)
25 | // To play H.264/AAC files, you must precede the stream name with mp4: and specify the
26 | // file extension. For example, to play the file sample.m4v, specify
27 | // "mp4:sample.m4v"
28 | StreamName string
29 |
30 | // TokenStr Token value, for authentication. it's optional.
31 | TokenStr string
32 |
33 | // Start An optional parameter that specifies the start time in seconds.
34 | // The default value is -2, which means the subscriber first tries to play the live
35 | // stream specified in the Stream Name field. If a live stream of that name is
36 | // not found, it plays the recorded stream specified in the Stream Name field.
37 | // If you pass -1 in the Start field, only the live stream specified in the Stream
38 | // Name field is played.
39 | // If you pass 0 or a positive number in the Start field, a recorded stream specified
40 | // in the Stream Name field is played beginning from the time specified in the
41 | // Start field.
42 | // If no recorded stream is found, the next item in the playlist is played.
43 | Start float64
44 |
45 | // Duration An optional parameter that specifies the duration of playback in seconds.
46 | // The default value is -1. The -1 value means a live stream is played until it is no
47 | // longer available or a recorded stream is played until it ends.
48 | // If u pass 0, it plays the single frame since the time specified in the Start field
49 | // from the beginning of a recorded stream. It is assumed that the value specified
50 | // in the Start field is equal to or greater than 0.
51 | // If you pass a positive number, it plays a live stream for the time period specified
52 | // in the Duration field. After that it becomes available or plays a recorded
53 | // stream for the time specified in the Duration field. (If a stream ends before the
54 | // time specified in the Duration field, playback ends when the stream ends.)
55 | // If you pass a negative number other than -1 in the Duration field, it interprets the
56 | // value as if it were -1.
57 | Duration float64
58 |
59 | // Reset An optional Boolean value or number that specifies whether to flush any
60 | // previous playlist.
61 | Reset bool
62 | }
63 |
64 | // Decode .
65 | func (pkt *PlayPacket) Decode(data []uint8) (err error) {
66 | var maxOffset uint32
67 | maxOffset = uint32(len(data)) - 1
68 |
69 | var offset uint32
70 |
71 | if pkt.CommandName, err = Amf0ReadString(data, &offset); err != nil {
72 | return
73 | }
74 |
75 | if RtmpAmf0CommandPlay != pkt.CommandName {
76 | err = fmt.Errorf("decode play packet, command name is not play.actully=%s", pkt.CommandName)
77 | return
78 | }
79 |
80 | if pkt.TransactionID, err = Amf0ReadNumber(data, &offset); err != nil {
81 | return
82 | }
83 |
84 | if err = amf0ReadNull(data, &offset); err != nil {
85 | return
86 | }
87 |
88 | var streamNameLocal string
89 | if streamNameLocal, err = Amf0ReadString(data, &offset); err != nil {
90 | return
91 | }
92 |
93 | i := strings.Index(streamNameLocal, TokenStr)
94 | if i < 0 {
95 | pkt.StreamName = streamNameLocal
96 | } else {
97 | pkt.StreamName = streamNameLocal[0:i]
98 | pkt.TokenStr = streamNameLocal[i+len(TokenStr):]
99 | }
100 |
101 | if maxOffset-offset > (1 + 8) { // number need at least 1(marker) + 8(number)
102 | if pkt.Start, err = Amf0ReadNumber(data, &offset); err != nil {
103 | return
104 | }
105 | }
106 |
107 | if maxOffset-offset > (1 + 8) {
108 | if pkt.Duration, err = Amf0ReadNumber(data, &offset); err != nil {
109 | return
110 | }
111 | }
112 |
113 | if offset >= uint32(len(data)) {
114 | return
115 | }
116 |
117 | if maxOffset-offset >= 2 { //because the bool type need 2 bytes at least
118 |
119 | var v interface{}
120 | var marker uint8
121 | if v, err = amf0ReadAny(data, &marker, &offset); err != nil {
122 | return
123 | }
124 |
125 | if RtmpAmf0Boolean == marker {
126 | pkt.Reset = v.(bool)
127 | } else if RtmpAmf0Number == marker {
128 | pkt.Reset = (v.(float64) != 0)
129 | }
130 | }
131 |
132 | return
133 | }
134 |
135 | // Encode .
136 | func (pkt *PlayPacket) Encode() (data []uint8) {
137 |
138 | data = append(data, amf0WriteString(pkt.CommandName)...)
139 | data = append(data, amf0WriteNumber(pkt.TransactionID)...)
140 | data = append(data, amf0WriteNull()...)
141 | data = append(data, amf0WriteString(pkt.StreamName)...)
142 | data = append(data, amf0WriteNumber(pkt.Start)...)
143 | data = append(data, amf0WriteNumber(pkt.Duration)...)
144 | data = append(data, amf0WriteBool(pkt.Reset)...)
145 |
146 | return
147 | }
148 |
149 | // GetMessageType .
150 | func (pkt *PlayPacket) GetMessageType() uint8 {
151 | return RtmpMsgAmf0CommandMessage
152 | }
153 |
154 | // GetPreferCsID .
155 | func (pkt *PlayPacket) GetPreferCsID() uint32 {
156 | return RtmpCidOverStream
157 | }
158 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_play_res.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | // PlayResPacket response for PlayPacket
4 | type PlayResPacket struct {
5 | // CommandName Name of the command. If the play command is successful, the command
6 | // name is set to onStatus.
7 | CommandName string
8 |
9 | // Transaction ID set to 0.
10 | TransactionID float64
11 |
12 | // CommandObject Command information does not exist. Set to null type.
13 | CommandObject Amf0Object
14 |
15 | // Desc If the play command is successful, the client receives OnStatus message from
16 | // server which is NetStream.Play.Start. If the specified stream is not found,
17 | // NetStream.Play.StreamNotFound is received.
18 | Desc []Amf0Object
19 | }
20 |
21 | // Decode .
22 | func (pkt *PlayResPacket) Decode(data []uint8) (err error) {
23 | //nothing
24 | return
25 | }
26 |
27 | // Encode .
28 | func (pkt *PlayResPacket) Encode() (data []uint8) {
29 | data = append(data, amf0WriteString(pkt.CommandName)...)
30 | data = append(data, amf0WriteNumber(pkt.TransactionID)...)
31 | data = append(data, amf0WriteObject(pkt.Desc)...)
32 |
33 | return
34 | }
35 |
36 | // GetMessageType .
37 | func (pkt *PlayResPacket) GetMessageType() uint8 {
38 | return RtmpMsgAmf0CommandMessage
39 | }
40 |
41 | // GetPreferCsID .
42 | func (pkt *PlayResPacket) GetPreferCsID() uint32 {
43 | return RtmpCidOverStream
44 | }
45 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_publish.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | // PublishPacket The client sends the publish command to publish a named stream to the
9 | // server. Using this name, any client can play this stream and receive
10 | // the published audio, video, and data messages.
11 | type PublishPacket struct {
12 |
13 | // CommandName Name of the command, set to “publish”.
14 | CommandName string
15 |
16 | // Transaction ID set to 0.
17 | TransactionID float64
18 |
19 | // CommandObject Command information object does not exist. Set to null type.
20 | CommandObject Amf0Object
21 |
22 | // StreamName Name with which the stream is published.
23 | StreamName string
24 |
25 | // TokenStr Token value, for authentication. it's optional.
26 | TokenStr string
27 |
28 | // Type of publishing. Set to “live”, “record”, or “append”.
29 | // record: The stream is published and the data is recorded to a new file.The file
30 | // is stored on the server in a subdirectory within the directory that
31 | // contains the server application. If the file already exists, it is
32 | // overwritten.
33 | // append: The stream is published and the data is appended to a file. If no file
34 | // is found, it is created.
35 | // live: Live data is published without recording it in a file.
36 | // @remark, only support live.
37 | // @remark, optional, default to live.
38 | Type string
39 | }
40 |
41 | // Decode .
42 | func (pkt *PublishPacket) Decode(data []uint8) (err error) {
43 | var offset uint32
44 |
45 | if pkt.CommandName, err = Amf0ReadString(data, &offset); err != nil {
46 | return
47 | }
48 |
49 | if RtmpAmf0CommandPublish != pkt.CommandName {
50 | err = fmt.Errorf("decode publish packet command name is error.actully=%s", pkt.CommandName)
51 | return
52 | }
53 |
54 | if pkt.TransactionID, err = Amf0ReadNumber(data, &offset); err != nil {
55 | return
56 | }
57 |
58 | if err = amf0ReadNull(data, &offset); err != nil {
59 | return
60 | }
61 |
62 | var streamNameLocal string
63 | if streamNameLocal, err = Amf0ReadString(data, &offset); err != nil {
64 | return
65 | }
66 |
67 | i := strings.Index(streamNameLocal, TokenStr)
68 | if i < 0 {
69 | pkt.StreamName = streamNameLocal
70 | } else {
71 | pkt.StreamName = streamNameLocal[0:i]
72 | pkt.TokenStr = streamNameLocal[i+len(TokenStr):]
73 | }
74 |
75 | if uint32(len(data))-offset > 0 {
76 | pkt.Type, err = Amf0ReadString(data, &offset)
77 | if err != nil {
78 | return
79 | }
80 | }
81 |
82 | return
83 | }
84 |
85 | // Encode .
86 | func (pkt *PublishPacket) Encode() (data []uint8) {
87 | data = append(data, amf0WriteString(pkt.CommandName)...)
88 | data = append(data, amf0WriteNumber(pkt.TransactionID)...)
89 | data = append(data, amf0WriteNull()...)
90 | data = append(data, amf0WriteString(pkt.StreamName)...)
91 | data = append(data, amf0WriteString(pkt.Type)...)
92 |
93 | return
94 | }
95 |
96 | // GetMessageType .
97 | func (pkt *PublishPacket) GetMessageType() uint8 {
98 | return RtmpMsgAmf0CommandMessage
99 | }
100 |
101 | // GetPreferCsID .
102 | func (pkt *PublishPacket) GetPreferCsID() uint32 {
103 | return RtmpCidOverStream
104 | }
105 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_sample_access.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | // SampleAccessPacket .
4 | type SampleAccessPacket struct {
5 |
6 | // CommandName Name of command. Set to "|RtmpSampleAccess".
7 | CommandName string
8 |
9 | // VideoSampleAccess whether allow access the sample of video.
10 | VideoSampleAccess bool
11 |
12 | // AudioSampleAccess whether allow access the sample of audio.
13 | AudioSampleAccess bool
14 | }
15 |
16 | // Decode .
17 | func (pkt *SampleAccessPacket) Decode(data []uint8) (err error) {
18 | //nothing
19 |
20 | return
21 | }
22 |
23 | // Encode .
24 | func (pkt *SampleAccessPacket) Encode() (data []uint8) {
25 | data = append(data, amf0WriteString(pkt.CommandName)...)
26 | data = append(data, amf0WriteBool(pkt.VideoSampleAccess)...)
27 | data = append(data, amf0WriteBool(pkt.AudioSampleAccess)...)
28 |
29 | return
30 | }
31 |
32 | // GetMessageType .
33 | func (pkt *SampleAccessPacket) GetMessageType() uint8 {
34 | return RtmpMsgAmf0DataMessage
35 | }
36 |
37 | // GetPreferCsID .
38 | func (pkt *SampleAccessPacket) GetPreferCsID() uint32 {
39 | return RtmpCidOverStream
40 | }
41 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_set_chunk_size.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | )
7 |
8 | // SetChunkSizePacket Protocol control message 1, Set Chunk Size, is used to notify the
9 | // peer about the new maximum chunk size.
10 | type SetChunkSizePacket struct {
11 | // ChunkSize The maximum chunk size can be 65536 bytes. The chunk size is
12 | // maintained independently for each direction.
13 | ChunkSize uint32
14 | }
15 |
16 | // Decode .
17 | func (pkt *SetChunkSizePacket) Decode(data []uint8) (err error) {
18 | if len(data) < 4 {
19 | err = fmt.Errorf("decode set chunk size packet, data len is not enough")
20 | return
21 | }
22 |
23 | var offset uint32
24 | pkt.ChunkSize = binary.BigEndian.Uint32(data[offset : offset+4])
25 | offset += 4
26 |
27 | return
28 | }
29 |
30 | // Encode .
31 | func (pkt *SetChunkSizePacket) Encode() (data []uint8) {
32 |
33 | data = make([]uint8, 4)
34 |
35 | var offset uint32
36 |
37 | binary.BigEndian.PutUint32(data[offset:offset+4], pkt.ChunkSize)
38 | offset += 4
39 |
40 | return
41 | }
42 |
43 | // GetMessageType .
44 | func (pkt *SetChunkSizePacket) GetMessageType() uint8 {
45 | return RtmpMsgSetChunkSize
46 | }
47 |
48 | // GetPreferCsID .
49 | func (pkt *SetChunkSizePacket) GetPreferCsID() uint32 {
50 | return RtmpCidProtocolControl
51 | }
52 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_set_peer_bandwidth.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | import (
4 | "encoding/binary"
5 | )
6 |
7 | // SetPeerBandWidthPacket The client or the server sends this message to update the output
8 | // bandwidth of the peer.
9 | type SetPeerBandWidthPacket struct {
10 | Bandwidth uint32
11 | TypeLimit uint8
12 | }
13 |
14 | // Decode .
15 | func (pkt *SetPeerBandWidthPacket) Decode(data []uint8) (err error) {
16 | //nothing
17 | return
18 | }
19 |
20 | // Encode .
21 | func (pkt *SetPeerBandWidthPacket) Encode() (data []uint8) {
22 | data = make([]uint8, 5)
23 |
24 | var offset uint32
25 |
26 | binary.BigEndian.PutUint32(data[offset:offset+4], pkt.Bandwidth)
27 | offset += 4
28 |
29 | data[offset] = pkt.TypeLimit
30 | offset++
31 |
32 | return
33 | }
34 |
35 | // GetMessageType .
36 | func (pkt *SetPeerBandWidthPacket) GetMessageType() uint8 {
37 | return RtmpMsgSetPeerBandwidth
38 | }
39 |
40 | // GetPreferCsID .
41 | func (pkt *SetPeerBandWidthPacket) GetPreferCsID() uint32 {
42 | return RtmpCidProtocolControl
43 | }
44 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_set_window_ack_size.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | )
7 |
8 | // SetWindowAckSizePacket The client or the server sends this message to inform the peer which
9 | // window size to use when sending acknowledgment.
10 | type SetWindowAckSizePacket struct {
11 | AckowledgementWindowSize uint32
12 | }
13 |
14 | // Decode .
15 | func (pkt *SetWindowAckSizePacket) Decode(data []uint8) (err error) {
16 | if len(data) < 4 {
17 | err = fmt.Errorf("decode set window ack size packet, len is not enough")
18 | return
19 | }
20 |
21 | var offset uint32
22 | pkt.AckowledgementWindowSize = binary.BigEndian.Uint32(data[offset : offset+4])
23 | offset += 4
24 |
25 | return
26 | }
27 |
28 | // Encode .
29 | func (pkt *SetWindowAckSizePacket) Encode() (data []uint8) {
30 |
31 | data = make([]uint8, 4)
32 |
33 | var offset uint32
34 | binary.BigEndian.PutUint32(data[offset:offset+4], pkt.AckowledgementWindowSize)
35 | offset += 4
36 |
37 | return
38 | }
39 |
40 | // GetMessageType .
41 | func (pkt *SetWindowAckSizePacket) GetMessageType() uint8 {
42 | return RtmpMsgWindowAcknowledgementSize
43 | }
44 |
45 | // GetPreferCsID .
46 | func (pkt *SetWindowAckSizePacket) GetPreferCsID() uint32 {
47 | return RtmpCidProtocolControl
48 | }
49 |
--------------------------------------------------------------------------------
/rtmp/pt/packet_user_control.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | import (
4 | "encoding/binary"
5 | "fmt"
6 | )
7 |
8 | // UserControlPacket User Control Message (4)
9 | // for the EventData is 4bytes.
10 | // Stream Begin(=0) 4-bytes stream ID
11 | // Stream EOF(=1) 4-bytes stream ID
12 | // StreamDry(=2) 4-bytes stream ID
13 | // SetBufferLength(=3) 8-bytes 4bytes stream ID, 4bytes buffer length.
14 | // StreamIsRecorded(=4) 4-bytes stream ID
15 | // PingRequest(=6) 4-bytes timestamp local server time
16 | // PingResponse(=7) 4-bytes timestamp received ping request.
17 | //
18 | // 3.7. User Control message
19 | // +------------------------------+-------------------------
20 | // | Event Type ( 2- bytes ) | Event Data
21 | // +------------------------------+-------------------------
22 | // Figure 5 Pay load for the ‘User Control Message’.
23 | type UserControlPacket struct {
24 |
25 | // Event type is followed by Event data.
26 | // @see: SrcPCUCEventType
27 | EventType uint16
28 | EventData uint32
29 |
30 | // ExtraData 4bytes if event_type is SetBufferLength; otherwise 0.
31 | ExtraData uint32
32 | }
33 |
34 | // Decode .
35 | func (pkt *UserControlPacket) Decode(data []uint8) (err error) {
36 | if len(data) < 6 {
37 | err = fmt.Errorf("decode usercontrol, data len is less than 6. actually is %d", len(data))
38 | return
39 | }
40 |
41 | var offset uint32
42 | pkt.EventType = binary.BigEndian.Uint16(data[offset : offset+2])
43 | offset += 2
44 |
45 | pkt.EventData = binary.BigEndian.Uint32(data[offset : offset+4])
46 | offset += 4
47 |
48 | if SrcPCUCSetBufferLength == pkt.EventType {
49 | if uint32(len(data))-offset < 4 {
50 | err = fmt.Errorf("decode user control packet extra data, len is not enough < 4")
51 | return
52 | }
53 | pkt.ExtraData = binary.BigEndian.Uint32(data[offset : offset+4])
54 | }
55 |
56 | return
57 | }
58 |
59 | // Encode .
60 | func (pkt *UserControlPacket) Encode() (data []uint8) {
61 |
62 | if SrcPCUCSetBufferLength == pkt.EventType {
63 | data = make([]uint8, 10)
64 | } else {
65 | data = make([]uint8, 6)
66 | }
67 |
68 | var offset uint32
69 |
70 | binary.BigEndian.PutUint16(data[offset:offset+2], pkt.EventType)
71 | offset += 2
72 |
73 | binary.BigEndian.PutUint32(data[offset:offset+4], pkt.EventData)
74 | offset += 4
75 |
76 | if SrcPCUCSetBufferLength == pkt.EventType {
77 | binary.BigEndian.PutUint32(data[offset:offset+4], pkt.ExtraData)
78 | offset += 4
79 | }
80 |
81 | return
82 | }
83 |
84 | // GetMessageType .
85 | func (pkt *UserControlPacket) GetMessageType() uint8 {
86 | return RtmpMsgUserControlMessage
87 | }
88 |
89 | // GetPreferCsID .
90 | func (pkt *UserControlPacket) GetPreferCsID() uint32 {
91 | return RtmpCidProtocolControl
92 | }
93 |
--------------------------------------------------------------------------------
/rtmp/pt/time_jitter.go:
--------------------------------------------------------------------------------
1 | package pt
2 |
3 | // TimeJitter time jitter detect and correct to make sure the rtmp stream is monotonically
4 | type TimeJitter struct {
5 | lastPktTime int64
6 | lastPktCorrectTime int64
7 | }
8 |
9 | // NewTimeJitter create a new time jitter
10 | func NewTimeJitter() *TimeJitter {
11 | return &TimeJitter{}
12 | }
13 |
14 | // Correct detect the time jitter and correct it.
15 | // tba, the audio timebase, used to calc the "right" delta if jitter detected.
16 | // tbv, the video timebase, used to calc the "right" delta if jitter detected.
17 | // start_at_zero whether ensure stream start at zero.
18 | // mono_increasing whether ensure stream is monotonically inscreasing.
19 | func (tj *TimeJitter) Correct(msg *Message, tba float64, tbv float64, timeJitter uint32) {
20 |
21 | if nil == msg {
22 | return
23 | }
24 |
25 | if RtmpTimeJitterFull != timeJitter {
26 | // all jitter correct features is disabled, ignore.
27 | if RtmpTimeJitterOff == timeJitter {
28 | return
29 | }
30 |
31 | // start at zero, but donot ensure monotonically increasing.
32 | if RtmpTimeJitterZero == timeJitter {
33 | // for the first time, last_pkt_correct_time is zero.
34 | // while when timestamp overflow, the timestamp become smaller,
35 | // reset the last_pkt_correct_time.
36 | if tj.lastPktCorrectTime <= 0 || tj.lastPktCorrectTime > int64(msg.Header.Timestamp) {
37 | tj.lastPktCorrectTime = int64(msg.Header.Timestamp)
38 | }
39 |
40 | msg.Header.Timestamp -= uint64(tj.lastPktCorrectTime)
41 |
42 | return
43 | }
44 | }
45 |
46 | // full jitter algorithm, do jitter correct.
47 | // set to 0 for metadata.
48 | if !msg.Header.IsAudio() && !msg.Header.IsVideo() {
49 | msg.Header.Timestamp = 0
50 | return
51 | }
52 |
53 | sampleRate := tba
54 | frameRate := tbv
55 |
56 | /**
57 | * we use a very simple time jitter detect/correct algorithm:
58 | * 1. delta: ensure the delta is positive and valid,
59 | * we set the delta to DefaultFrameTimeMs,
60 | * if the delta of time is nagative or greater than MaxJitterMs.
61 | * 2. last_pkt_time: specifies the original packet time,
62 | * is used to detect next jitter.
63 | * 3. last_pkt_correct_time: simply add the positive delta,
64 | * and enforce the time monotonically.
65 | */
66 | timeLocal := msg.Header.Timestamp
67 | delta := int64(timeLocal) - tj.lastPktTime
68 |
69 | // if jitter detected, reset the delta.
70 | if delta < 0 || delta > MaxJitterMs {
71 | // calc the right diff by audio sample rate
72 | if msg.Header.IsAudio() && sampleRate > 0 {
73 | delta = (int64)(float64(delta) * 1000.0 / sampleRate)
74 | } else if msg.Header.IsVideo() && frameRate > 0 {
75 | delta = (int64)(float64(delta) * 1.0 / frameRate)
76 | } else {
77 | delta = DefaultFrameTimeMs
78 | }
79 | }
80 |
81 | // sometimes, the time is absolute time, so correct it again.
82 | if delta < 0 || delta > MaxJitterMs {
83 | delta = DefaultFrameTimeMs
84 | }
85 |
86 | if tj.lastPktCorrectTime+delta > 0 {
87 | tj.lastPktCorrectTime = tj.lastPktCorrectTime + delta
88 | } else {
89 | tj.lastPktCorrectTime = 0
90 | }
91 |
92 | msg.Header.Timestamp = uint64(tj.lastPktCorrectTime)
93 | tj.lastPktTime = int64(timeLocal)
94 |
95 | }
96 |
--------------------------------------------------------------------------------
/rtmp_server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net"
6 | "seal/conf"
7 | "seal/rtmp/co"
8 |
9 | "github.com/calabashdad/utiltools"
10 | )
11 |
12 | type rtmpServer struct {
13 | }
14 |
15 | func (rs *rtmpServer) Start() {
16 | defer func() {
17 | if err := recover(); err != nil {
18 | log.Println(utiltools.PanicTrace())
19 | }
20 |
21 | gGuards.Done()
22 | }()
23 |
24 | listener, err := net.Listen("tcp", ":"+conf.GlobalConfInfo.Rtmp.Listen)
25 | if err != nil {
26 | log.Println("start listen at "+conf.GlobalConfInfo.Rtmp.Listen+" failed. err=", err)
27 | return
28 | }
29 | log.Println("rtmp server start liste at :" + conf.GlobalConfInfo.Rtmp.Listen)
30 |
31 | for {
32 | if netConn, err := listener.Accept(); err != nil {
33 | log.Println("rtmp server, listen accept failed, err=", err)
34 | break
35 | } else {
36 | log.Println("one rtmp connection come in, remote=", netConn.RemoteAddr())
37 | rtmpConn := co.NewRtmpConnection(netConn)
38 | go rtmpConn.Cycle()
39 | }
40 | }
41 |
42 | log.Println("rtmp server quit, err=", err)
43 | }
44 |
--------------------------------------------------------------------------------
/seal.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "log"
6 | "os"
7 | "runtime"
8 | "seal/conf"
9 | "sync"
10 | "time"
11 |
12 | "github.com/calabashdad/utiltools"
13 | )
14 |
15 | const sealVersion = "seal: 1.0.0 build-2018052801"
16 |
17 | var (
18 | configFile = flag.String("c", "./seal.yaml", "configure filename")
19 | showVersion = flag.Bool("v", false, "show version of seal")
20 | )
21 |
22 | var (
23 | gGuards sync.WaitGroup
24 | )
25 |
26 | func init() {
27 | log.SetFlags(log.Lshortfile | log.Ldate | log.Lmicroseconds)
28 | flag.Parse()
29 | }
30 |
31 | func main() {
32 | defer func() {
33 | if err := recover(); err != nil {
34 | log.Println(utiltools.PanicTrace())
35 | time.Sleep(1 * time.Second)
36 | }
37 | }()
38 |
39 | if len(os.Args) < 2 {
40 | log.Println("Show usage : ./seal --help.")
41 | return
42 | }
43 |
44 | if *showVersion {
45 | log.Println(sealVersion)
46 | return
47 | }
48 |
49 | err := conf.GlobalConfInfo.Loads(*configFile)
50 | if err != nil {
51 | log.Println("conf loads failed.err=", err)
52 | return
53 | }
54 |
55 | log.Printf("load conf file success, conf=%+v\n", conf.GlobalConfInfo)
56 |
57 | cpuNums := runtime.NumCPU()
58 | if 0 == conf.GlobalConfInfo.System.CPUNums {
59 | runtime.GOMAXPROCS(cpuNums)
60 | log.Println("app run on auto cpu nums=", cpuNums)
61 | } else {
62 | runtime.GOMAXPROCS(int(conf.GlobalConfInfo.System.CPUNums))
63 | log.Println("app run on cpu nums set by config, num=", conf.GlobalConfInfo.System.CPUNums)
64 | }
65 |
66 | gGuards.Add(1)
67 | if true {
68 | rtmpSrv := rtmpServer{}
69 | go rtmpSrv.Start()
70 | }
71 |
72 | gGuards.Add(1)
73 | if true {
74 | hlsSrv := hlsServer{}
75 | go hlsSrv.Start()
76 | }
77 |
78 | gGuards.Wait()
79 | log.Println("seal quit gracefully.")
80 | }
81 |
--------------------------------------------------------------------------------
/seal_vs_srs.md:
--------------------------------------------------------------------------------
1 | # seal vs srs performance report
2 |
3 | # players
4 | * seal https://github.com/calabashdad/seal
5 | * srs https://github.com/ossrs/srs
6 |
7 | # upload bandwidth
8 | 400Mbits/s
9 |
10 | # cpu nums
11 | * srs 1 cpu
12 | * seal 1 cpu. more than 1 is too bad for cpu usage.
13 |
14 | # srs
15 | * memory 60m statble
16 | * cpu 80%
17 |
18 | # seal
19 | * memory 1.0g statble (i can not believe it.)
20 | * cpu 90%
21 |
22 |
--------------------------------------------------------------------------------