Skip to content

Commit

Permalink
Add ivfwriter support for VP9
Browse files Browse the repository at this point in the history
Adds the necessary wiring to get VP9 to work with `ivfwriter`.
Update the README of save-to-disk to inform users it supports
both VP8 and VP9.
    
ivfwriter currently assumes 30 fps but it seems that the other codecs
also assume 30 fps so that is not a net-new assumption.
  • Loading branch information
kevmo314 authored Feb 12, 2025
1 parent 306dc37 commit bea7ae3
Show file tree
Hide file tree
Showing 5 changed files with 122 additions and 42 deletions.
3 changes: 0 additions & 3 deletions examples/play-from-disk-renegotiation/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ package main
import (
"encoding/json"
"fmt"
"math/rand"
"net/http"
"os"
"time"
Expand Down Expand Up @@ -116,8 +115,6 @@ func removeVideo(res http.ResponseWriter, req *http.Request) {
}

func main() {
rand.Seed(time.Now().UTC().UnixNano())

var err error
if peerConnection, err = webrtc.NewPeerConnection(webrtc.Configuration{}); err != nil {
panic(err)
Expand Down
2 changes: 2 additions & 0 deletions examples/save-to-disk/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# save-to-disk
save-to-disk is a simple application that shows how to record your webcam/microphone using Pion WebRTC and save VP8/Opus to disk.

If you wish to save VP9 instead of VP8 you can just replace all occurences of VP8 with VP9 in [main.go](https://github.com/pion/example-webrtc-applications/tree/master/save-to-disk/main.go).

If you wish to save VP8/Opus inside the same file see [save-to-webm](https://github.com/pion/example-webrtc-applications/tree/master/save-to-webm)

If you wish to save AV1 instead see [save-to-disk-av1](https://github.com/pion/webrtc/tree/master/examples/save-to-disk-av1)
Expand Down
2 changes: 1 addition & 1 deletion examples/save-to-disk/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func main() {
if err != nil {
panic(err)
}
ivfFile, err := ivfwriter.New("output.ivf")
ivfFile, err := ivfwriter.New("output.ivf", ivfwriter.WithCodec("video/VP8"))
if err != nil {
panic(err)
}
Expand Down
122 changes: 84 additions & 38 deletions pkg/media/ivfwriter/ivfwriter.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,40 +16,48 @@ import (
)

var (
errFileNotOpened = errors.New("file not opened")
errInvalidNilPacket = errors.New("invalid nil packet")
errCodecAlreadySet = errors.New("codec is already set")
errNoSuchCodec = errors.New("no codec for this MimeType")
errFileNotOpened = errors.New("file not opened")
errInvalidNilPacket = errors.New("invalid nil packet")
errCodecUnset = errors.New("codec is unset")
errCodecAlreadySet = errors.New("codec is already set")
errNoSuchCodec = errors.New("no codec for this MimeType")
errInvalidMediaTimebase = errors.New("invalid media timebase")
)

const (
mimeTypeVP8 = "video/VP8"
mimeTypeAV1 = "video/AV1"
type (
codec int

ivfFileHeaderSignature = "DKIF"
)
// IVFWriter is used to take RTP packets and write them to an IVF on disk.
IVFWriter struct {
ioWriter io.Writer
count uint64
seenKeyFrame bool

var errInvalidMediaTimebase = errors.New("invalid media timebase")
codec codec

// IVFWriter is used to take RTP packets and write them to an IVF on disk.
type IVFWriter struct {
ioWriter io.Writer
count uint64
seenKeyFrame bool
timebaseDenominator uint32
timebaseNumerator uint32
firstFrameTimestamp uint32
clockRate uint64

isVP8, isAV1 bool
// VP8, VP9
currentFrame []byte

timebaseDenominator uint32
timebaseNumerator uint32
firstFrameTimestamp uint32
clockRate uint64
// AV1
av1Frame frame.AV1
}
)

// VP8
currentFrame []byte
const (
codecUnset codec = iota
codecVP8
codecVP9
codecAV1

// AV1
av1Frame frame.AV1
}
mimeTypeVP8 = "video/VP8"
mimeTypeVP9 = "video/VP9"
mimeTypeAV1 = "video/AV1"
)

// New builds a new IVF writer.
func New(fileName string, opts ...Option) (*IVFWriter, error) {
Expand Down Expand Up @@ -86,8 +94,8 @@ func NewWith(out io.Writer, opts ...Option) (*IVFWriter, error) {
}
}

if !writer.isAV1 && !writer.isVP8 {
writer.isVP8 = true
if writer.codec == codecUnset {
writer.codec = codecVP8
}

if err := writer.writeHeader(); err != nil {
Expand All @@ -103,15 +111,20 @@ func NewWith(out io.Writer, opts ...Option) (*IVFWriter, error) {

func (i *IVFWriter) writeHeader() error {
header := make([]byte, 32)
copy(header[0:], ivfFileHeaderSignature) // DKIF
copy(header[0:], "DKIF") // DKIF
binary.LittleEndian.PutUint16(header[4:], 0) // Version
binary.LittleEndian.PutUint16(header[6:], 32) // Header size

// FOURCC
if i.isVP8 {
switch i.codec {
case codecVP8:
copy(header[8:], "VP80")
} else if i.isAV1 {
case codecVP9:
copy(header[8:], "VP90")
case codecAV1:
copy(header[8:], "AV01")
default:
return errCodecUnset
}

binary.LittleEndian.PutUint16(header[12:], 640) // Width in pixels
Expand Down Expand Up @@ -146,19 +159,20 @@ func (i *IVFWriter) writeFrame(frame []byte, timestamp uint64) error {
}

// WriteRTP adds a new packet and writes the appropriate headers for it.
func (i *IVFWriter) WriteRTP(packet *rtp.Packet) error { //nolint:cyclop
func (i *IVFWriter) WriteRTP(packet *rtp.Packet) error { //nolint:cyclop, gocognit
if i.ioWriter == nil {
return errFileNotOpened
} else if len(packet.Payload) == 0 {
return nil
}

if i.count == 0 {
i.firstFrameTimestamp = packet.Header.Timestamp
i.firstFrameTimestamp = packet.Timestamp
}
relativeTstampMs := 1000 * uint64(packet.Header.Timestamp-i.firstFrameTimestamp) / i.clockRate
relativeTstampMs := 1000 * uint64(packet.Timestamp-i.firstFrameTimestamp) / i.clockRate

if i.isVP8 { //nolint:nestif
switch i.codec {
case codecVP8:
vp8Packet := codecs.VP8Packet{}
if _, err := vp8Packet.Unmarshal(packet.Payload); err != nil {
return err
Expand All @@ -185,7 +199,35 @@ func (i *IVFWriter) WriteRTP(packet *rtp.Packet) error { //nolint:cyclop
return err
}
i.currentFrame = nil
} else if i.isAV1 {
case codecVP9:
vp9Packet := codecs.VP9Packet{}
if _, err := vp9Packet.Unmarshal(packet.Payload); err != nil {
return err
}

switch {
case !i.seenKeyFrame && vp9Packet.P:
return nil
case i.currentFrame == nil && !vp9Packet.B:
return nil
}

i.seenKeyFrame = true
i.currentFrame = append(i.currentFrame, vp9Packet.Payload[0:]...)

if !packet.Marker {
return nil
} else if len(i.currentFrame) == 0 {
return nil
}

// the timestamp must be sequential. webrtc mandates a clock rate of 90000
// and we've assumed 30fps in the header.
if err := i.writeFrame(i.currentFrame, uint64(packet.Timestamp)/3000); err != nil {
return err
}
i.currentFrame = nil
case codecAV1:
av1Packet := &codecs.AV1Packet{}
if _, err := av1Packet.Unmarshal(packet.Payload); err != nil {
return err
Expand All @@ -201,6 +243,8 @@ func (i *IVFWriter) WriteRTP(packet *rtp.Packet) error { //nolint:cyclop
return err
}
}
default:
return errCodecUnset
}

return nil
Expand Down Expand Up @@ -243,15 +287,17 @@ type Option func(i *IVFWriter) error
// WithCodec configures if IVFWriter is writing AV1 or VP8 packets to disk.
func WithCodec(mimeType string) Option {
return func(i *IVFWriter) error {
if i.isVP8 || i.isAV1 {
if i.codec != codecUnset {
return errCodecAlreadySet
}

switch mimeType {
case mimeTypeVP8:
i.isVP8 = true
i.codec = codecVP8
case mimeTypeVP9:
i.codec = codecVP9
case mimeTypeAV1:
i.isAV1 = true
i.codec = codecAV1
default:
return errNoSuchCodec
}
Expand Down
35 changes: 35 additions & 0 deletions pkg/media/ivfwriter/ivfwriter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -302,3 +302,38 @@ func TestIVFWriter_AV1(t *testing.T) {
assert.NoError(t, writer.Close())
})
}

func TestIVFWriter_VP9(t *testing.T) {
buffer := &bytes.Buffer{}
writer, err := NewWith(buffer, WithCodec(mimeTypeVP9))
assert.NoError(t, err)

// No keyframe yet, ignore non-keyframe packets (P)
assert.NoError(t, writer.WriteRTP(&rtp.Packet{Payload: []byte{0xD0, 0x02, 0xAA}}))
assert.Equal(t, buffer.Bytes(), []byte{
0x44, 0x4b, 0x49, 0x46, 0x00, 0x00, 0x20, 0x00, 0x56, 0x50, 0x39, 0x30, 0x80, 0x02, 0xe0, 0x01,
0x1e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
})

// No current frame, ignore packets that don't start a frame (B)
assert.NoError(t, writer.WriteRTP(&rtp.Packet{Payload: []byte{0x00, 0xAA}}))
assert.Equal(t, buffer.Bytes(), []byte{
0x44, 0x4b, 0x49, 0x46, 0x00, 0x00, 0x20, 0x00, 0x56, 0x50, 0x39, 0x30, 0x80, 0x02, 0xe0, 0x01,
0x1e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
})

// B packet, no marker bit
assert.NoError(t, writer.WriteRTP(&rtp.Packet{Payload: []byte{0x08, 0xAA}}))
assert.Equal(t, buffer.Bytes(), []byte{
0x44, 0x4b, 0x49, 0x46, 0x00, 0x00, 0x20, 0x00, 0x56, 0x50, 0x39, 0x30, 0x80, 0x02, 0xe0, 0x01,
0x1e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
})

// B packet, Marker Bit
assert.NoError(t, writer.WriteRTP(&rtp.Packet{Header: rtp.Header{Marker: true}, Payload: []byte{0x08, 0xAB}}))
assert.Equal(t, buffer.Bytes(), []byte{
0x44, 0x4b, 0x49, 0x46, 0x00, 0x00, 0x20, 0x00, 0x56, 0x50, 0x39, 0x30, 0x80, 0x02, 0xe0, 0x01,
0x1e, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x84, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xaa, 0xab,
})
}

0 comments on commit bea7ae3

Please sign in to comment.