Add utilities/ivfreader from 804a12fed3/pkg/media/ivfreader

This commit is contained in:
DataHoarder 2022-11-09 22:18:23 +01:00
parent 7f00890636
commit e447e95974
Signed by: DataHoarder
SSH key fingerprint: SHA256:OLTRf6Fl87G52SiR7sWLGNzlJt4WOX+tfI2yxo0z7xk
6 changed files with 365 additions and 3 deletions

11
go.mod
View file

@ -2,4 +2,13 @@ module git.gammaspectra.live/S.O.N.G/Ignite
go 1.19
require golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561
require (
github.com/stretchr/testify v1.8.1
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

21
go.sum
View file

@ -1,2 +1,19 @@
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326 h1:QfTh0HpN6hlw6D3vu8DAwC8pBIwikq0AI1evdm+FksE=
golang.org/x/exp v0.0.0-20221031165847-c99f073a8326/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,3 @@
# ivfreader
Taken from [github.com/pion/webrtc](https://github.com/pion/webrtc/tree/804a12fed3ee45637aa08fd70023cfb2a44b651a/pkg/media/ivfreader) implementation.

View file

@ -0,0 +1,145 @@
// Package ivfreader implements IVF media container reader
package ivfreader
import (
"encoding/binary"
"errors"
"fmt"
"io"
)
const (
ivfFileHeaderSignature = "DKIF"
ivfFileHeaderSize = 32
ivfFrameHeaderSize = 12
)
var (
errNilStream = errors.New("stream is nil")
errIncompleteFrameHeader = errors.New("incomplete frame header")
errIncompleteFrameData = errors.New("incomplete frame data")
errIncompleteFileHeader = errors.New("incomplete file header")
errSignatureMismatch = errors.New("IVF signature mismatch")
errUnknownIVFVersion = errors.New("IVF version unknown, parser may not parse correctly")
)
// IVFFileHeader 32-byte header for IVF files
// https://wiki.multimedia.cx/index.php/IVF
type IVFFileHeader struct {
signature string // 0-3
version uint16 // 4-5
headerSize uint16 // 6-7
FourCC string // 8-11
Width uint16 // 12-13
Height uint16 // 14-15
TimebaseDenominator uint32 // 16-19
TimebaseNumerator uint32 // 20-23
NumFrames uint32 // 24-27
unused uint32 // 28-31
}
// IVFFrameHeader 12-byte header for IVF frames
// https://wiki.multimedia.cx/index.php/IVF
type IVFFrameHeader struct {
FrameSize uint32 // 0-3
Timestamp uint64 // 4-11
}
// IVFReader is used to read IVF files and return frame payloads
type IVFReader struct {
stream io.Reader
bytesReadSuccesfully int64
}
// NewWith returns a new IVF reader and IVF file header
// with an io.Reader input
func NewWith(in io.Reader) (*IVFReader, *IVFFileHeader, error) {
if in == nil {
return nil, nil, errNilStream
}
reader := &IVFReader{
stream: in,
}
header, err := reader.parseFileHeader()
if err != nil {
return nil, nil, err
}
return reader, header, nil
}
// ResetReader resets the internal stream of IVFReader. This is useful
// for live streams, where the end of the file might be read without the
// data being finished.
func (i *IVFReader) ResetReader(reset func(bytesRead int64) io.Reader) {
i.stream = reset(i.bytesReadSuccesfully)
}
// ParseNextFrame reads from stream and returns IVF frame payload, header,
// and an error if there is incomplete frame data.
// Returns all nil values when no more frames are available.
func (i *IVFReader) ParseNextFrame() ([]byte, *IVFFrameHeader, error) {
buffer := make([]byte, ivfFrameHeaderSize)
var header *IVFFrameHeader
bytesRead, err := io.ReadFull(i.stream, buffer)
headerBytesRead := bytesRead
if errors.Is(err, io.ErrUnexpectedEOF) {
return nil, nil, errIncompleteFrameHeader
} else if err != nil {
return nil, nil, err
}
header = &IVFFrameHeader{
FrameSize: binary.LittleEndian.Uint32(buffer[:4]),
Timestamp: binary.LittleEndian.Uint64(buffer[4:12]),
}
payload := make([]byte, header.FrameSize)
bytesRead, err = io.ReadFull(i.stream, payload)
if errors.Is(err, io.ErrUnexpectedEOF) {
return nil, nil, errIncompleteFrameData
} else if err != nil {
return nil, nil, err
}
i.bytesReadSuccesfully += int64(headerBytesRead) + int64(bytesRead)
return payload, header, nil
}
// parseFileHeader reads 32 bytes from stream and returns
// IVF file header. This is always called before ParseNextFrame()
func (i *IVFReader) parseFileHeader() (*IVFFileHeader, error) {
buffer := make([]byte, ivfFileHeaderSize)
bytesRead, err := io.ReadFull(i.stream, buffer)
if errors.Is(err, io.ErrUnexpectedEOF) {
return nil, errIncompleteFileHeader
} else if err != nil {
return nil, err
}
header := &IVFFileHeader{
signature: string(buffer[:4]),
version: binary.LittleEndian.Uint16(buffer[4:6]),
headerSize: binary.LittleEndian.Uint16(buffer[6:8]),
FourCC: string(buffer[8:12]),
Width: binary.LittleEndian.Uint16(buffer[12:14]),
Height: binary.LittleEndian.Uint16(buffer[14:16]),
TimebaseDenominator: binary.LittleEndian.Uint32(buffer[16:20]),
TimebaseNumerator: binary.LittleEndian.Uint32(buffer[20:24]),
NumFrames: binary.LittleEndian.Uint32(buffer[24:28]),
unused: binary.LittleEndian.Uint32(buffer[28:32]),
}
if header.signature != ivfFileHeaderSignature {
return nil, errSignatureMismatch
} else if header.version != uint16(0) {
return nil, fmt.Errorf("%w: expected(0) got(%d)", errUnknownIVFVersion, header.version)
}
i.bytesReadSuccesfully += int64(bytesRead)
return header, nil
}

View file

@ -0,0 +1,167 @@
package ivfreader
import (
"bytes"
"io"
"testing"
"github.com/stretchr/testify/assert"
)
// buildIVFContainer takes frames and prepends valid IVF file header
func buildIVFContainer(frames ...*[]byte) *bytes.Buffer {
// Valid IVF file header taken from: https://github.com/webmproject/...
// vp8-test-vectors/blob/master/vp80-00-comprehensive-001.ivf
// Video Image Width - 176
// Video Image Height - 144
// Frame Rate Rate - 30000
// Frame Rate Scale - 1000
// Video Length in Frames - 29
// BitRate: 64.01 kb/s
ivf := []byte{
0x44, 0x4b, 0x49, 0x46, 0x00, 0x00, 0x20, 0x00,
0x56, 0x50, 0x38, 0x30, 0xb0, 0x00, 0x90, 0x00,
0x30, 0x75, 0x00, 0x00, 0xe8, 0x03, 0x00, 0x00,
0x1d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
}
for f := range frames {
ivf = append(ivf, *frames[f]...)
}
return bytes.NewBuffer(ivf)
}
func TestIVFReader_ParseValidFileHeader(t *testing.T) {
assert := assert.New(t)
ivf := buildIVFContainer(&[]byte{})
reader, header, err := NewWith(ivf)
assert.Nil(err, "IVFReader should be created")
assert.NotNil(reader, "Reader shouldn't be nil")
assert.NotNil(header, "Header shouldn't be nil")
assert.Equal("DKIF", header.signature, "signature is 'DKIF'")
assert.Equal(uint16(0), header.version, "version should be 0")
assert.Equal("VP80", header.FourCC, "FourCC should be 'VP80'")
assert.Equal(uint16(176), header.Width, "width should be 176")
assert.Equal(uint16(144), header.Height, "height should be 144")
assert.Equal(uint32(30000), header.TimebaseDenominator, "timebase denominator should be 30000")
assert.Equal(uint32(1000), header.TimebaseNumerator, "timebase numerator should be 1000")
assert.Equal(uint32(29), header.NumFrames, "number of frames should be 29")
assert.Equal(uint32(0), header.unused, "bytes should be unused")
}
func TestIVFReader_ParseValidFrames(t *testing.T) {
assert := assert.New(t)
// Frame Length - 4
// Timestamp - None
// Frame Payload - 0xDEADBEEF
validFrame1 := []byte{
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF,
}
// Frame Length - 12
// Timestamp - None
// Frame Payload - 0xDEADBEEFDEADBEEF
validFrame2 := []byte{
0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD, 0xBE, 0xEF,
0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF,
}
ivf := buildIVFContainer(&validFrame1, &validFrame2)
reader, _, err := NewWith(ivf)
assert.Nil(err, "IVFReader should be created")
assert.NotNil(reader, "Reader shouldn't be nil")
// Parse Frame #1
payload, header, err := reader.ParseNextFrame()
assert.Nil(err, "Should have parsed frame #1 without error")
assert.Equal(uint32(4), header.FrameSize, "Frame header frameSize should be 4")
assert.Equal(4, len(payload), "Payload should be length 4")
assert.Equal(
payload,
[]byte{
0xDE, 0xAD, 0xBE, 0xEF,
},
"Payload value should be 0xDEADBEEF")
assert.Equal(int64(ivfFrameHeaderSize+ivfFileHeaderSize+header.FrameSize), reader.bytesReadSuccesfully)
previousBytesRead := reader.bytesReadSuccesfully
// Parse Frame #2
payload, header, err = reader.ParseNextFrame()
assert.Nil(err, "Should have parsed frame #2 without error")
assert.Equal(uint32(12), header.FrameSize, "Frame header frameSize should be 4")
assert.Equal(12, len(payload), "Payload should be length 12")
assert.Equal(
payload,
[]byte{
0xDE, 0xAD, 0xBE, 0xEF, 0xDE, 0xAD,
0xBE, 0xEF, 0xDE, 0xAD, 0xBE, 0xEF,
},
"Payload value should be 0xDEADBEEFDEADBEEF")
assert.Equal(int64(ivfFrameHeaderSize+header.FrameSize)+previousBytesRead, reader.bytesReadSuccesfully)
}
func TestIVFReader_ParseIncompleteFrameHeader(t *testing.T) {
assert := assert.New(t)
// frame with 11-byte header (missing 1 byte)
incompleteFrame := []byte{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
}
ivf := buildIVFContainer(&incompleteFrame)
reader, _, err := NewWith(ivf)
assert.Nil(err, "IVFReader should be created")
assert.NotNil(reader, "Reader shouldn't be nil")
// Parse Frame #1
payload, header, err := reader.ParseNextFrame()
assert.Nil(payload, "Payload should be nil")
assert.Nil(header, "Incomplete header should be nil")
assert.Equal(errIncompleteFrameHeader, err)
}
func TestIVFReader_ParseIncompleteFramePayload(t *testing.T) {
assert := assert.New(t)
// frame with header defining frameSize of 4
// but only 2 bytes available (missing 2 bytes)
incompleteFrame := []byte{
0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0xDE, 0xAD,
}
ivf := buildIVFContainer(&incompleteFrame)
reader, _, err := NewWith(ivf)
assert.Nil(err, "IVFReader should be created")
assert.NotNil(reader, "Reader shouldn't be nil")
// Parse Frame #1
payload, header, err := reader.ParseNextFrame()
assert.Nil(payload, "Incomplete payload should be nil")
assert.Nil(header, "Header should be nil")
assert.Equal(errIncompleteFrameData, err)
}
func TestIVFReader_EOFWhenNoFramesLeft(t *testing.T) {
assert := assert.New(t)
ivf := buildIVFContainer(&[]byte{})
reader, _, err := NewWith(ivf)
assert.Nil(err, "IVFReader should be created")
assert.NotNil(reader, "Reader shouldn't be nil")
_, _, err = reader.ParseNextFrame()
assert.Equal(io.EOF, err)
}