mirror of
https://github.com/cnotch/ipchub.git
synced 2025-09-27 03:45:54 +08:00
refactoring flv muxer
This commit is contained in:
94
av/format/flv/aac_packetizer.go
Normal file
94
av/format/flv/aac_packetizer.go
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
// Copyright (c) 2019,CAOHONGJU All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package flv
|
||||||
|
|
||||||
|
import "github.com/cnotch/ipchub/av/codec"
|
||||||
|
|
||||||
|
type aacPacketizer struct {
|
||||||
|
meta *codec.AudioMeta
|
||||||
|
dataTemplate *AudioData
|
||||||
|
tagWriter TagWriter
|
||||||
|
spsMuxed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAacPacketizer(meta *codec.AudioMeta, tagWriter TagWriter) Packetizer {
|
||||||
|
ap := &aacPacketizer{
|
||||||
|
meta: meta,
|
||||||
|
tagWriter: tagWriter,
|
||||||
|
}
|
||||||
|
ap.prepareTemplate()
|
||||||
|
return ap
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *aacPacketizer) prepareTemplate() {
|
||||||
|
audioData := &AudioData{
|
||||||
|
SoundFormat: SoundFormatAAC,
|
||||||
|
AACPacketType: AACPacketTypeRawData,
|
||||||
|
Body: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ap.meta.SampleRate {
|
||||||
|
case 5512:
|
||||||
|
audioData.SoundRate = SoundRate5512
|
||||||
|
case 11025:
|
||||||
|
audioData.SoundRate = SoundRate11025
|
||||||
|
case 22050:
|
||||||
|
audioData.SoundRate = SoundRate22050
|
||||||
|
case 44100:
|
||||||
|
audioData.SoundRate = SoundRate44100
|
||||||
|
default:
|
||||||
|
audioData.SoundRate = SoundRate44100
|
||||||
|
}
|
||||||
|
|
||||||
|
if ap.meta.SampleSize == 8 {
|
||||||
|
audioData.SoundSize = SoundeSize8bit
|
||||||
|
} else {
|
||||||
|
audioData.SoundSize = SoundeSize16bit
|
||||||
|
}
|
||||||
|
|
||||||
|
if ap.meta.Channels > 1 {
|
||||||
|
audioData.SoundType = SoundTypeStereo
|
||||||
|
} else {
|
||||||
|
audioData.SoundType = SoundTypeMono
|
||||||
|
}
|
||||||
|
|
||||||
|
ap.dataTemplate = audioData
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *aacPacketizer) PacketizeSequenceHeader() error {
|
||||||
|
if ap.spsMuxed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ap.spsMuxed = true
|
||||||
|
audioData := *ap.dataTemplate
|
||||||
|
audioData.AACPacketType = AACPacketTypeSequenceHeader
|
||||||
|
audioData.Body = ap.meta.Sps
|
||||||
|
data, _ := audioData.Marshal()
|
||||||
|
|
||||||
|
tag := &Tag{
|
||||||
|
TagType: TagTypeAudio,
|
||||||
|
DataSize: uint32(len(data)),
|
||||||
|
Timestamp: 0,
|
||||||
|
StreamID: 0,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
return ap.tagWriter.WriteFlvTag(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ap *aacPacketizer) Packetize(basePts int64, frame *codec.Frame) error {
|
||||||
|
audioData := *ap.dataTemplate
|
||||||
|
audioData.Body = frame.Payload
|
||||||
|
data, _ := audioData.Marshal()
|
||||||
|
|
||||||
|
tag := &Tag{
|
||||||
|
TagType: TagTypeAudio,
|
||||||
|
DataSize: uint32(len(data)),
|
||||||
|
Timestamp: uint32(frame.AbsTimestamp-basePts) + ptsDelay,
|
||||||
|
StreamID: 0,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
return ap.tagWriter.WriteFlvTag(tag)
|
||||||
|
}
|
@@ -40,7 +40,7 @@ func TestFlvWriter(t *testing.T) {
|
|||||||
var audio codec.AudioMeta
|
var audio codec.AudioMeta
|
||||||
sdp.ParseMetadata(string(sdpraw), &video, &audio)
|
sdp.ParseMetadata(string(sdpraw), &video, &audio)
|
||||||
writer, err := NewWriter(out, 5)
|
writer, err := NewWriter(out, 5)
|
||||||
flvMuxer := NewMuxerAvcAac(video, audio, writer, xlog.L())
|
flvMuxer,_ := NewMuxer(&video, &audio, writer, xlog.L())
|
||||||
|
|
||||||
rtpDemuxer,_ := rtp.NewDemuxer(&video,&audio,flvMuxer, xlog.L())
|
rtpDemuxer,_ := rtp.NewDemuxer(&video,&audio,flvMuxer, xlog.L())
|
||||||
channels := []int{int(rtp.ChannelVideo), int(rtp.ChannelVideoControl), int(rtp.ChannelAudio), int(rtp.ChannelAudioControl)}
|
channels := []int{int(rtp.ChannelVideo), int(rtp.ChannelVideoControl), int(rtp.ChannelAudio), int(rtp.ChannelAudioControl)}
|
||||||
|
111
av/format/flv/h264_packetizer.go
Normal file
111
av/format/flv/h264_packetizer.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// Copyright (c) 2019,CAOHONGJU All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package flv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/cnotch/ipchub/av/codec"
|
||||||
|
"github.com/cnotch/ipchub/av/codec/h264"
|
||||||
|
)
|
||||||
|
|
||||||
|
type h264Packetizer struct {
|
||||||
|
meta *codec.VideoMeta
|
||||||
|
tagWriter TagWriter
|
||||||
|
spsMuxed bool
|
||||||
|
nextDts float64
|
||||||
|
dtsStep float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewH264Packetizer(meta *codec.VideoMeta, tagWriter TagWriter) Packetizer {
|
||||||
|
h264p := &h264Packetizer{
|
||||||
|
meta: meta,
|
||||||
|
tagWriter: tagWriter,
|
||||||
|
}
|
||||||
|
|
||||||
|
if meta.FrameRate > 0 {
|
||||||
|
h264p.dtsStep = 1000.0 / meta.FrameRate
|
||||||
|
}
|
||||||
|
return h264p
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h264p *h264Packetizer) PacketizeSequenceHeader() error {
|
||||||
|
if h264p.spsMuxed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !h264.MetadataIsReady(h264p.meta) {
|
||||||
|
// not enough
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
h264p.spsMuxed = true
|
||||||
|
|
||||||
|
if h264p.meta.FixedFrameRate {
|
||||||
|
h264p.dtsStep = 1000.0 / h264p.meta.FrameRate
|
||||||
|
} else { // TODO:
|
||||||
|
h264p.dtsStep = 1000.0 / 30
|
||||||
|
}
|
||||||
|
|
||||||
|
record := NewAVCDecoderConfigurationRecord(h264p.meta.Sps, h264p.meta.Pps)
|
||||||
|
body, _ := record.Marshal()
|
||||||
|
|
||||||
|
videoData := &VideoData{
|
||||||
|
FrameType: FrameTypeKeyFrame,
|
||||||
|
CodecID: CodecIDAVC,
|
||||||
|
AVCPacketType: AVCPacketTypeSequenceHeader,
|
||||||
|
CompositionTime: 0,
|
||||||
|
Body: body,
|
||||||
|
}
|
||||||
|
data, _ := videoData.Marshal()
|
||||||
|
|
||||||
|
tag := &Tag{
|
||||||
|
TagType: TagTypeVideo,
|
||||||
|
DataSize: uint32(len(data)),
|
||||||
|
Timestamp: 0,
|
||||||
|
StreamID: 0,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
return h264p.tagWriter.WriteFlvTag(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h264p *h264Packetizer) Packetize(basePts int64, frame *codec.Frame) error {
|
||||||
|
if frame.Payload[0]&0x1F == h264.NalSps {
|
||||||
|
return h264p.PacketizeSequenceHeader()
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame.Payload[0]&0x1F == h264.NalPps {
|
||||||
|
return h264p.PacketizeSequenceHeader()
|
||||||
|
}
|
||||||
|
|
||||||
|
dts := int64(h264p.nextDts)
|
||||||
|
h264p.nextDts += h264p.dtsStep
|
||||||
|
pts := frame.AbsTimestamp - basePts + ptsDelay
|
||||||
|
if dts > pts {
|
||||||
|
pts = dts
|
||||||
|
}
|
||||||
|
|
||||||
|
videoData := &VideoData{
|
||||||
|
FrameType: FrameTypeInterFrame,
|
||||||
|
CodecID: CodecIDAVC,
|
||||||
|
AVCPacketType: AVCPacketTypeNALU,
|
||||||
|
CompositionTime: uint32(pts - dts),
|
||||||
|
Body: frame.Payload,
|
||||||
|
}
|
||||||
|
|
||||||
|
if frame.Payload[0]&0x1F == h264.NalIdrSlice {
|
||||||
|
videoData.FrameType = FrameTypeKeyFrame
|
||||||
|
}
|
||||||
|
data, _ := videoData.Marshal()
|
||||||
|
|
||||||
|
tag := &Tag{
|
||||||
|
TagType: TagTypeVideo,
|
||||||
|
DataSize: uint32(len(data)),
|
||||||
|
Timestamp: uint32(dts),
|
||||||
|
StreamID: 0,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
return h264p.tagWriter.WriteFlvTag(tag)
|
||||||
|
}
|
222
av/format/flv/muxer.go
Normal file
222
av/format/flv/muxer.go
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
// Copyright (c) 2019,CAOHONGJU All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package flv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"runtime/debug"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cnotch/ipchub/av/codec"
|
||||||
|
"github.com/cnotch/ipchub/av/format/amf"
|
||||||
|
"github.com/cnotch/queue"
|
||||||
|
"github.com/cnotch/xlog"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Packetizer 封包器
|
||||||
|
type Packetizer interface {
|
||||||
|
PacketizeSequenceHeader() error
|
||||||
|
Packetize(basePts int64, frame *codec.Frame) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type emptyPacketizer struct{}
|
||||||
|
|
||||||
|
func (emptyPacketizer) PacketizeSequenceHeader() error { return nil }
|
||||||
|
func (emptyPacketizer) Packetize(basePts int64, frame *codec.Frame) error { return nil }
|
||||||
|
|
||||||
|
// 网络播放时 PTS(Presentation Time Stamp)的延时
|
||||||
|
// 影响视频 Tag 的 CTS 和音频的 DTS(Decoding Time Stamp)
|
||||||
|
const (
|
||||||
|
ptsDelay = 1000
|
||||||
|
)
|
||||||
|
|
||||||
|
// Muxer flv muxer from av.Frame(H264[+AAC])
|
||||||
|
type Muxer struct {
|
||||||
|
videoMeta *codec.VideoMeta
|
||||||
|
audioMeta *codec.AudioMeta
|
||||||
|
vp Packetizer
|
||||||
|
ap Packetizer
|
||||||
|
typeFlags byte
|
||||||
|
recvQueue *queue.SyncQueue
|
||||||
|
tagWriter TagWriter
|
||||||
|
closed bool
|
||||||
|
|
||||||
|
logger *xlog.Logger // 日志对象
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMuxer .
|
||||||
|
func NewMuxer(videoMeta *codec.VideoMeta, audioMeta *codec.AudioMeta, tagWriter TagWriter, logger *xlog.Logger) (*Muxer, error) {
|
||||||
|
muxer := &Muxer{
|
||||||
|
recvQueue: queue.NewSyncQueue(),
|
||||||
|
videoMeta: videoMeta,
|
||||||
|
audioMeta: audioMeta,
|
||||||
|
vp: emptyPacketizer{},
|
||||||
|
ap: emptyPacketizer{},
|
||||||
|
typeFlags: byte(TypeFlagsVideo),
|
||||||
|
tagWriter: tagWriter,
|
||||||
|
closed: false,
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
switch videoMeta.Codec {
|
||||||
|
case "H264":
|
||||||
|
muxer.vp = NewH264Packetizer(videoMeta, tagWriter)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("flv muxer unsupport video codec type:%s", videoMeta.Codec)
|
||||||
|
}
|
||||||
|
|
||||||
|
if audioMeta.Codec == "AAC" {
|
||||||
|
muxer.typeFlags |= TypeFlagsAudio
|
||||||
|
muxer.ap = NewAacPacketizer(audioMeta, tagWriter)
|
||||||
|
}
|
||||||
|
|
||||||
|
go muxer.process()
|
||||||
|
return muxer, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteFrame .
|
||||||
|
func (muxer *Muxer) WriteFrame(frame *codec.Frame) error {
|
||||||
|
muxer.recvQueue.Push(frame)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close .
|
||||||
|
func (muxer *Muxer) Close() error {
|
||||||
|
if muxer.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
muxer.closed = true
|
||||||
|
muxer.recvQueue.Signal()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeFlags 返回 flv header 中的 TypeFlags
|
||||||
|
func (muxer *Muxer) TypeFlags() byte {
|
||||||
|
return muxer.typeFlags
|
||||||
|
}
|
||||||
|
|
||||||
|
func (muxer *Muxer) process() {
|
||||||
|
defer func() {
|
||||||
|
defer func() { // 避免 handler 再 panic
|
||||||
|
recover()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
muxer.logger.Errorf("flvmuxer routine panic;r = %v \n %s", r, debug.Stack())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尽早通知GC,回收内存
|
||||||
|
muxer.recvQueue.Reset()
|
||||||
|
}()
|
||||||
|
|
||||||
|
var basePts int64
|
||||||
|
for !muxer.closed {
|
||||||
|
f := muxer.recvQueue.Pop()
|
||||||
|
if f == nil {
|
||||||
|
if !muxer.closed {
|
||||||
|
muxer.logger.Warn("flvmuxer:receive nil frame")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
frame := f.(*codec.Frame)
|
||||||
|
if basePts == 0 {
|
||||||
|
basePts = frame.AbsTimestamp
|
||||||
|
muxer.muxMetadataTag()
|
||||||
|
muxer.vp.PacketizeSequenceHeader()
|
||||||
|
muxer.ap.PacketizeSequenceHeader()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch frame.MediaType {
|
||||||
|
case codec.MediaTypeVideo:
|
||||||
|
if err := muxer.vp.Packetize(basePts, frame); err != nil {
|
||||||
|
muxer.logger.Errorf("flvmuxer: muxVideoTag error - %s", err.Error())
|
||||||
|
}
|
||||||
|
case codec.MediaTypeAudio:
|
||||||
|
if err := muxer.ap.Packetize(basePts, frame); err != nil {
|
||||||
|
muxer.logger.Errorf("flvmuxer: muxAudioTag error - %s", err.Error())
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (muxer *Muxer) muxMetadataTag() error {
|
||||||
|
properties := make(amf.EcmaArray, 0, 12)
|
||||||
|
|
||||||
|
properties = append(properties,
|
||||||
|
amf.ObjectProperty{
|
||||||
|
Name: "creator",
|
||||||
|
Value: "ipchub stream media server"})
|
||||||
|
properties = append(properties,
|
||||||
|
amf.ObjectProperty{
|
||||||
|
Name: MetaDataCreationDate,
|
||||||
|
Value: time.Now().Format(time.RFC3339)})
|
||||||
|
|
||||||
|
if muxer.typeFlags&TypeFlagsAudio > 0 {
|
||||||
|
properties = append(properties,
|
||||||
|
amf.ObjectProperty{
|
||||||
|
Name: MetaDataAudioCodecID,
|
||||||
|
Value: SoundFormatAAC})
|
||||||
|
properties = append(properties,
|
||||||
|
amf.ObjectProperty{
|
||||||
|
Name: MetaDataAudioDateRate,
|
||||||
|
Value: muxer.audioMeta.DataRate})
|
||||||
|
properties = append(properties,
|
||||||
|
amf.ObjectProperty{
|
||||||
|
Name: MetaDataAudioSampleRate,
|
||||||
|
Value: muxer.audioMeta.SampleRate})
|
||||||
|
properties = append(properties,
|
||||||
|
amf.ObjectProperty{
|
||||||
|
Name: MetaDataAudioSampleSize,
|
||||||
|
Value: muxer.audioMeta.SampleSize})
|
||||||
|
properties = append(properties,
|
||||||
|
amf.ObjectProperty{
|
||||||
|
Name: MetaDataStereo,
|
||||||
|
Value: muxer.audioMeta.Channels > 1})
|
||||||
|
}
|
||||||
|
|
||||||
|
vcodecID := CodecIDAVC
|
||||||
|
if muxer.videoMeta.Codec == "H265" {
|
||||||
|
vcodecID = CodecIDHEVC
|
||||||
|
}
|
||||||
|
|
||||||
|
properties = append(properties,
|
||||||
|
amf.ObjectProperty{
|
||||||
|
Name: MetaDataVideoCodecID,
|
||||||
|
Value: vcodecID})
|
||||||
|
properties = append(properties,
|
||||||
|
amf.ObjectProperty{
|
||||||
|
Name: MetaDataVideoDataRate,
|
||||||
|
Value: muxer.videoMeta.DataRate})
|
||||||
|
properties = append(properties,
|
||||||
|
amf.ObjectProperty{
|
||||||
|
Name: MetaDataFrameRate,
|
||||||
|
Value: muxer.videoMeta.FrameRate})
|
||||||
|
properties = append(properties,
|
||||||
|
amf.ObjectProperty{
|
||||||
|
Name: MetaDataWidth,
|
||||||
|
Value: muxer.videoMeta.Width})
|
||||||
|
properties = append(properties,
|
||||||
|
amf.ObjectProperty{
|
||||||
|
Name: MetaDataHeight,
|
||||||
|
Value: muxer.videoMeta.Height})
|
||||||
|
|
||||||
|
scriptData := ScriptData{
|
||||||
|
Name: ScriptOnMetaData,
|
||||||
|
Value: properties,
|
||||||
|
}
|
||||||
|
data, _ := scriptData.Marshal()
|
||||||
|
|
||||||
|
tag := &Tag{
|
||||||
|
TagType: TagTypeAmf0Data,
|
||||||
|
DataSize: uint32(len(data)),
|
||||||
|
Timestamp: 0,
|
||||||
|
StreamID: 0,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
|
||||||
|
return muxer.tagWriter.WriteFlvTag(tag)
|
||||||
|
}
|
@@ -1,362 +0,0 @@
|
|||||||
// Copyright (c) 2019,CAOHONGJU All rights reserved.
|
|
||||||
// Use of this source code is governed by a MIT-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package flv
|
|
||||||
|
|
||||||
import (
|
|
||||||
"runtime/debug"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/cnotch/ipchub/av/codec"
|
|
||||||
"github.com/cnotch/ipchub/av/codec/h264"
|
|
||||||
"github.com/cnotch/ipchub/av/format/amf"
|
|
||||||
"github.com/cnotch/queue"
|
|
||||||
"github.com/cnotch/xlog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 网络播放时 PTS(Presentation Time Stamp)的延时
|
|
||||||
// 影响视频 Tag 的 CTS 和音频的 DTS(Decoding Time Stamp)
|
|
||||||
const (
|
|
||||||
dtsDelay = 0
|
|
||||||
ptsDelay = 1000
|
|
||||||
)
|
|
||||||
|
|
||||||
// MuxerAvcAac flv muxer from av.Frame(H264[+AAC])
|
|
||||||
type MuxerAvcAac struct {
|
|
||||||
videoMeta codec.VideoMeta
|
|
||||||
audioMeta codec.AudioMeta
|
|
||||||
typeFlags byte
|
|
||||||
audioDataTemplate *AudioData
|
|
||||||
recvQueue *queue.SyncQueue
|
|
||||||
tagWriter TagWriter
|
|
||||||
closed bool
|
|
||||||
spsMuxed bool
|
|
||||||
basePts int64
|
|
||||||
nextDts float64
|
|
||||||
dtsStep float64
|
|
||||||
logger *xlog.Logger // 日志对象
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMuxerAvcAac .
|
|
||||||
func NewMuxerAvcAac(videoMeta codec.VideoMeta, audioMeta codec.AudioMeta, tagWriter TagWriter, logger *xlog.Logger) *MuxerAvcAac {
|
|
||||||
muxer := &MuxerAvcAac{
|
|
||||||
recvQueue: queue.NewSyncQueue(),
|
|
||||||
videoMeta: videoMeta,
|
|
||||||
audioMeta: audioMeta,
|
|
||||||
typeFlags: byte(TypeFlagsVideo),
|
|
||||||
tagWriter: tagWriter,
|
|
||||||
closed: false,
|
|
||||||
nextDts: dtsDelay,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
|
|
||||||
if videoMeta.FrameRate > 0 {
|
|
||||||
muxer.dtsStep = 1000.0 / videoMeta.FrameRate
|
|
||||||
}
|
|
||||||
if audioMeta.Codec == "AAC" {
|
|
||||||
muxer.typeFlags |= TypeFlagsAudio
|
|
||||||
muxer.prepareTemplate()
|
|
||||||
}
|
|
||||||
|
|
||||||
go muxer.process()
|
|
||||||
return muxer
|
|
||||||
}
|
|
||||||
|
|
||||||
// WriteFrame .
|
|
||||||
func (muxer *MuxerAvcAac) WriteFrame(frame *codec.Frame) error {
|
|
||||||
muxer.recvQueue.Push(frame)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close .
|
|
||||||
func (muxer *MuxerAvcAac) Close() error {
|
|
||||||
if muxer.closed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
muxer.closed = true
|
|
||||||
muxer.recvQueue.Signal()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TypeFlags 返回 flv header 中的 TypeFlags
|
|
||||||
func (muxer *MuxerAvcAac) TypeFlags() byte {
|
|
||||||
return muxer.typeFlags
|
|
||||||
}
|
|
||||||
|
|
||||||
func (muxer *MuxerAvcAac) process() {
|
|
||||||
defer func() {
|
|
||||||
defer func() { // 避免 handler 再 panic
|
|
||||||
recover()
|
|
||||||
}()
|
|
||||||
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
muxer.logger.Errorf("flvmuxer routine panic;r = %v \n %s", r, debug.Stack())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尽早通知GC,回收内存
|
|
||||||
muxer.recvQueue.Reset()
|
|
||||||
}()
|
|
||||||
|
|
||||||
muxer.muxMetadataTag()
|
|
||||||
muxer.muxSequenceHeaderTag()
|
|
||||||
|
|
||||||
for !muxer.closed {
|
|
||||||
f := muxer.recvQueue.Pop()
|
|
||||||
if f == nil {
|
|
||||||
if !muxer.closed {
|
|
||||||
muxer.logger.Warn("flvmuxer:receive nil frame")
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
frame := f.(*codec.Frame)
|
|
||||||
if muxer.basePts == 0 {
|
|
||||||
muxer.basePts = frame.AbsTimestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
if frame.MediaType == codec.MediaTypeVideo {
|
|
||||||
if err := muxer.muxVideoTag(frame); err != nil {
|
|
||||||
muxer.logger.Errorf("flvmuxer: muxVideoTag error - %s", err.Error())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if err := muxer.muxAudioTag(frame); err != nil {
|
|
||||||
muxer.logger.Errorf("flvmuxer: muxAudioTag error - %s", err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (muxer *MuxerAvcAac) muxVideoTag(frame *codec.Frame) error {
|
|
||||||
if frame.Payload[0]&0x1F == h264.NalSps {
|
|
||||||
if len(muxer.videoMeta.Sps) == 0 {
|
|
||||||
muxer.videoMeta.Sps = frame.Payload
|
|
||||||
}
|
|
||||||
return muxer.muxSequenceHeaderTag()
|
|
||||||
}
|
|
||||||
|
|
||||||
if frame.Payload[0]&0x1F == h264.NalPps {
|
|
||||||
if len(muxer.videoMeta.Pps) == 0 {
|
|
||||||
muxer.videoMeta.Pps = frame.Payload
|
|
||||||
}
|
|
||||||
return muxer.muxSequenceHeaderTag()
|
|
||||||
}
|
|
||||||
|
|
||||||
dts := int64(muxer.nextDts)
|
|
||||||
muxer.nextDts += muxer.dtsStep
|
|
||||||
pts := frame.AbsTimestamp - muxer.basePts + ptsDelay
|
|
||||||
if dts > pts {
|
|
||||||
pts = dts
|
|
||||||
}
|
|
||||||
|
|
||||||
videoData := &VideoData{
|
|
||||||
FrameType: FrameTypeInterFrame,
|
|
||||||
CodecID: CodecIDAVC,
|
|
||||||
AVCPacketType: AVCPacketTypeNALU,
|
|
||||||
CompositionTime: uint32(pts - dts),
|
|
||||||
Body: frame.Payload,
|
|
||||||
}
|
|
||||||
|
|
||||||
if frame.Payload[0]&0x1F == h264.NalIdrSlice {
|
|
||||||
videoData.FrameType = FrameTypeKeyFrame
|
|
||||||
}
|
|
||||||
data, _ := videoData.Marshal()
|
|
||||||
|
|
||||||
tag := &Tag{
|
|
||||||
TagType: TagTypeVideo,
|
|
||||||
DataSize: uint32(len(data)),
|
|
||||||
Timestamp: uint32(dts),
|
|
||||||
StreamID: 0,
|
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
|
|
||||||
return muxer.tagWriter.WriteFlvTag(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (muxer *MuxerAvcAac) muxAudioTag(frame *codec.Frame) error {
|
|
||||||
audioData := *muxer.audioDataTemplate
|
|
||||||
audioData.Body = frame.Payload
|
|
||||||
data, _ := audioData.Marshal()
|
|
||||||
|
|
||||||
tag := &Tag{
|
|
||||||
TagType: TagTypeAudio,
|
|
||||||
DataSize: uint32(len(data)),
|
|
||||||
Timestamp: uint32(frame.AbsTimestamp-muxer.basePts) + ptsDelay,
|
|
||||||
StreamID: 0,
|
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
return muxer.tagWriter.WriteFlvTag(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (muxer *MuxerAvcAac) muxMetadataTag() error {
|
|
||||||
properties := make(amf.EcmaArray, 0, 12)
|
|
||||||
|
|
||||||
properties = append(properties,
|
|
||||||
amf.ObjectProperty{
|
|
||||||
Name: "creator",
|
|
||||||
Value: "ipchub stream media server"})
|
|
||||||
properties = append(properties,
|
|
||||||
amf.ObjectProperty{
|
|
||||||
Name: MetaDataCreationDate,
|
|
||||||
Value: time.Now().Format(time.RFC3339)})
|
|
||||||
|
|
||||||
if muxer.typeFlags&TypeFlagsAudio > 0 {
|
|
||||||
properties = append(properties,
|
|
||||||
amf.ObjectProperty{
|
|
||||||
Name: MetaDataAudioCodecID,
|
|
||||||
Value: SoundFormatAAC})
|
|
||||||
properties = append(properties,
|
|
||||||
amf.ObjectProperty{
|
|
||||||
Name: MetaDataAudioDateRate,
|
|
||||||
Value: muxer.audioMeta.DataRate})
|
|
||||||
properties = append(properties,
|
|
||||||
amf.ObjectProperty{
|
|
||||||
Name: MetaDataAudioSampleRate,
|
|
||||||
Value: muxer.audioMeta.SampleRate})
|
|
||||||
properties = append(properties,
|
|
||||||
amf.ObjectProperty{
|
|
||||||
Name: MetaDataAudioSampleSize,
|
|
||||||
Value: muxer.audioMeta.SampleSize})
|
|
||||||
properties = append(properties,
|
|
||||||
amf.ObjectProperty{
|
|
||||||
Name: MetaDataStereo,
|
|
||||||
Value: muxer.audioMeta.Channels > 1})
|
|
||||||
}
|
|
||||||
|
|
||||||
properties = append(properties,
|
|
||||||
amf.ObjectProperty{
|
|
||||||
Name: MetaDataVideoCodecID,
|
|
||||||
Value: CodecIDAVC})
|
|
||||||
properties = append(properties,
|
|
||||||
amf.ObjectProperty{
|
|
||||||
Name: MetaDataVideoDataRate,
|
|
||||||
Value: muxer.videoMeta.DataRate})
|
|
||||||
properties = append(properties,
|
|
||||||
amf.ObjectProperty{
|
|
||||||
Name: MetaDataFrameRate,
|
|
||||||
Value: muxer.videoMeta.FrameRate})
|
|
||||||
properties = append(properties,
|
|
||||||
amf.ObjectProperty{
|
|
||||||
Name: MetaDataWidth,
|
|
||||||
Value: muxer.videoMeta.Width})
|
|
||||||
properties = append(properties,
|
|
||||||
amf.ObjectProperty{
|
|
||||||
Name: MetaDataHeight,
|
|
||||||
Value: muxer.videoMeta.Height})
|
|
||||||
|
|
||||||
scriptData := ScriptData{
|
|
||||||
Name: ScriptOnMetaData,
|
|
||||||
Value: properties,
|
|
||||||
}
|
|
||||||
data, _ := scriptData.Marshal()
|
|
||||||
|
|
||||||
tag := &Tag{
|
|
||||||
TagType: TagTypeAmf0Data,
|
|
||||||
DataSize: uint32(len(data)),
|
|
||||||
Timestamp: 0,
|
|
||||||
StreamID: 0,
|
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
|
|
||||||
return muxer.tagWriter.WriteFlvTag(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (muxer *MuxerAvcAac) muxSequenceHeaderTag() error {
|
|
||||||
if muxer.spsMuxed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if !h264.MetadataIsReady(&muxer.videoMeta) {
|
|
||||||
// not enough
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if muxer.videoMeta.FixedFrameRate {
|
|
||||||
muxer.dtsStep = 1000.0 / muxer.videoMeta.FrameRate
|
|
||||||
} else { // TODO:
|
|
||||||
muxer.dtsStep = 1000.0 / 30
|
|
||||||
}
|
|
||||||
muxer.spsMuxed = true
|
|
||||||
|
|
||||||
record := NewAVCDecoderConfigurationRecord(muxer.videoMeta.Sps, muxer.videoMeta.Pps)
|
|
||||||
body, _ := record.Marshal()
|
|
||||||
|
|
||||||
videoData := &VideoData{
|
|
||||||
FrameType: FrameTypeKeyFrame,
|
|
||||||
CodecID: CodecIDAVC,
|
|
||||||
AVCPacketType: AVCPacketTypeSequenceHeader,
|
|
||||||
CompositionTime: 0,
|
|
||||||
Body: body,
|
|
||||||
}
|
|
||||||
data, _ := videoData.Marshal()
|
|
||||||
|
|
||||||
tag := &Tag{
|
|
||||||
TagType: TagTypeVideo,
|
|
||||||
DataSize: uint32(len(data)),
|
|
||||||
Timestamp: 0,
|
|
||||||
StreamID: 0,
|
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := muxer.tagWriter.WriteFlvTag(tag); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return muxer.muxAudioSequenceHeaderTag()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (muxer *MuxerAvcAac) muxAudioSequenceHeaderTag() error {
|
|
||||||
if muxer.typeFlags&TypeFlagsAudio == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
audioData := *muxer.audioDataTemplate
|
|
||||||
audioData.AACPacketType = AACPacketTypeSequenceHeader
|
|
||||||
audioData.Body = muxer.audioMeta.Sps
|
|
||||||
data, _ := audioData.Marshal()
|
|
||||||
|
|
||||||
tag := &Tag{
|
|
||||||
TagType: TagTypeAudio,
|
|
||||||
DataSize: uint32(len(data)),
|
|
||||||
Timestamp: 0,
|
|
||||||
StreamID: 0,
|
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
return muxer.tagWriter.WriteFlvTag(tag)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (muxer *MuxerAvcAac) prepareTemplate() {
|
|
||||||
audioData := &AudioData{
|
|
||||||
SoundFormat: SoundFormatAAC,
|
|
||||||
AACPacketType: AACPacketTypeRawData,
|
|
||||||
Body: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch muxer.audioMeta.SampleRate {
|
|
||||||
case 5512:
|
|
||||||
audioData.SoundRate = SoundRate5512
|
|
||||||
case 11025:
|
|
||||||
audioData.SoundRate = SoundRate11025
|
|
||||||
case 22050:
|
|
||||||
audioData.SoundRate = SoundRate22050
|
|
||||||
case 44100:
|
|
||||||
audioData.SoundRate = SoundRate44100
|
|
||||||
default:
|
|
||||||
audioData.SoundRate = SoundRate44100
|
|
||||||
}
|
|
||||||
|
|
||||||
if muxer.audioMeta.SampleSize == 8 {
|
|
||||||
audioData.SoundSize = SoundeSize8bit
|
|
||||||
} else {
|
|
||||||
audioData.SoundSize = SoundeSize16bit
|
|
||||||
}
|
|
||||||
|
|
||||||
if muxer.audioMeta.Channels > 1 {
|
|
||||||
audioData.SoundType = SoundTypeStereo
|
|
||||||
} else {
|
|
||||||
audioData.SoundType = SoundTypeMono
|
|
||||||
}
|
|
||||||
|
|
||||||
muxer.audioDataTemplate = audioData
|
|
||||||
}
|
|
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type aacDepacketizer struct {
|
type aacDepacketizer struct {
|
||||||
audio *codec.AudioMeta
|
meta *codec.AudioMeta
|
||||||
w codec.FrameWriter
|
w codec.FrameWriter
|
||||||
sizeLength int
|
sizeLength int
|
||||||
indexLength int
|
indexLength int
|
||||||
@@ -19,14 +19,14 @@ type aacDepacketizer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewAacDepacketizer 实例化 AAC 解包器
|
// NewAacDepacketizer 实例化 AAC 解包器
|
||||||
func NewAacDepacketizer(audio *codec.AudioMeta, w codec.FrameWriter) depacketizer {
|
func NewAacDepacketizer(meta *codec.AudioMeta, w codec.FrameWriter) Depacketizer {
|
||||||
fe := &aacDepacketizer{
|
fe := &aacDepacketizer{
|
||||||
audio: audio,
|
meta: meta,
|
||||||
w: w,
|
w: w,
|
||||||
sizeLength: 13,
|
sizeLength: 13,
|
||||||
indexLength: 3,
|
indexLength: 3,
|
||||||
}
|
}
|
||||||
fe.syncClock.RTPTimeUnit = 1000.0 / float64(audio.SampleRate)
|
fe.syncClock.RTPTimeUnit = 1000.0 / float64(meta.SampleRate)
|
||||||
return fe
|
return fe
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -13,8 +13,8 @@ import (
|
|||||||
"github.com/cnotch/xlog"
|
"github.com/cnotch/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
// depacketizer 解包器
|
// Depacketizer 解包器
|
||||||
type depacketizer interface {
|
type Depacketizer interface {
|
||||||
Control(p *Packet) error
|
Control(p *Packet) error
|
||||||
Depacketize(p *Packet) error
|
Depacketize(p *Packet) error
|
||||||
}
|
}
|
||||||
@@ -37,15 +37,14 @@ func NewDemuxer(video *codec.VideoMeta, audio *codec.AudioMeta, fw codec.FrameWr
|
|||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
var videoDepacketizer, audioDepacketizer depacketizer
|
var videoDepacketizer, audioDepacketizer Depacketizer
|
||||||
switch video.Codec {
|
switch video.Codec {
|
||||||
case "H264":
|
case "H264":
|
||||||
videoDepacketizer = NewH264Depacketizer(video, fw)
|
videoDepacketizer = NewH264Depacketizer(video, fw)
|
||||||
case "H265":
|
case "H265":
|
||||||
videoDepacketizer = NewH265Depacketizer(video, fw)
|
videoDepacketizer = NewH265Depacketizer(video, fw)
|
||||||
}
|
default:
|
||||||
if videoDepacketizer == nil {
|
return nil, fmt.Errorf("rtp demuxer unsupport video codec type:%s", video.Codec)
|
||||||
return nil, fmt.Errorf("Unsupport video codec type:%s", video.Codec)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fc.depacketizeFuncs[ChannelVideo] = videoDepacketizer.Depacketize
|
fc.depacketizeFuncs[ChannelVideo] = videoDepacketizer.Depacketize
|
||||||
|
@@ -13,19 +13,19 @@ import (
|
|||||||
|
|
||||||
type h264Depacketizer struct {
|
type h264Depacketizer struct {
|
||||||
fragments []*Packet // 分片包
|
fragments []*Packet // 分片包
|
||||||
video *codec.VideoMeta
|
meta *codec.VideoMeta
|
||||||
w codec.FrameWriter
|
w codec.FrameWriter
|
||||||
syncClock SyncClock
|
syncClock SyncClock
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewH264Depacketizer 实例化 H264 帧提取器
|
// NewH264Depacketizer 实例化 H264 帧提取器
|
||||||
func NewH264Depacketizer(video *codec.VideoMeta, w codec.FrameWriter) depacketizer {
|
func NewH264Depacketizer(meta *codec.VideoMeta, w codec.FrameWriter) Depacketizer {
|
||||||
fe := &h264Depacketizer{
|
fe := &h264Depacketizer{
|
||||||
video: video,
|
meta: meta,
|
||||||
fragments: make([]*Packet, 0, 16),
|
fragments: make([]*Packet, 0, 16),
|
||||||
w: w,
|
w: w,
|
||||||
}
|
}
|
||||||
fe.syncClock.RTPTimeUnit = 1000.0 / float64(video.ClockRate)
|
fe.syncClock.RTPTimeUnit = 1000.0 / float64(meta.ClockRate)
|
||||||
return fe
|
return fe
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,12 +199,12 @@ func (h264dp *h264Depacketizer) writeFrame(frame *codec.Frame) error {
|
|||||||
nalType := frame.Payload[0] & 0x1f
|
nalType := frame.Payload[0] & 0x1f
|
||||||
switch nalType {
|
switch nalType {
|
||||||
case h264.NalSps:
|
case h264.NalSps:
|
||||||
if len(h264dp.video.Sps) == 0 {
|
if len(h264dp.meta.Sps) == 0 {
|
||||||
h264dp.video.Sps = frame.Payload
|
h264dp.meta.Sps = frame.Payload
|
||||||
}
|
}
|
||||||
case h264.NalPps:
|
case h264.NalPps:
|
||||||
if len(h264dp.video.Pps) == 0 {
|
if len(h264dp.meta.Pps) == 0 {
|
||||||
h264dp.video.Pps = frame.Payload
|
h264dp.meta.Pps = frame.Payload
|
||||||
}
|
}
|
||||||
case h264.NalFillerData: // ?ignore...
|
case h264.NalFillerData: // ?ignore...
|
||||||
return nil
|
return nil
|
||||||
|
@@ -11,19 +11,19 @@ import (
|
|||||||
|
|
||||||
type h265Depacketizer struct {
|
type h265Depacketizer struct {
|
||||||
fragments []*Packet // 分片包
|
fragments []*Packet // 分片包
|
||||||
video *codec.VideoMeta
|
meta *codec.VideoMeta
|
||||||
w codec.FrameWriter
|
w codec.FrameWriter
|
||||||
syncClock SyncClock
|
syncClock SyncClock
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewH265Depacketizer 实例化 H265 帧提取器
|
// NewH265Depacketizer 实例化 H265 帧提取器
|
||||||
func NewH265Depacketizer(video *codec.VideoMeta, w codec.FrameWriter) depacketizer {
|
func NewH265Depacketizer(meta *codec.VideoMeta, w codec.FrameWriter) Depacketizer {
|
||||||
fe := &h265Depacketizer{
|
fe := &h265Depacketizer{
|
||||||
video: video,
|
meta: meta,
|
||||||
fragments: make([]*Packet, 0, 16),
|
fragments: make([]*Packet, 0, 16),
|
||||||
w: w,
|
w: w,
|
||||||
}
|
}
|
||||||
fe.syncClock.RTPTimeUnit = 1000.0 / float64(video.ClockRate)
|
fe.syncClock.RTPTimeUnit = 1000.0 / float64(meta.ClockRate)
|
||||||
return fe
|
return fe
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,16 +178,16 @@ func (h265dp *h265Depacketizer) writeFrame(frame *codec.Frame) error {
|
|||||||
nalType := (frame.Payload[0] >> 1) & 0x3f
|
nalType := (frame.Payload[0] >> 1) & 0x3f
|
||||||
switch nalType {
|
switch nalType {
|
||||||
case hevc.NalVps:
|
case hevc.NalVps:
|
||||||
if len(h265dp.video.Vps) == 0 {
|
if len(h265dp.meta.Vps) == 0 {
|
||||||
h265dp.video.Vps = frame.Payload
|
h265dp.meta.Vps = frame.Payload
|
||||||
}
|
}
|
||||||
case hevc.NalSps:
|
case hevc.NalSps:
|
||||||
if len(h265dp.video.Sps) == 0 {
|
if len(h265dp.meta.Sps) == 0 {
|
||||||
h265dp.video.Sps = frame.Payload
|
h265dp.meta.Sps = frame.Payload
|
||||||
}
|
}
|
||||||
case hevc.NalPps:
|
case hevc.NalPps:
|
||||||
if len(h265dp.video.Pps) == 0 {
|
if len(h265dp.meta.Pps) == 0 {
|
||||||
h265dp.video.Pps = frame.Payload
|
h265dp.meta.Pps = frame.Payload
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return h265dp.w.WriteFrame(frame)
|
return h265dp.w.WriteFrame(frame)
|
||||||
|
@@ -105,7 +105,7 @@ func (s *Stream) prepareOtherStream() {
|
|||||||
s.flvCache = emptyCache{}
|
s.flvCache = emptyCache{}
|
||||||
s.flvMuxer = emptyFlvMuxer{}
|
s.flvMuxer = emptyFlvMuxer{}
|
||||||
|
|
||||||
// prepare rtp.Packet -> av.Frame
|
// prepare rtp.Packet -> codec.Frame
|
||||||
var err error
|
var err error
|
||||||
if s.rtpDemuxer, err = rtp.NewDemuxer(&s.Video, &s.Audio,
|
if s.rtpDemuxer, err = rtp.NewDemuxer(&s.Video, &s.Audio,
|
||||||
s, s.logger.With(xlog.Fields(xlog.F("extra", "rtp2frame")))); err != nil {
|
s, s.logger.With(xlog.Fields(xlog.F("extra", "rtp2frame")))); err != nil {
|
||||||
@@ -113,14 +113,15 @@ func (s *Stream) prepareOtherStream() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare av.Frame -> flv.Tag
|
// prepare codec.Frame -> flv.Tag
|
||||||
if s.Video.Codec == "H264" {
|
var flvMuxer *flv.Muxer
|
||||||
|
if flvMuxer, err = flv.NewMuxer(&s.Video, &s.Audio,
|
||||||
|
s, s.logger.With(xlog.Fields(xlog.F("extra", "frame2flv")))); err == nil {
|
||||||
s.flvCache = cache.NewFlvCache(config.CacheGop())
|
s.flvCache = cache.NewFlvCache(config.CacheGop())
|
||||||
s.flvMuxer = flv.NewMuxerAvcAac(s.Video, s.Audio,
|
s.flvMuxer = flvMuxer
|
||||||
s, s.logger.With(xlog.Fields(xlog.F("extra", "frame2flv"))))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// prepare av.Frame -> mpegts.Frame
|
// prepare codec.Frame -> mpegts.Frame
|
||||||
if s.Video.Codec == "H264" {
|
if s.Video.Codec == "H264" {
|
||||||
hlsPlaylist := hls.NewPlaylist()
|
hlsPlaylist := hls.NewPlaylist()
|
||||||
sg, err := hls.NewSegmentGenerator(hlsPlaylist, s.path,
|
sg, err := hls.NewSegmentGenerator(hlsPlaylist, s.path,
|
||||||
|
Reference in New Issue
Block a user