2018-02-13 11:39:26 +03:00
|
|
|
/*
|
|
|
|
NAME
|
2018-05-31 12:54:20 +03:00
|
|
|
flv_generator.go
|
2018-02-13 11:39:26 +03:00
|
|
|
|
|
|
|
DESCRIPTION
|
|
|
|
See Readme.md
|
|
|
|
|
|
|
|
AUTHOR
|
2018-08-19 13:59:22 +03:00
|
|
|
Dan Kortschak <dan@ausocean.org>
|
2018-02-28 16:46:59 +03:00
|
|
|
Saxon Nelson-Milton <saxon@ausocean.org>
|
2018-02-13 11:39:26 +03:00
|
|
|
|
|
|
|
LICENSE
|
2018-05-31 12:54:20 +03:00
|
|
|
flv_generator.go is Copyright (C) 2017 the Australian Ocean Lab (AusOcean)
|
2018-02-13 11:39:26 +03:00
|
|
|
|
|
|
|
It is free software: you can redistribute it and/or modify them
|
|
|
|
under the terms of the GNU General Public License as published by the
|
|
|
|
Free Software Foundation, either version 3 of the License, or (at your
|
|
|
|
option) any later version.
|
|
|
|
|
|
|
|
It is distributed in the hope that it will be useful, but WITHOUT
|
|
|
|
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
|
|
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
|
|
for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
2018-08-19 13:59:22 +03:00
|
|
|
along with revid in gpl.txt. If not, see http://www.gnu.org/licenses.
|
2018-02-13 11:39:26 +03:00
|
|
|
*/
|
2018-08-19 13:59:22 +03:00
|
|
|
package flv
|
2018-02-10 09:59:56 +03:00
|
|
|
|
2018-10-19 03:50:08 +03:00
|
|
|
import (
|
|
|
|
"io"
|
|
|
|
"time"
|
|
|
|
)
|
2018-02-12 10:58:29 +03:00
|
|
|
|
|
|
|
const (
|
2018-05-05 18:52:36 +03:00
|
|
|
inputChanLength = 500
|
|
|
|
outputChanLength = 500
|
2018-02-28 16:46:59 +03:00
|
|
|
audioSize = 18
|
|
|
|
videoHeaderSize = 16
|
2018-02-12 10:58:29 +03:00
|
|
|
)
|
|
|
|
|
2018-02-28 16:46:59 +03:00
|
|
|
// Data representing silent audio (required for youtube)
|
2018-07-07 08:57:59 +03:00
|
|
|
var (
|
|
|
|
dummyAudioTag1Data = []byte{
|
|
|
|
0x00, 0x12, 0x08, 0x56, 0xe5, 0x00,
|
|
|
|
}
|
|
|
|
|
|
|
|
dummyAudioTag2Data = []byte{
|
|
|
|
0x01, 0xdc, 0x00, 0x4c, 0x61, 0x76, 0x63, 0x35, 0x38,
|
|
|
|
0x2e, 0x36, 0x2e, 0x31, 0x30, 0x32, 0x00, 0x02, 0x30,
|
|
|
|
0x40, 0x0e,
|
|
|
|
}
|
|
|
|
)
|
2018-02-28 16:46:59 +03:00
|
|
|
|
2018-08-19 14:35:04 +03:00
|
|
|
// Encoder provides properties required for the generation of flv video
|
2018-02-28 16:46:59 +03:00
|
|
|
// from raw video data
|
2018-08-19 14:35:04 +03:00
|
|
|
type Encoder struct {
|
2018-10-19 03:50:08 +03:00
|
|
|
dst io.Writer
|
|
|
|
|
|
|
|
fps int
|
|
|
|
audio bool
|
|
|
|
video bool
|
|
|
|
lastTagSize int
|
|
|
|
header Header
|
|
|
|
start time.Time
|
2018-02-10 16:25:55 +03:00
|
|
|
}
|
|
|
|
|
2018-08-19 14:35:04 +03:00
|
|
|
// NewEncoder retuns a new FLV encoder.
|
2019-01-05 10:26:55 +03:00
|
|
|
func NewEncoder(dst io.Writer, audio, video bool, fps int) *Encoder {
|
|
|
|
return &Encoder{
|
2018-10-19 03:50:08 +03:00
|
|
|
dst: dst,
|
|
|
|
fps: fps,
|
|
|
|
audio: audio,
|
|
|
|
video: video,
|
2018-07-07 08:57:59 +03:00
|
|
|
}
|
2018-02-10 16:25:55 +03:00
|
|
|
}
|
|
|
|
|
2018-08-19 14:58:20 +03:00
|
|
|
// HeaderBytes returns the a
|
|
|
|
func (e *Encoder) HeaderBytes() []byte {
|
2018-08-19 13:59:22 +03:00
|
|
|
header := Header{
|
2018-08-19 14:35:04 +03:00
|
|
|
HasAudio: e.audio,
|
|
|
|
HasVideo: e.video,
|
2018-02-28 16:46:59 +03:00
|
|
|
}
|
2018-08-19 14:58:20 +03:00
|
|
|
return header.Bytes()
|
|
|
|
}
|
|
|
|
|
2018-03-18 03:00:05 +03:00
|
|
|
// getNextTimestamp generates and returns the next timestamp based on current time
|
2018-08-19 14:35:04 +03:00
|
|
|
func (e *Encoder) getNextTimestamp() (timestamp uint32) {
|
2018-10-19 03:50:08 +03:00
|
|
|
if e.start.IsZero() {
|
|
|
|
e.start = time.Now()
|
2018-07-07 08:57:59 +03:00
|
|
|
return 0
|
2018-03-13 12:29:15 +03:00
|
|
|
}
|
2018-10-19 03:50:08 +03:00
|
|
|
return uint32(time.Now().Sub(e.start).Seconds() * float64(1000))
|
2018-02-10 16:25:55 +03:00
|
|
|
}
|
|
|
|
|
2018-07-07 08:57:59 +03:00
|
|
|
// http://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-H.264-200305-S!!PDF-E&type=items
|
|
|
|
// Table 7-1 NAL unit type codes
|
|
|
|
const (
|
|
|
|
nonIdrPic = 1
|
|
|
|
idrPic = 5
|
|
|
|
suppEnhInf = 6
|
|
|
|
seqParamSet = 7
|
|
|
|
paramSet = 8
|
|
|
|
)
|
|
|
|
|
2018-03-18 03:00:05 +03:00
|
|
|
// isKeyFrame returns true if the passed frame data represents that of a keyframe
|
2018-07-07 08:57:59 +03:00
|
|
|
// FIXME(kortschak): Clarify and document the logic of this functions.
|
2018-03-13 08:00:02 +03:00
|
|
|
func isKeyFrame(frame []byte) bool {
|
2018-07-07 08:57:59 +03:00
|
|
|
sc := frameScanner{buf: frame}
|
|
|
|
for {
|
|
|
|
b, ok := sc.readByte()
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for i := 1; b == 0x00 && i < 4; i++ {
|
|
|
|
b, ok = sc.readByte()
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if b != 0x01 || (i != 3 && i != 2) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
b, ok = sc.readByte()
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
switch nalTyp := b & 0x1f; nalTyp {
|
|
|
|
case idrPic, suppEnhInf:
|
|
|
|
return true
|
|
|
|
case nonIdrPic:
|
|
|
|
return false
|
2018-02-16 08:46:24 +03:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2018-03-18 03:00:05 +03:00
|
|
|
// isSequenceHeader returns true if the passed frame data represents that of a
|
|
|
|
// a sequence header.
|
2018-07-07 08:57:59 +03:00
|
|
|
// FIXME(kortschak): Clarify and document the logic of this functions.
|
2018-02-16 08:46:24 +03:00
|
|
|
func isSequenceHeader(frame []byte) bool {
|
2018-07-07 08:57:59 +03:00
|
|
|
sc := frameScanner{buf: frame}
|
|
|
|
for {
|
|
|
|
b, ok := sc.readByte()
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
for i := 1; b == 0x00 && i != 4; i++ {
|
|
|
|
b, ok = sc.readByte()
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
if b != 0x01 || (i != 2 && i != 3) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
b, ok = sc.readByte()
|
|
|
|
if !ok {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
switch nalTyp := b & 0x1f; nalTyp {
|
|
|
|
case suppEnhInf, seqParamSet, paramSet:
|
|
|
|
return true
|
|
|
|
case nonIdrPic, idrPic:
|
|
|
|
return false
|
2018-03-13 08:00:02 +03:00
|
|
|
}
|
|
|
|
}
|
2018-02-16 08:46:24 +03:00
|
|
|
}
|
2018-07-07 08:57:59 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
type frameScanner struct {
|
|
|
|
off int
|
|
|
|
buf []byte
|
|
|
|
}
|
|
|
|
|
|
|
|
func (s *frameScanner) readByte() (b byte, ok bool) {
|
|
|
|
if s.off >= len(s.buf) {
|
|
|
|
return 0, false
|
|
|
|
}
|
|
|
|
b = s.buf[s.off]
|
|
|
|
s.off++
|
|
|
|
return b, true
|
2018-02-16 08:46:24 +03:00
|
|
|
}
|
|
|
|
|
2018-02-28 16:46:59 +03:00
|
|
|
// generate takes in raw video data from the input chan and packetises it into
|
|
|
|
// flv tags, which are then passed to the output channel.
|
2018-08-19 14:58:20 +03:00
|
|
|
func (e *Encoder) Encode(frame []byte) error {
|
2018-02-28 17:50:14 +03:00
|
|
|
var frameType byte
|
|
|
|
var packetType byte
|
2018-08-19 14:58:20 +03:00
|
|
|
timeStamp := e.getNextTimestamp()
|
|
|
|
// Do we have video to send off?
|
|
|
|
if e.video {
|
|
|
|
if isKeyFrame(frame) {
|
|
|
|
frameType = KeyFrameType
|
|
|
|
} else {
|
|
|
|
frameType = InterFrameType
|
|
|
|
}
|
|
|
|
if isSequenceHeader(frame) {
|
|
|
|
packetType = SequenceHeader
|
|
|
|
} else {
|
|
|
|
packetType = AVCNALU
|
|
|
|
}
|
2018-07-07 08:57:59 +03:00
|
|
|
|
2018-08-19 14:58:20 +03:00
|
|
|
tag := VideoTag{
|
|
|
|
TagType: uint8(VideoTagType),
|
|
|
|
DataSize: uint32(len(frame)) + DataHeaderLength,
|
|
|
|
Timestamp: timeStamp,
|
|
|
|
TimestampExtended: NoTimestampExtension,
|
|
|
|
FrameType: frameType,
|
|
|
|
Codec: H264,
|
|
|
|
PacketType: packetType,
|
|
|
|
CompositionTime: 0,
|
|
|
|
Data: frame,
|
|
|
|
PrevTagSize: uint32(videoHeaderSize + len(frame)),
|
|
|
|
}
|
2018-10-19 03:50:08 +03:00
|
|
|
_, err := e.dst.Write(tag.Bytes())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-08-19 14:58:20 +03:00
|
|
|
}
|
|
|
|
// Do we even have some audio to send off ?
|
|
|
|
if e.audio {
|
|
|
|
// Not sure why but we need two audio tags for dummy silent audio
|
|
|
|
// TODO: create constants or SoundSize and SoundType parameters
|
|
|
|
tag := AudioTag{
|
|
|
|
TagType: uint8(AudioTagType),
|
|
|
|
DataSize: 7,
|
|
|
|
Timestamp: timeStamp,
|
|
|
|
TimestampExtended: NoTimestampExtension,
|
|
|
|
SoundFormat: AACAudioFormat,
|
|
|
|
SoundRate: 3,
|
|
|
|
SoundSize: true,
|
|
|
|
SoundType: true,
|
|
|
|
Data: dummyAudioTag1Data,
|
|
|
|
PrevTagSize: uint32(audioSize),
|
|
|
|
}
|
2018-10-19 03:50:08 +03:00
|
|
|
_, err := e.dst.Write(tag.Bytes())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-02-28 16:46:59 +03:00
|
|
|
|
2018-08-19 14:58:20 +03:00
|
|
|
tag = AudioTag{
|
|
|
|
TagType: uint8(AudioTagType),
|
|
|
|
DataSize: 21,
|
|
|
|
Timestamp: timeStamp,
|
|
|
|
TimestampExtended: NoTimestampExtension,
|
|
|
|
SoundFormat: AACAudioFormat,
|
|
|
|
SoundRate: 3,
|
|
|
|
SoundSize: true,
|
|
|
|
SoundType: true,
|
|
|
|
Data: dummyAudioTag2Data,
|
|
|
|
PrevTagSize: uint32(22),
|
2018-02-28 16:46:59 +03:00
|
|
|
}
|
2018-10-19 03:50:08 +03:00
|
|
|
_, err = e.dst.Write(tag.Bytes())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2018-02-28 16:46:59 +03:00
|
|
|
}
|
2018-08-19 14:58:20 +03:00
|
|
|
|
|
|
|
return nil
|
2018-02-10 09:59:56 +03:00
|
|
|
}
|