├── 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 | --------------------------------------------------------------------------------