Codex: Expose RTMP AMF0 interfaces.

Add public AMF0 and RTMP protocol interfaces, update the RTMP proxy to use the accessor APIs, and cover AMF0 encoding/decoding with unit tests and examples.
This commit is contained in:
winlin 2026-04-29 11:26:40 -04:00
parent a76a982563
commit 9b6842da9a
5 changed files with 924 additions and 174 deletions

View File

@ -95,7 +95,7 @@ var createBuffer = func() amf0Buffer {
} }
// All AMF0 things. // All AMF0 things.
type amf0Any interface { type Amf0Any interface {
// Binary marshaler and unmarshaler. // Binary marshaler and unmarshaler.
encoding.BinaryUnmarshaler encoding.BinaryUnmarshaler
encoding.BinaryMarshaler encoding.BinaryMarshaler
@ -106,59 +106,83 @@ type amf0Any interface {
amf0Marker() amf0Marker amf0Marker() amf0Marker
} }
type amf0Converter struct { type Amf0Converter interface {
from amf0Any ToNumber() Amf0Number
ToBoolean() Amf0Boolean
ToString() Amf0String
ToObject() Amf0Object
ToNull() Amf0Null
ToUndefined() Amf0Undefined
ToEcmaArray() Amf0EcmaArray
ToStrictArray() Amf0StrictArray
} }
func NewAmf0Converter(from amf0Any) *amf0Converter { type amf0Converter struct {
from Amf0Any
}
func NewAmf0Converter(from Amf0Any) Amf0Converter {
return &amf0Converter{from: from} return &amf0Converter{from: from}
} }
func (v *amf0Converter) ToNumber() *amf0Number { func (v *amf0Converter) ToNumber() Amf0Number {
return amf0AnyTo[*amf0Number](v.from) if r, ok := v.from.(Amf0Number); ok {
} return r
func (v *amf0Converter) ToBoolean() *amf0Boolean {
return amf0AnyTo[*amf0Boolean](v.from)
}
func (v *amf0Converter) ToString() *amf0String {
return amf0AnyTo[*amf0String](v.from)
}
func (v *amf0Converter) ToObject() *amf0Object {
return amf0AnyTo[*amf0Object](v.from)
}
func (v *amf0Converter) ToNull() *amf0Null {
return amf0AnyTo[*amf0Null](v.from)
}
func (v *amf0Converter) ToUndefined() *amf0Undefined {
return amf0AnyTo[*amf0Undefined](v.from)
}
func (v *amf0Converter) ToEcmaArray() *amf0EcmaArray {
return amf0AnyTo[*amf0EcmaArray](v.from)
}
func (v *amf0Converter) ToStrictArray() *amf0StrictArray {
return amf0AnyTo[*amf0StrictArray](v.from)
}
// Convert any to specified object.
func amf0AnyTo[T amf0Any](a amf0Any) T {
var to T
if a != nil {
if v, ok := a.(T); ok {
return v
}
} }
return to return nil
}
func (v *amf0Converter) ToBoolean() Amf0Boolean {
if r, ok := v.from.(Amf0Boolean); ok {
return r
}
return nil
}
func (v *amf0Converter) ToString() Amf0String {
if r, ok := v.from.(Amf0String); ok {
return r
}
return nil
}
func (v *amf0Converter) ToObject() Amf0Object {
if r, ok := v.from.(Amf0Object); ok {
return r
}
return nil
}
func (v *amf0Converter) ToNull() Amf0Null {
if r, ok := v.from.(Amf0Null); ok {
return r
}
return nil
}
func (v *amf0Converter) ToUndefined() Amf0Undefined {
if r, ok := v.from.(Amf0Undefined); ok {
return r
}
return nil
}
func (v *amf0Converter) ToEcmaArray() Amf0EcmaArray {
if r, ok := v.from.(Amf0EcmaArray); ok {
return r
}
return nil
}
func (v *amf0Converter) ToStrictArray() Amf0StrictArray {
if r, ok := v.from.(Amf0StrictArray); ok {
return r
}
return nil
} }
// Discovery the amf0 object from the bytes b. // Discovery the amf0 object from the bytes b.
func Amf0Discovery(p []byte) (a amf0Any, err error) { func Amf0Discovery(p []byte) (a Amf0Any, err error) {
if len(p) < 1 { if len(p) < 1 {
return nil, errors.Errorf("require 1 bytes only %v", len(p)) return nil, errors.Errorf("require 1 bytes only %v", len(p))
} }
@ -228,14 +252,24 @@ func (v *amf0UTF8) MarshalBinary() (data []byte, err error) {
return return
} }
// Amf0Number is the AMF0 number type.
type Amf0Number interface {
Amf0Any
Float64() float64
}
// The number object, please read @doc amf0_spec_121207.pdf, @page 5, @section 2.2 Number Type // The number object, please read @doc amf0_spec_121207.pdf, @page 5, @section 2.2 Number Type
type amf0Number float64 type amf0Number float64
func NewAmf0Number(f float64) *amf0Number { func NewAmf0Number(f float64) Amf0Number {
v := amf0Number(f) v := amf0Number(f)
return &v return &v
} }
func (v *amf0Number) Float64() float64 {
return float64(*v)
}
func (v *amf0Number) amf0Marker() amf0Marker { func (v *amf0Number) amf0Marker() amf0Marker {
return amf0MarkerNumber return amf0MarkerNumber
} }
@ -266,14 +300,28 @@ func (v *amf0Number) MarshalBinary() (data []byte, err error) {
return return
} }
// Amf0String is the AMF0 string type.
type Amf0String interface {
Amf0Any
String() string
}
// The string objet, please read @doc amf0_spec_121207.pdf, @page 5, @section 2.4 String Type // The string objet, please read @doc amf0_spec_121207.pdf, @page 5, @section 2.4 String Type
type amf0String string type amf0String string
func NewAmf0String(s string) *amf0String { func NewAmf0String(s string) Amf0String {
return newAmf0String(s)
}
func newAmf0String(s string) *amf0String {
v := amf0String(s) v := amf0String(s)
return &v return &v
} }
func (v *amf0String) String() string {
return string(*v)
}
func (v *amf0String) amf0Marker() amf0Marker { func (v *amf0String) amf0Marker() amf0Marker {
return amf0MarkerString return amf0MarkerString
} }
@ -344,7 +392,7 @@ func (v *amf0ObjectEOF) MarshalBinary() (data []byte, err error) {
// Use array for object and ecma array, to keep the original order. // Use array for object and ecma array, to keep the original order.
type amf0Property struct { type amf0Property struct {
key amf0UTF8 key amf0UTF8
value amf0Any value Amf0Any
} }
// The object-like AMF0 structure, like object and ecma array and strict array. // The object-like AMF0 structure, like object and ecma array and strict array.
@ -367,7 +415,7 @@ func (v *amf0ObjectBase) Size() int {
return size return size
} }
func (v *amf0ObjectBase) Get(key string) amf0Any { func (v *amf0ObjectBase) Get(key string) Amf0Any {
v.lock.Lock() v.lock.Lock()
defer v.lock.Unlock() defer v.lock.Unlock()
@ -380,7 +428,7 @@ func (v *amf0ObjectBase) Get(key string) amf0Any {
return nil return nil
} }
func (v *amf0ObjectBase) Set(key string, value amf0Any) *amf0ObjectBase { func (v *amf0ObjectBase) Set(key string, value Amf0Any) *amf0ObjectBase {
v.lock.Lock() v.lock.Lock()
defer v.lock.Unlock() defer v.lock.Unlock()
@ -411,21 +459,21 @@ func (v *amf0ObjectBase) unmarshal(p []byte, eof bool, maxElems int) (err error)
return errors.Errorf("maxElems=%v with eof", maxElems) return errors.Errorf("maxElems=%v with eof", maxElems)
} }
readOne := func() (amf0UTF8, amf0Any, error) { readOne := func() (amf0UTF8, Amf0Any, error) {
var u amf0UTF8 var u amf0UTF8
if err = u.UnmarshalBinary(p); err != nil { if err = u.UnmarshalBinary(p); err != nil {
return "", nil, errors.WithMessage(err, "prop name") return "", nil, errors.WithMessage(err, "prop name")
} }
p = p[u.Size():] p = p[u.Size():]
var a amf0Any var a Amf0Any
if a, err = Amf0Discovery(p); err != nil { if a, err = Amf0Discovery(p); err != nil {
return "", nil, errors.WithMessage(err, fmt.Sprintf("discover prop %v", string(u))) return "", nil, errors.WithMessage(err, fmt.Sprintf("discover prop %v", string(u)))
} }
return u, a, nil return u, a, nil
} }
pushOne := func(u amf0UTF8, a amf0Any) error { pushOne := func(u amf0UTF8, a Amf0Any) error {
// For object property, consume the whole bytes. // For object property, consume the whole bytes.
if err = a.UnmarshalBinary(p); err != nil { if err = a.UnmarshalBinary(p); err != nil {
return errors.WithMessage(err, fmt.Sprintf("unmarshal prop %v", string(u))) return errors.WithMessage(err, fmt.Sprintf("unmarshal prop %v", string(u)))
@ -494,13 +542,24 @@ func (v *amf0ObjectBase) marshal(b amf0Buffer) (err error) {
return return
} }
// Amf0Object is the AMF0 object type.
type Amf0Object interface {
Amf0Any
Get(key string) Amf0Any
Set(key string, value Amf0Any) Amf0Object
}
// The AMF0 object, please read @doc amf0_spec_121207.pdf, @page 5, @section 2.5 Object Type // The AMF0 object, please read @doc amf0_spec_121207.pdf, @page 5, @section 2.5 Object Type
type amf0Object struct { type amf0Object struct {
amf0ObjectBase amf0ObjectBase
eof amf0ObjectEOF eof amf0ObjectEOF
} }
func NewAmf0Object() *amf0Object { func NewAmf0Object() Amf0Object {
return newAmf0Object()
}
func newAmf0Object() *amf0Object {
v := &amf0Object{} v := &amf0Object{}
v.properties = []*amf0Property{} v.properties = []*amf0Property{}
return v return v
@ -510,6 +569,15 @@ func (v *amf0Object) amf0Marker() amf0Marker {
return amf0MarkerObject return amf0MarkerObject
} }
func (v *amf0Object) Get(key string) Amf0Any {
return v.amf0ObjectBase.Get(key)
}
func (v *amf0Object) Set(key string, value Amf0Any) Amf0Object {
v.amf0ObjectBase.Set(key, value)
return v
}
func (v *amf0Object) Size() int { func (v *amf0Object) Size() int {
return int(1) + v.eof.Size() + v.amf0ObjectBase.Size() return int(1) + v.eof.Size() + v.amf0ObjectBase.Size()
} }
@ -542,17 +610,22 @@ func (v *amf0Object) MarshalBinary() (data []byte, err error) {
return nil, errors.WithMessage(err, "marshal") return nil, errors.WithMessage(err, "marshal")
} }
var pb []byte if pb, err := v.eof.MarshalBinary(); err != nil {
if pb, err = v.eof.MarshalBinary(); err != nil {
return nil, errors.WithMessage(err, "marshal") return nil, errors.WithMessage(err, "marshal")
} } else if _, err = b.Write(pb); err != nil {
if _, err = b.Write(pb); err != nil {
return nil, errors.Wrap(err, "marshal") return nil, errors.Wrap(err, "marshal")
} }
return b.Bytes(), nil return b.Bytes(), nil
} }
// Amf0EcmaArray is the AMF0 ECMA array type.
type Amf0EcmaArray interface {
Amf0Any
Get(key string) Amf0Any
Set(key string, value Amf0Any) Amf0EcmaArray
}
// The AMF0 ecma array, please read @doc amf0_spec_121207.pdf, @page 6, @section 2.10 ECMA Array Type // The AMF0 ecma array, please read @doc amf0_spec_121207.pdf, @page 6, @section 2.10 ECMA Array Type
type amf0EcmaArray struct { type amf0EcmaArray struct {
amf0ObjectBase amf0ObjectBase
@ -560,7 +633,11 @@ type amf0EcmaArray struct {
eof amf0ObjectEOF eof amf0ObjectEOF
} }
func NewAmf0EcmaArray() *amf0EcmaArray { func NewAmf0EcmaArray() Amf0EcmaArray {
return newAmf0EcmaArray()
}
func newAmf0EcmaArray() *amf0EcmaArray {
v := &amf0EcmaArray{} v := &amf0EcmaArray{}
v.properties = []*amf0Property{} v.properties = []*amf0Property{}
return v return v
@ -570,6 +647,15 @@ func (v *amf0EcmaArray) amf0Marker() amf0Marker {
return amf0MarkerEcmaArray return amf0MarkerEcmaArray
} }
func (v *amf0EcmaArray) Get(key string) Amf0Any {
return v.amf0ObjectBase.Get(key)
}
func (v *amf0EcmaArray) Set(key string, value Amf0Any) Amf0EcmaArray {
v.amf0ObjectBase.Set(key, value)
return v
}
func (v *amf0EcmaArray) Size() int { func (v *amf0EcmaArray) Size() int {
return int(1) + 4 + v.eof.Size() + v.amf0ObjectBase.Size() return int(1) + 4 + v.eof.Size() + v.amf0ObjectBase.Size()
} }
@ -606,24 +692,29 @@ func (v *amf0EcmaArray) MarshalBinary() (data []byte, err error) {
return nil, errors.WithMessage(err, "marshal") return nil, errors.WithMessage(err, "marshal")
} }
var pb []byte if pb, err := v.eof.MarshalBinary(); err != nil {
if pb, err = v.eof.MarshalBinary(); err != nil {
return nil, errors.WithMessage(err, "marshal") return nil, errors.WithMessage(err, "marshal")
} } else if _, err = b.Write(pb); err != nil {
if _, err = b.Write(pb); err != nil {
return nil, errors.Wrap(err, "marshal") return nil, errors.Wrap(err, "marshal")
} }
return b.Bytes(), nil return b.Bytes(), nil
} }
// Amf0StrictArray is the AMF0 strict array type.
type Amf0StrictArray interface {
Amf0Any
Get(key string) Amf0Any
Set(key string, value Amf0Any) Amf0StrictArray
}
// The AMF0 strict array, please read @doc amf0_spec_121207.pdf, @page 7, @section 2.12 Strict Array Type // The AMF0 strict array, please read @doc amf0_spec_121207.pdf, @page 7, @section 2.12 Strict Array Type
type amf0StrictArray struct { type amf0StrictArray struct {
amf0ObjectBase amf0ObjectBase
count uint32 count uint32
} }
func NewAmf0StrictArray() *amf0StrictArray { func NewAmf0StrictArray() Amf0StrictArray {
v := &amf0StrictArray{} v := &amf0StrictArray{}
v.properties = []*amf0Property{} v.properties = []*amf0Property{}
return v return v
@ -633,6 +724,15 @@ func (v *amf0StrictArray) amf0Marker() amf0Marker {
return amf0MarkerStrictArray return amf0MarkerStrictArray
} }
func (v *amf0StrictArray) Get(key string) Amf0Any {
return v.amf0ObjectBase.Get(key)
}
func (v *amf0StrictArray) Set(key string, value Amf0Any) Amf0StrictArray {
v.amf0ObjectBase.Set(key, value)
return v
}
func (v *amf0StrictArray) Size() int { func (v *amf0StrictArray) Size() int {
return int(1) + 4 + v.amf0ObjectBase.Size() return int(1) + 4 + v.amf0ObjectBase.Size()
} }
@ -708,36 +808,56 @@ func (v *amf0SingleMarkerObject) MarshalBinary() (data []byte, err error) {
return []byte{byte(v.target)}, nil return []byte{byte(v.target)}, nil
} }
// Amf0Null is the AMF0 null type.
type Amf0Null interface {
Amf0Any
}
// The AMF0 null, please read @doc amf0_spec_121207.pdf, @page 6, @section 2.7 null Type // The AMF0 null, please read @doc amf0_spec_121207.pdf, @page 6, @section 2.7 null Type
type amf0Null struct { type amf0Null struct {
amf0SingleMarkerObject amf0SingleMarkerObject
} }
func NewAmf0Null() *amf0Null { func NewAmf0Null() Amf0Null {
v := amf0Null{} v := amf0Null{}
v.amf0SingleMarkerObject = newAmf0SingleMarkerObject(amf0MarkerNull) v.amf0SingleMarkerObject = newAmf0SingleMarkerObject(amf0MarkerNull)
return &v return &v
} }
// Amf0Undefined is the AMF0 undefined type.
type Amf0Undefined interface {
Amf0Any
}
// The AMF0 undefined, please read @doc amf0_spec_121207.pdf, @page 6, @section 2.8 undefined Type // The AMF0 undefined, please read @doc amf0_spec_121207.pdf, @page 6, @section 2.8 undefined Type
type amf0Undefined struct { type amf0Undefined struct {
amf0SingleMarkerObject amf0SingleMarkerObject
} }
func NewAmf0Undefined() amf0Any { func NewAmf0Undefined() Amf0Undefined {
v := amf0Undefined{} v := amf0Undefined{}
v.amf0SingleMarkerObject = newAmf0SingleMarkerObject(amf0MarkerUndefined) v.amf0SingleMarkerObject = newAmf0SingleMarkerObject(amf0MarkerUndefined)
return &v return &v
} }
// Amf0Boolean is the public typed view of an AMF0 boolean.
type Amf0Boolean interface {
Amf0Any
Bool() bool
}
// The AMF0 boolean, please read @doc amf0_spec_121207.pdf, @page 5, @section 2.3 Boolean Type // The AMF0 boolean, please read @doc amf0_spec_121207.pdf, @page 5, @section 2.3 Boolean Type
type amf0Boolean bool type amf0Boolean bool
func NewAmf0Boolean(b bool) amf0Any { func NewAmf0Boolean(b bool) Amf0Boolean {
v := amf0Boolean(b) v := amf0Boolean(b)
return &v return &v
} }
func (v *amf0Boolean) Bool() bool {
return bool(*v)
}
func (v *amf0Boolean) amf0Marker() amf0Marker { func (v *amf0Boolean) amf0Marker() amf0Marker {
return amf0MarkerBoolean return amf0MarkerBoolean
} }

509
internal/rtmp/amf0_test.go Normal file
View File

@ -0,0 +1,509 @@
// Copyright (c) 2026 Winlin
//
// SPDX-License-Identifier: MIT
package rtmp
import (
"bytes"
"fmt"
"math"
"strings"
"testing"
)
func TestAmf0MarkerString(t *testing.T) {
for _, tt := range []struct {
marker amf0Marker
want string
}{
{amf0MarkerNumber, "Amf0Number"},
{amf0MarkerBoolean, "amf0Boolean"},
{amf0MarkerString, "Amf0String"},
{amf0MarkerObject, "Amf0Object"},
{amf0MarkerMovieClip, "MovieClip"},
{amf0MarkerNull, "Null"},
{amf0MarkerUndefined, "Undefined"},
{amf0MarkerReference, "Reference"},
{amf0MarkerEcmaArray, "EcmaArray"},
{amf0MarkerObjectEnd, "ObjectEnd"},
{amf0MarkerStrictArray, "StrictArray"},
{amf0MarkerDate, "Date"},
{amf0MarkerLongString, "LongString"},
{amf0MarkerUnsupported, "Unsupported"},
{amf0MarkerRecordSet, "RecordSet"},
{amf0MarkerXmlDocument, "XmlDocument"},
{amf0MarkerTypedObject, "TypedObject"},
{amf0MarkerAvmPlusObject, "AvmPlusObject"},
{amf0MarkerForbidden, "Forbidden"},
{amf0Marker(0xee), "Forbidden"},
} {
if got := tt.marker.String(); got != tt.want {
t.Fatalf("marker=%#x String()=%v, want %v", byte(tt.marker), got, tt.want)
}
}
}
func TestAmf0Discovery(t *testing.T) {
for _, tt := range []struct {
name string
data []byte
ok func(Amf0Any) bool
}{
{"number", []byte{byte(amf0MarkerNumber)}, func(v Amf0Any) bool { _, ok := v.(Amf0Number); return ok }},
{"boolean", []byte{byte(amf0MarkerBoolean)}, func(v Amf0Any) bool { _, ok := v.(Amf0Boolean); return ok }},
{"string", []byte{byte(amf0MarkerString)}, func(v Amf0Any) bool { _, ok := v.(Amf0String); return ok }},
{"object", []byte{byte(amf0MarkerObject)}, func(v Amf0Any) bool { _, ok := v.(Amf0Object); return ok }},
{"null", []byte{byte(amf0MarkerNull)}, func(v Amf0Any) bool { _, ok := v.(Amf0Null); return ok }},
{"undefined", []byte{byte(amf0MarkerUndefined)}, func(v Amf0Any) bool { _, ok := v.(Amf0Undefined); return ok }},
{"ecma-array", []byte{byte(amf0MarkerEcmaArray)}, func(v Amf0Any) bool { _, ok := v.(Amf0EcmaArray); return ok }},
{"object-end", []byte{byte(amf0MarkerObjectEnd)}, func(v Amf0Any) bool { _, ok := v.(*amf0ObjectEOF); return ok }},
{"strict-array", []byte{byte(amf0MarkerStrictArray)}, func(v Amf0Any) bool { _, ok := v.(Amf0StrictArray); return ok }},
} {
t.Run(tt.name, func(t *testing.T) {
value, err := Amf0Discovery(tt.data)
if err != nil {
t.Fatalf("Amf0Discovery() err=%v", err)
}
if !tt.ok(value) {
t.Fatalf("Amf0Discovery()=%T", value)
}
})
}
for _, data := range [][]byte{{}, {byte(amf0MarkerReference)}, {byte(amf0MarkerDate)}, {byte(amf0MarkerForbidden)}} {
if value, err := Amf0Discovery(data); err == nil || value != nil {
t.Fatalf("Amf0Discovery(%v) value=%T, err=%v, want error", data, value, err)
}
}
}
func TestAmf0Converter(t *testing.T) {
values := []struct {
name string
in Amf0Any
ok func(Amf0Converter) bool
}{
{"number", NewAmf0Number(1), func(c Amf0Converter) bool { return c.ToNumber() != nil }},
{"boolean", NewAmf0Boolean(true), func(c Amf0Converter) bool { return c.ToBoolean() != nil }},
{"string", NewAmf0String("v"), func(c Amf0Converter) bool { return c.ToString() != nil }},
{"object", NewAmf0Object(), func(c Amf0Converter) bool { return c.ToObject() != nil }},
{"null", NewAmf0Null(), func(c Amf0Converter) bool { return c.ToNull() != nil }},
{"undefined", NewAmf0Undefined(), func(c Amf0Converter) bool { return c.ToUndefined() != nil }},
{"ecma-array", NewAmf0EcmaArray(), func(c Amf0Converter) bool { return c.ToEcmaArray() != nil }},
{"strict-array", NewAmf0StrictArray(), func(c Amf0Converter) bool { return c.ToStrictArray() != nil }},
}
for _, tt := range values {
t.Run(tt.name, func(t *testing.T) {
converter := NewAmf0Converter(tt.in)
if !tt.ok(converter) {
t.Fatalf("expected successful conversion for %T", tt.in)
}
})
}
nilConverter := NewAmf0Converter(nil)
if nilConverter.ToNumber() != nil || nilConverter.ToBoolean() != nil || nilConverter.ToString() != nil ||
nilConverter.ToObject() != nil || nilConverter.ToNull() != nil || nilConverter.ToUndefined() != nil ||
nilConverter.ToEcmaArray() != nil || nilConverter.ToStrictArray() != nil {
t.Fatal("nil converter should not convert")
}
}
func TestAmf0UTF8(t *testing.T) {
var value amf0UTF8 = "hello"
b, err := value.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary() err=%v", err)
}
if value.Size() != len(b) {
t.Fatalf("Size()=%v, len=%v", value.Size(), len(b))
}
var decoded amf0UTF8
if err := decoded.UnmarshalBinary(b); err != nil {
t.Fatalf("UnmarshalBinary() err=%v", err)
}
if decoded != value {
t.Fatalf("decoded=%v, want %v", decoded, value)
}
for _, data := range [][]byte{{0x00}, {0x00, 0x05, 'h'}} {
if err := decoded.UnmarshalBinary(data); err == nil {
t.Fatalf("UnmarshalBinary(%v) should fail", data)
}
}
}
func TestAmf0Number(t *testing.T) {
number := NewAmf0Number(math.Pi)
if number.Size() != 9 || number.(*amf0Number).amf0Marker() != amf0MarkerNumber {
t.Fatalf("unexpected number metadata")
}
b, err := number.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary() err=%v", err)
}
decoded := NewAmf0Number(0)
if err := decoded.UnmarshalBinary(b); err != nil {
t.Fatalf("UnmarshalBinary() err=%v", err)
}
if got := decoded.Float64(); got != math.Pi {
t.Fatalf("Float64()=%v, want %v", got, math.Pi)
}
for _, data := range [][]byte{{byte(amf0MarkerNumber)}, append([]byte{byte(amf0MarkerString)}, b[1:]...)} {
if err := decoded.UnmarshalBinary(data); err == nil {
t.Fatalf("UnmarshalBinary(%v) should fail", data)
}
}
}
func TestAmf0Boolean(t *testing.T) {
for _, want := range []bool{false, true} {
boolean := NewAmf0Boolean(want)
if boolean.Size() != 2 || boolean.(*amf0Boolean).amf0Marker() != amf0MarkerBoolean {
t.Fatalf("unexpected boolean metadata")
}
b, err := boolean.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary() err=%v", err)
}
decoded := NewAmf0Boolean(!want)
if err := decoded.UnmarshalBinary(b); err != nil {
t.Fatalf("UnmarshalBinary() err=%v", err)
}
if got := decoded.Bool(); got != want {
t.Fatalf("Bool()=%v, want %v", got, want)
}
}
decoded := NewAmf0Boolean(false)
for _, data := range [][]byte{{byte(amf0MarkerBoolean)}, {byte(amf0MarkerNumber), 1}} {
if err := decoded.UnmarshalBinary(data); err == nil {
t.Fatalf("UnmarshalBinary(%v) should fail", data)
}
}
}
func TestAmf0String(t *testing.T) {
value := NewAmf0String("hello")
if value.Size() != 8 || value.(*amf0String).amf0Marker() != amf0MarkerString {
t.Fatalf("unexpected string metadata")
}
b, err := value.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary() err=%v", err)
}
decoded := NewAmf0String("")
if err := decoded.UnmarshalBinary(b); err != nil {
t.Fatalf("UnmarshalBinary() err=%v", err)
}
if got := decoded.String(); got != "hello" {
t.Fatalf("String()=%v, want hello", got)
}
for _, data := range [][]byte{{}, {byte(amf0MarkerNumber), 0, 0}, {byte(amf0MarkerString), 0, 5, 'h'}} {
if err := decoded.UnmarshalBinary(data); err == nil {
t.Fatalf("UnmarshalBinary(%v) should fail", data)
}
}
}
func TestAmf0ObjectEOF(t *testing.T) {
eof := &amf0ObjectEOF{}
if eof.Size() != 3 || eof.amf0Marker() != amf0MarkerObjectEnd {
t.Fatalf("unexpected eof metadata")
}
b, err := eof.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary() err=%v", err)
}
if !bytes.Equal(b, []byte{0, 0, 9}) {
t.Fatalf("MarshalBinary()=%v", b)
}
for _, data := range [][]byte{b, {0, 0, 9, 1}} {
if err := eof.UnmarshalBinary(data); err != nil {
t.Fatalf("UnmarshalBinary(%v) err=%v", data, err)
}
}
for _, data := range [][]byte{{0, 0}, {0, 1, 9}} {
if err := eof.UnmarshalBinary(data); err == nil {
t.Fatalf("UnmarshalBinary(%v) should fail", data)
}
}
}
func TestAmf0Object(t *testing.T) {
object := NewAmf0Object().
Set("name", NewAmf0String("stream")).
Set("code", NewAmf0Number(100)).
Set("ok", NewAmf0Boolean(true))
object.Set("code", NewAmf0Number(200))
if object.(*amf0Object).amf0Marker() != amf0MarkerObject || object.Size() == 0 {
t.Fatalf("unexpected object metadata")
}
if object.Get("missing") != nil {
t.Fatal("missing property should be nil")
}
b, err := object.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary() err=%v", err)
}
decoded := NewAmf0Object()
if err := decoded.UnmarshalBinary(b); err != nil {
t.Fatalf("UnmarshalBinary() err=%v", err)
}
if got := NewAmf0Converter(decoded.Get("name")).ToString().String(); got != "stream" {
t.Fatalf("name=%v", got)
}
if got := NewAmf0Converter(decoded.Get("code")).ToNumber().Float64(); got != 200 {
t.Fatalf("code=%v", got)
}
if got := NewAmf0Converter(decoded.Get("ok")).ToBoolean().Bool(); !got {
t.Fatalf("ok=%v", got)
}
for _, data := range [][]byte{{}, {byte(amf0MarkerString)}, {byte(amf0MarkerObject), 0, 4, 'n'}} {
if err := decoded.UnmarshalBinary(data); err == nil {
t.Fatalf("UnmarshalBinary(%v) should fail", data)
}
}
base := &amf0ObjectBase{}
if err := base.unmarshal(nil, false, -1); err == nil {
t.Fatal("unmarshal without eof and negative maxElems should fail")
}
if err := base.unmarshal(nil, true, 0); err == nil {
t.Fatal("unmarshal with eof and non-negative maxElems should fail")
}
}
func TestAmf0EcmaArray(t *testing.T) {
array := NewAmf0EcmaArray().
Set("name", NewAmf0String("stream")).
Set("code", NewAmf0Number(100))
if array.(*amf0EcmaArray).amf0Marker() != amf0MarkerEcmaArray || array.Size() == 0 {
t.Fatalf("unexpected ecma array metadata")
}
if array.Get("missing") != nil {
t.Fatal("missing property should be nil")
}
b, err := array.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary() err=%v", err)
}
decoded := NewAmf0EcmaArray()
if err := decoded.UnmarshalBinary(b); err != nil {
t.Fatalf("UnmarshalBinary() err=%v", err)
}
if got := NewAmf0Converter(decoded.Get("name")).ToString().String(); got != "stream" {
t.Fatalf("name=%v", got)
}
if got := NewAmf0Converter(decoded.Get("code")).ToNumber().Float64(); got != 100 {
t.Fatalf("code=%v", got)
}
for _, data := range [][]byte{{}, {byte(amf0MarkerEcmaArray), 0}, {byte(amf0MarkerString), 0, 0, 0, 0}, {byte(amf0MarkerEcmaArray), 0, 0, 0, 0, 0, 4, 'n'}} {
if err := decoded.UnmarshalBinary(data); err == nil {
t.Fatalf("UnmarshalBinary(%v) should fail", data)
}
}
}
func TestAmf0StrictArray(t *testing.T) {
array := NewAmf0StrictArray().
Set("name", NewAmf0String("stream")).
Set("code", NewAmf0Number(100))
array.(*amf0StrictArray).count = 2
if array.(*amf0StrictArray).amf0Marker() != amf0MarkerStrictArray || array.Size() == 0 {
t.Fatalf("unexpected strict array metadata")
}
if array.Get("missing") != nil {
t.Fatal("missing property should be nil")
}
b, err := array.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary() err=%v", err)
}
decoded := NewAmf0StrictArray()
if err := decoded.UnmarshalBinary(b); err != nil {
t.Fatalf("UnmarshalBinary() err=%v", err)
}
if got := NewAmf0Converter(decoded.Get("name")).ToString().String(); got != "stream" {
t.Fatalf("name=%v", got)
}
if got := NewAmf0Converter(decoded.Get("code")).ToNumber().Float64(); got != 100 {
t.Fatalf("code=%v", got)
}
empty := append([]byte{byte(amf0MarkerStrictArray)}, 0, 0, 0, 0)
if err := decoded.UnmarshalBinary(empty); err != nil {
t.Fatalf("UnmarshalBinary(empty) err=%v", err)
}
for _, data := range [][]byte{{}, {byte(amf0MarkerStrictArray), 0}, {byte(amf0MarkerString), 0, 0, 0, 0}, {byte(amf0MarkerStrictArray), 0, 0, 0, 1, 0, 4, 'n'}} {
if err := NewAmf0StrictArray().UnmarshalBinary(data); err == nil {
t.Fatalf("UnmarshalBinary(%v) should fail", data)
}
}
}
func TestAmf0SingleMarkerObjects(t *testing.T) {
for _, tt := range []struct {
name string
value Amf0Any
marker amf0Marker
}{
{"null", NewAmf0Null(), amf0MarkerNull},
{"undefined", NewAmf0Undefined(), amf0MarkerUndefined},
} {
t.Run(tt.name, func(t *testing.T) {
if tt.value.Size() != 1 || tt.value.amf0Marker() != tt.marker {
t.Fatalf("unexpected metadata")
}
b, err := tt.value.MarshalBinary()
if err != nil {
t.Fatalf("MarshalBinary() err=%v", err)
}
if err := tt.value.UnmarshalBinary(b); err != nil {
t.Fatalf("UnmarshalBinary() err=%v", err)
}
for _, data := range [][]byte{{}, {byte(amf0MarkerString)}} {
if err := tt.value.UnmarshalBinary(data); err == nil {
t.Fatalf("UnmarshalBinary(%v) should fail", data)
}
}
})
}
}
type errorAmf0Buffer struct {
writeByteErr bool
writeErr bool
}
func (v *errorAmf0Buffer) Bytes() []byte {
return nil
}
func (v *errorAmf0Buffer) WriteByte(byte) error {
if v.writeByteErr {
return fmt.Errorf("write byte")
}
return nil
}
func (v *errorAmf0Buffer) Write([]byte) (int, error) {
if v.writeErr {
return 0, fmt.Errorf("write")
}
return 0, nil
}
type errorAmf0Any struct {
Amf0Any
}
func (v *errorAmf0Any) Size() int {
return 1
}
func (v *errorAmf0Any) MarshalBinary() ([]byte, error) {
return nil, fmt.Errorf("marshal")
}
func (v *errorAmf0Any) UnmarshalBinary([]byte) error {
return nil
}
func (v *errorAmf0Any) amf0Marker() amf0Marker {
return amf0MarkerNumber
}
func TestAmf0MarshalErrors(t *testing.T) {
originalCreateBuffer := createBuffer
defer func() { createBuffer = originalCreateBuffer }()
for _, tt := range []struct {
name string
make func() Amf0Any
}{
{"object", func() Amf0Any { return NewAmf0Object() }},
{"ecma-array", func() Amf0Any { return NewAmf0EcmaArray() }},
{"strict-array", func() Amf0Any { return NewAmf0StrictArray() }},
} {
t.Run(tt.name+" write-byte", func(t *testing.T) {
createBuffer = func() amf0Buffer { return &errorAmf0Buffer{writeByteErr: true} }
if _, err := tt.make().MarshalBinary(); err == nil {
t.Fatal("MarshalBinary() should fail")
}
})
t.Run(tt.name+" write-prop", func(t *testing.T) {
createBuffer = func() amf0Buffer { return &errorAmf0Buffer{writeErr: true} }
value := tt.make()
switch v := value.(type) {
case Amf0Object:
v.Set("name", NewAmf0String("stream"))
case Amf0EcmaArray:
v.Set("name", NewAmf0String("stream"))
case Amf0StrictArray:
v.Set("name", NewAmf0String("stream"))
v.(*amf0StrictArray).count = 1
}
if _, err := value.MarshalBinary(); err == nil {
t.Fatal("MarshalBinary() should fail")
}
})
}
createBuffer = originalCreateBuffer
for _, tt := range []struct {
name string
make func() Amf0Any
}{
{"object", func() Amf0Any { return NewAmf0Object().Set("bad", &errorAmf0Any{}) }},
{"ecma-array", func() Amf0Any { return NewAmf0EcmaArray().Set("bad", &errorAmf0Any{}) }},
{"strict-array", func() Amf0Any {
value := NewAmf0StrictArray().Set("bad", &errorAmf0Any{})
value.(*amf0StrictArray).count = 1
return value
}},
} {
t.Run(tt.name+" marshal-value", func(t *testing.T) {
if _, err := tt.make().MarshalBinary(); err == nil {
t.Fatal("MarshalBinary() should fail")
}
})
}
}
func TestAmf0UnmarshalNestedErrors(t *testing.T) {
// Object property with unsupported marker.
data := []byte{byte(amf0MarkerObject), 0, 3, 'b', 'a', 'd', byte(amf0MarkerDate)}
if err := NewAmf0Object().UnmarshalBinary(data); err == nil || !strings.Contains(err.Error(), "discover prop bad") {
t.Fatalf("err=%v, want discover prop bad", err)
}
// Object property with invalid payload size.
data = []byte{byte(amf0MarkerObject), 0, 3, 'b', 'a', 'd', byte(amf0MarkerNumber), 0}
if err := NewAmf0Object().UnmarshalBinary(data); err == nil || !strings.Contains(err.Error(), "unmarshal prop bad") {
t.Fatalf("err=%v, want unmarshal prop bad", err)
}
}

View File

@ -0,0 +1,62 @@
// Copyright (c) 2026 Winlin
//
// SPDX-License-Identifier: MIT
package rtmp_test
import (
"fmt"
"srsx/internal/rtmp"
)
func ExampleAmf0Number() {
number := rtmp.NewAmf0Number(3.14)
b, err := number.MarshalBinary()
if err != nil {
panic(err)
}
value, err := rtmp.Amf0Discovery(b)
if err != nil {
panic(err)
}
if err := value.UnmarshalBinary(b); err != nil {
panic(err)
}
converter := rtmp.NewAmf0Converter(value)
fmt.Println("number:", converter.ToNumber().Float64())
fmt.Println("is string:", converter.ToString() != nil)
// Output:
// number: 3.14
// is string: false
}
func ExampleAmf0Object() {
object := rtmp.NewAmf0Object().
Set("code", rtmp.NewAmf0Number(100)).
Set("level", rtmp.NewAmf0String("status"))
b, err := object.MarshalBinary()
if err != nil {
panic(err)
}
value, err := rtmp.Amf0Discovery(b)
if err != nil {
panic(err)
}
if err := value.UnmarshalBinary(b); err != nil {
panic(err)
}
converter := rtmp.NewAmf0Converter(value)
fmt.Println("code:", rtmp.NewAmf0Converter(converter.ToObject().Get("code")).ToNumber().Float64())
fmt.Println("level:", rtmp.NewAmf0Converter(converter.ToObject().Get("level")).ToString().String())
fmt.Println("is number:", converter.ToNumber() != nil)
// Output:
// code: 100
// level: status
// is number: false
}

View File

@ -17,21 +17,31 @@ import (
"srsx/internal/errors" "srsx/internal/errors"
) )
// The handshake implements the RTMP handshake protocol. // Handshake implements the RTMP handshake protocol.
type Handshake struct { type Handshake interface {
C1S1() []byte
WriteC0S0(w io.Writer) error
ReadC0S0(r io.Reader) ([]byte, error)
WriteC1S1(w io.Writer) error
ReadC1S1(r io.Reader) ([]byte, error)
WriteC2S2(w io.Writer, s1c1 []byte) error
ReadC2S2(r io.Reader) ([]byte, error)
}
type handshake struct {
// The c1s1 cache. // The c1s1 cache.
c1s1 []byte c1s1 []byte
} }
func NewHandshake() *Handshake { func NewHandshake() Handshake {
return &Handshake{} return &handshake{}
} }
func (v *Handshake) C1S1() []byte { func (v *handshake) C1S1() []byte {
return v.c1s1 return v.c1s1
} }
func (v *Handshake) WriteC0S0(w io.Writer) (err error) { func (v *handshake) WriteC0S0(w io.Writer) (err error) {
r := bytes.NewReader([]byte{0x03}) r := bytes.NewReader([]byte{0x03})
if _, err = io.Copy(w, r); err != nil { if _, err = io.Copy(w, r); err != nil {
return errors.Wrap(err, "write c0s0") return errors.Wrap(err, "write c0s0")
@ -40,7 +50,7 @@ func (v *Handshake) WriteC0S0(w io.Writer) (err error) {
return return
} }
func (v *Handshake) ReadC0S0(r io.Reader) (c0 []byte, err error) { func (v *handshake) ReadC0S0(r io.Reader) (c0 []byte, err error) {
b := &bytes.Buffer{} b := &bytes.Buffer{}
if _, err = io.CopyN(b, r, 1); err != nil { if _, err = io.CopyN(b, r, 1); err != nil {
return nil, errors.Wrap(err, "read c0s0") return nil, errors.Wrap(err, "read c0s0")
@ -51,7 +61,7 @@ func (v *Handshake) ReadC0S0(r io.Reader) (c0 []byte, err error) {
return return
} }
func (v *Handshake) WriteC1S1(w io.Writer) (err error) { func (v *handshake) WriteC1S1(w io.Writer) (err error) {
p := make([]byte, 1536) p := make([]byte, 1536)
// Use crypto/rand for thread-safe random generation // Use crypto/rand for thread-safe random generation
@ -67,7 +77,7 @@ func (v *Handshake) WriteC1S1(w io.Writer) (err error) {
return return
} }
func (v *Handshake) ReadC1S1(r io.Reader) (c1s1 []byte, err error) { func (v *handshake) ReadC1S1(r io.Reader) (c1s1 []byte, err error) {
b := &bytes.Buffer{} b := &bytes.Buffer{}
if _, err = io.CopyN(b, r, 1536); err != nil { if _, err = io.CopyN(b, r, 1536); err != nil {
return nil, errors.Wrap(err, "read c1s1") return nil, errors.Wrap(err, "read c1s1")
@ -79,7 +89,7 @@ func (v *Handshake) ReadC1S1(r io.Reader) (c1s1 []byte, err error) {
return return
} }
func (v *Handshake) WriteC2S2(w io.Writer, s1c1 []byte) (err error) { func (v *handshake) WriteC2S2(w io.Writer, s1c1 []byte) (err error) {
r := bytes.NewReader(s1c1[:]) r := bytes.NewReader(s1c1[:])
if _, err = io.Copy(w, r); err != nil { if _, err = io.Copy(w, r); err != nil {
return errors.Wrap(err, "write c2s2") return errors.Wrap(err, "write c2s2")
@ -88,7 +98,7 @@ func (v *Handshake) WriteC2S2(w io.Writer, s1c1 []byte) (err error) {
return return
} }
func (v *Handshake) ReadC2S2(r io.Reader) (c2 []byte, err error) { func (v *handshake) ReadC2S2(r io.Reader) (c2 []byte, err error) {
b := &bytes.Buffer{} b := &bytes.Buffer{}
if _, err = io.CopyN(b, r, 1536); err != nil { if _, err = io.CopyN(b, r, 1536); err != nil {
return nil, errors.Wrap(err, "read c2s2") return nil, errors.Wrap(err, "read c2s2")
@ -129,7 +139,7 @@ type chunkStream struct {
format formatType format formatType
cid chunkID cid chunkID
header messageHeader header messageHeader
message *Message message *message
count uint64 count uint64
extendedTimestamp bool extendedTimestamp bool
} }
@ -138,8 +148,18 @@ func newChunkStream() *chunkStream {
return &chunkStream{} return &chunkStream{}
} }
// The protocol implements the RTMP command and chunk stack. // Protocol implements the RTMP command and chunk stack.
type Protocol struct { type Protocol interface {
// Deprecated: Please use rtmp.ExpectPacket instead.
ExpectPacket(ctx context.Context, ppkt any) (Message, error)
ExpectMessage(ctx context.Context, types ...MessageType) (Message, error)
DecodeMessage(m Message) (Packet, error)
ReadMessage(ctx context.Context) (Message, error)
WritePacket(ctx context.Context, pkt Packet, streamID int) error
WriteMessage(ctx context.Context, m Message) error
}
type protocol struct {
r *bufio.Reader r *bufio.Reader
w *bufio.Writer w *bufio.Writer
input struct { input struct {
@ -154,8 +174,8 @@ type Protocol struct {
} }
} }
func NewProtocol(rw io.ReadWriter) *Protocol { func NewProtocol(rw io.ReadWriter) Protocol {
v := &Protocol{ v := &protocol{
r: bufio.NewReader(rw), r: bufio.NewReader(rw),
w: bufio.NewWriter(rw), w: bufio.NewWriter(rw),
} }
@ -169,7 +189,7 @@ func NewProtocol(rw io.ReadWriter) *Protocol {
return v return v
} }
func ExpectPacket[T Packet](ctx context.Context, v *Protocol, ppkt *T) (m *Message, err error) { func ExpectPacket[T Packet](ctx context.Context, v Protocol, ppkt *T) (m Message, err error) {
for { for {
if m, err = v.ReadMessage(ctx); err != nil { if m, err = v.ReadMessage(ctx); err != nil {
return nil, errors.WithMessage(err, "read message") return nil, errors.WithMessage(err, "read message")
@ -190,11 +210,11 @@ func ExpectPacket[T Packet](ctx context.Context, v *Protocol, ppkt *T) (m *Messa
} }
// Deprecated: Please use rtmp.ExpectPacket instead. // Deprecated: Please use rtmp.ExpectPacket instead.
func (v *Protocol) ExpectPacket(ctx context.Context, ppkt any) (m *Message, err error) { func (v *protocol) ExpectPacket(ctx context.Context, ppkt any) (m Message, err error) {
panic("Please use rtmp.ExpectPacket instead") panic("Please use rtmp.ExpectPacket instead")
} }
func (v *Protocol) ExpectMessage(ctx context.Context, types ...MessageType) (m *Message, err error) { func (v *protocol) ExpectMessage(ctx context.Context, types ...MessageType) (m Message, err error) {
for { for {
if m, err = v.ReadMessage(ctx); err != nil { if m, err = v.ReadMessage(ctx); err != nil {
return nil, errors.WithMessage(err, "read message") return nil, errors.WithMessage(err, "read message")
@ -205,7 +225,7 @@ func (v *Protocol) ExpectMessage(ctx context.Context, types ...MessageType) (m *
} }
for _, t := range types { for _, t := range types {
if m.MessageType == t { if m.MessageType() == t {
return return
} }
} }
@ -214,7 +234,7 @@ func (v *Protocol) ExpectMessage(ctx context.Context, types ...MessageType) (m *
return return
} }
func (v *Protocol) parseAMFObject(p []byte) (pkt Packet, err error) { func (v *protocol) parseAMFObject(p []byte) (pkt Packet, err error) {
var commandName amf0String var commandName amf0String
if err = commandName.UnmarshalBinary(p); err != nil { if err = commandName.UnmarshalBinary(p); err != nil {
return nil, errors.WithMessage(err, "unmarshal command name") return nil, errors.WithMessage(err, "unmarshal command name")
@ -266,18 +286,18 @@ func (v *Protocol) parseAMFObject(p []byte) (pkt Packet, err error) {
} }
} }
func (v *Protocol) DecodeMessage(m *Message) (pkt Packet, err error) { func (v *protocol) DecodeMessage(m Message) (pkt Packet, err error) {
p := m.Payload[:] p := m.Payload()[:]
if len(p) == 0 { if len(p) == 0 {
return nil, errors.New("Empty packet") return nil, errors.New("Empty packet")
} }
switch m.MessageType { switch m.MessageType() {
case MessageTypeAMF3Command, MessageTypeAMF3Data: case MessageTypeAMF3Command, MessageTypeAMF3Data:
p = p[1:] p = p[1:]
} }
switch m.MessageType { switch m.MessageType() {
case MessageTypeSetChunkSize: case MessageTypeSetChunkSize:
pkt = NewSetChunkSize() pkt = NewSetChunkSize()
case MessageTypeWindowAcknowledgementSize: case MessageTypeWindowAcknowledgementSize:
@ -286,22 +306,22 @@ func (v *Protocol) DecodeMessage(m *Message) (pkt Packet, err error) {
pkt = NewSetPeerBandwidth() pkt = NewSetPeerBandwidth()
case MessageTypeAMF0Command, MessageTypeAMF3Command, MessageTypeAMF0Data, MessageTypeAMF3Data: case MessageTypeAMF0Command, MessageTypeAMF3Command, MessageTypeAMF0Data, MessageTypeAMF3Data:
if pkt, err = v.parseAMFObject(p); err != nil { if pkt, err = v.parseAMFObject(p); err != nil {
return nil, errors.WithMessage(err, fmt.Sprintf("Parse AMF %v", m.MessageType)) return nil, errors.WithMessage(err, fmt.Sprintf("Parse AMF %v", m.MessageType()))
} }
case MessageTypeUserControl: case MessageTypeUserControl:
pkt = NewUserControl() pkt = NewUserControl()
default: default:
return nil, errors.Errorf("Unknown message %v", m.MessageType) return nil, errors.Errorf("Unknown message %v", m.MessageType())
} }
if err = pkt.UnmarshalBinary(p); err != nil { if err = pkt.UnmarshalBinary(p); err != nil {
return nil, errors.WithMessage(err, fmt.Sprintf("Unmarshal %v", m.MessageType)) return nil, errors.WithMessage(err, fmt.Sprintf("Unmarshal %v", m.MessageType()))
} }
return return
} }
func (v *Protocol) ReadMessage(ctx context.Context) (m *Message, err error) { func (v *protocol) ReadMessage(ctx context.Context) (m Message, err error) {
for m == nil { for m == nil {
// TODO: We should convert buffered io to async io, because we will be stuck in block io here, // TODO: We should convert buffered io to async io, because we will be stuck in block io here,
// TODO: but the risk is acceptable because we literally will set the underlay io timeout. // TODO: but the risk is acceptable because we literally will set the underlay io timeout.
@ -331,15 +351,17 @@ func (v *Protocol) ReadMessage(ctx context.Context) (m *Message, err error) {
return nil, errors.WithMessage(err, "read message payload") return nil, errors.WithMessage(err, "read message payload")
} }
if err = v.onMessageArrivated(m); err != nil { if m != nil {
return nil, errors.WithMessage(err, "on message") if err = v.onMessageArrivated(m.asMessage()); err != nil {
return nil, errors.WithMessage(err, "on message")
}
} }
} }
return return
} }
func (v *Protocol) readMessagePayload(ctx context.Context, chunk *chunkStream) (m *Message, err error) { func (v *protocol) readMessagePayload(ctx context.Context, chunk *chunkStream) (m Message, err error) {
// Empty payload message. // Empty payload message.
if chunk.message.payloadLength == 0 { if chunk.message.payloadLength == 0 {
m = chunk.message m = chunk.message
@ -348,7 +370,7 @@ func (v *Protocol) readMessagePayload(ctx context.Context, chunk *chunkStream) (
} }
// Calculate the chunk payload size. // Calculate the chunk payload size.
chunkedPayloadSize := int(chunk.message.payloadLength) - len(chunk.message.Payload) chunkedPayloadSize := int(chunk.message.payloadLength) - len(chunk.message.payload)
if chunkedPayloadSize > int(v.input.opt.chunkSize) { if chunkedPayloadSize > int(v.input.opt.chunkSize) {
chunkedPayloadSize = int(v.input.opt.chunkSize) chunkedPayloadSize = int(v.input.opt.chunkSize)
} }
@ -357,10 +379,10 @@ func (v *Protocol) readMessagePayload(ctx context.Context, chunk *chunkStream) (
if _, err = io.ReadFull(v.r, b); err != nil { if _, err = io.ReadFull(v.r, b); err != nil {
return nil, errors.Wrapf(err, "read chunk %vB", chunkedPayloadSize) return nil, errors.Wrapf(err, "read chunk %vB", chunkedPayloadSize)
} }
chunk.message.Payload = append(chunk.message.Payload, b...) chunk.message.payload = append(chunk.message.payload, b...)
// Got entire RTMP message? // Got entire RTMP message?
if int(chunk.message.payloadLength) == len(chunk.message.Payload) { if int(chunk.message.payloadLength) == len(chunk.message.payload) {
m = chunk.message m = chunk.message
chunk.message = nil chunk.message = nil
} }
@ -426,7 +448,7 @@ var messageHeaderSizes = []int{11, 7, 3, 0}
// fmt=1, 0x4X // fmt=1, 0x4X
// fmt=2, 0x8X // fmt=2, 0x8X
// fmt=3, 0xCX // fmt=3, 0xCX
func (v *Protocol) readMessageHeader(ctx context.Context, chunk *chunkStream, format formatType) (err error) { func (v *protocol) readMessageHeader(ctx context.Context, chunk *chunkStream, format formatType) (err error) {
// We should not assert anything about fmt, for the first packet. // We should not assert anything about fmt, for the first packet.
// (when first packet, the chunk.message is nil). // (when first packet, the chunk.message is nil).
// the fmt maybe 0/1/2/3, the FMLE will send a 0xC4 for some audio packet. // the fmt maybe 0/1/2/3, the FMLE will send a 0xC4 for some audio packet.
@ -480,7 +502,7 @@ func (v *Protocol) readMessageHeader(ctx context.Context, chunk *chunkStream, fo
// Create msg when new chunk stream start // Create msg when new chunk stream start
if chunk.message == nil { if chunk.message == nil {
chunk.message = NewMessage() chunk.message = newMessage()
} }
// Read the message header. // Read the message header.
@ -659,7 +681,7 @@ func (v *Protocol) readMessageHeader(ctx context.Context, chunk *chunkStream, fo
// //
// Chunk stream IDs with values 64-319 could be represented by both 2- // Chunk stream IDs with values 64-319 could be represented by both 2-
// byte version and 3-byte version of this field. // byte version and 3-byte version of this field.
func (v *Protocol) readBasicHeader(ctx context.Context) (format formatType, cid chunkID, err error) { func (v *protocol) readBasicHeader(ctx context.Context) (format formatType, cid chunkID, err error) {
// 2-63, 1B chunk header // 2-63, 1B chunk header
var t uint8 var t uint8
if err = binary.Read(v.r, binary.BigEndian, &t); err != nil { if err = binary.Read(v.r, binary.BigEndian, &t); err != nil {
@ -689,14 +711,14 @@ func (v *Protocol) readBasicHeader(ctx context.Context) (format formatType, cid
return return
} }
func (v *Protocol) WritePacket(ctx context.Context, pkt Packet, streamID int) (err error) { func (v *protocol) WritePacket(ctx context.Context, pkt Packet, streamID int) (err error) {
m := NewMessage() m := newMessage()
if m.Payload, err = pkt.MarshalBinary(); err != nil { if m.payload, err = pkt.MarshalBinary(); err != nil {
return errors.WithMessage(err, "marshal payload") return errors.WithMessage(err, "marshal payload")
} }
m.MessageType = pkt.Type() m.messageHeader.MessageType = pkt.Type()
m.streamID = uint32(streamID) m.streamID = uint32(streamID)
m.betterCid = pkt.BetterCid() m.betterCid = pkt.BetterCid()
@ -711,7 +733,7 @@ func (v *Protocol) WritePacket(ctx context.Context, pkt Packet, streamID int) (e
return return
} }
func (v *Protocol) onPacketWriten(m *Message, pkt Packet) (err error) { func (v *protocol) onPacketWriten(m *message, pkt Packet) (err error) {
var tid amf0Number var tid amf0Number
var name amf0String var name amf0String
@ -734,16 +756,16 @@ func (v *Protocol) onPacketWriten(m *Message, pkt Packet) (err error) {
return return
} }
func (v *Protocol) onMessageArrivated(m *Message) (err error) { func (v *protocol) onMessageArrivated(m *message) (err error) {
if m == nil { if m == nil {
return return
} }
var pkt Packet var pkt Packet
switch m.MessageType { switch m.MessageType() {
case MessageTypeSetChunkSize, MessageTypeUserControl, MessageTypeWindowAcknowledgementSize: case MessageTypeSetChunkSize, MessageTypeUserControl, MessageTypeWindowAcknowledgementSize:
if pkt, err = v.DecodeMessage(m); err != nil { if pkt, err = v.DecodeMessage(m); err != nil {
return errors.Errorf("decode message %v", m.MessageType) return errors.Errorf("decode message %v", m.MessageType())
} }
} }
@ -755,19 +777,20 @@ func (v *Protocol) onMessageArrivated(m *Message) (err error) {
return return
} }
func (v *Protocol) WriteMessage(ctx context.Context, m *Message) (err error) { func (v *protocol) WriteMessage(ctx context.Context, m Message) (err error) {
m.payloadLength = uint32(len(m.Payload)) msg := m.asMessage()
msg.payloadLength = uint32(len(msg.payload))
var c0h, c3h []byte var c0h, c3h []byte
if c0h, err = m.generateC0Header(); err != nil { if c0h, err = msg.generateC0Header(); err != nil {
return errors.WithMessage(err, "generate c0 header") return errors.WithMessage(err, "generate c0 header")
} }
if c3h, err = m.generateC3Header(); err != nil { if c3h, err = msg.generateC3Header(); err != nil {
return errors.WithMessage(err, "generate c3 header") return errors.WithMessage(err, "generate c3 header")
} }
var h []byte var h []byte
p := m.Payload p := msg.payload
for len(p) > 0 { for len(p) > 0 {
// TODO: We should convert buffered io to async io, because we will be stuck in block io here, // TODO: We should convert buffered io to async io, because we will be stuck in block io here,
// TODO: but the risk is acceptable because we literally will set the underlay io timeout. // TODO: but the risk is acceptable because we literally will set the underlay io timeout.
@ -899,29 +922,56 @@ type messageHeader struct {
Timestamp uint64 Timestamp uint64
} }
// The RTMP message, transport over chunk stream in RTMP. // Message is an RTMP message transported over a chunk stream.
// Please read the cs id of @doc rtmp_specification_1.0.pdf, @page 30, @section 4.1. Message Header // Please read the cs id of @doc rtmp_specification_1.0.pdf, @page 30, @section 4.1. Message Header
type Message struct { type Message interface {
MessageType() MessageType
Timestamp() uint64
Payload() []byte
asMessage() *message
}
type message struct {
messageHeader messageHeader
// The payload which carries the RTMP packet. // The payload which carries the RTMP packet.
Payload []byte payload []byte
} }
func NewMessage() *Message { func NewMessage() Message {
return &Message{} return newMessage()
} }
func NewStreamMessage(streamID int) *Message { func newMessage() *message {
v := NewMessage() return &message{}
}
func NewStreamMessage(streamID int) Message {
v := newMessage()
v.streamID = uint32(streamID) v.streamID = uint32(streamID)
v.betterCid = chunkIDOverStream v.betterCid = chunkIDOverStream
return v return v
} }
func (v *Message) generateC3Header() ([]byte, error) { func (v *message) MessageType() MessageType {
return v.messageHeader.MessageType
}
func (v *message) Timestamp() uint64 {
return v.messageHeader.Timestamp
}
func (v *message) Payload() []byte {
return v.payload
}
func (v *message) asMessage() *message {
return v
}
func (v *message) generateC3Header() ([]byte, error) {
var c3h []byte var c3h []byte
if v.Timestamp < extendedTimestamp { if v.messageHeader.Timestamp < extendedTimestamp {
c3h = make([]byte, 1) c3h = make([]byte, 1)
} else { } else {
c3h = make([]byte, 1+4) c3h = make([]byte, 1+4)
@ -935,19 +985,19 @@ func (v *Message) generateC3Header() ([]byte, error) {
// but actually all products from adobe, such as FMS/AMS and Flash player and FMLE, // but actually all products from adobe, such as FMS/AMS and Flash player and FMLE,
// always carry a extended timestamp in C3 header. // always carry a extended timestamp in C3 header.
// @see: http://blog.csdn.net/win_lin/article/details/13363699 // @see: http://blog.csdn.net/win_lin/article/details/13363699
if v.Timestamp >= extendedTimestamp { if v.messageHeader.Timestamp >= extendedTimestamp {
p[0] = byte(v.Timestamp >> 24) p[0] = byte(v.messageHeader.Timestamp >> 24)
p[1] = byte(v.Timestamp >> 16) p[1] = byte(v.messageHeader.Timestamp >> 16)
p[2] = byte(v.Timestamp >> 8) p[2] = byte(v.messageHeader.Timestamp >> 8)
p[3] = byte(v.Timestamp) p[3] = byte(v.messageHeader.Timestamp)
} }
return c3h, nil return c3h, nil
} }
func (v *Message) generateC0Header() ([]byte, error) { func (v *message) generateC0Header() ([]byte, error) {
var c0h []byte var c0h []byte
if v.Timestamp < extendedTimestamp { if v.messageHeader.Timestamp < extendedTimestamp {
c0h = make([]byte, 1+3+3+1+4) c0h = make([]byte, 1+3+3+1+4)
} else { } else {
c0h = make([]byte, 1+3+3+1+4+4) c0h = make([]byte, 1+3+3+1+4+4)
@ -957,10 +1007,10 @@ func (v *Message) generateC0Header() ([]byte, error) {
p[0] = byte(v.betterCid) & 0x3f p[0] = byte(v.betterCid) & 0x3f
p = p[1:] p = p[1:]
if v.Timestamp < extendedTimestamp { if v.messageHeader.Timestamp < extendedTimestamp {
p[0] = byte(v.Timestamp >> 16) p[0] = byte(v.messageHeader.Timestamp >> 16)
p[1] = byte(v.Timestamp >> 8) p[1] = byte(v.messageHeader.Timestamp >> 8)
p[2] = byte(v.Timestamp) p[2] = byte(v.messageHeader.Timestamp)
} else { } else {
p[0] = 0xff p[0] = 0xff
p[1] = 0xff p[1] = 0xff
@ -973,7 +1023,7 @@ func (v *Message) generateC0Header() ([]byte, error) {
p[2] = byte(v.payloadLength) p[2] = byte(v.payloadLength)
p = p[3:] p = p[3:]
p[0] = byte(v.MessageType) p[0] = byte(v.messageHeader.MessageType)
p = p[1:] p = p[1:]
p[0] = byte(v.streamID) p[0] = byte(v.streamID)
@ -982,11 +1032,11 @@ func (v *Message) generateC0Header() ([]byte, error) {
p[3] = byte(v.streamID >> 24) p[3] = byte(v.streamID >> 24)
p = p[4:] p = p[4:]
if v.Timestamp >= extendedTimestamp { if v.messageHeader.Timestamp >= extendedTimestamp {
p[0] = byte(v.Timestamp >> 24) p[0] = byte(v.messageHeader.Timestamp >> 24)
p[1] = byte(v.Timestamp >> 16) p[1] = byte(v.messageHeader.Timestamp >> 16)
p[2] = byte(v.Timestamp >> 8) p[2] = byte(v.messageHeader.Timestamp >> 8)
p[3] = byte(v.Timestamp) p[3] = byte(v.messageHeader.Timestamp)
} }
return c0h, nil return c0h, nil
@ -1039,8 +1089,8 @@ type Packet interface {
type objectCallPacket struct { type objectCallPacket struct {
CommandName amf0String CommandName amf0String
TransactionID amf0Number TransactionID amf0Number
CommandObject *amf0Object CommandObject Amf0Object
Args *amf0Object Args Amf0Object
} }
func (v *objectCallPacket) BetterCid() chunkID { func (v *objectCallPacket) BetterCid() chunkID {
@ -1081,7 +1131,7 @@ func (v *objectCallPacket) UnmarshalBinary(data []byte) (err error) {
return return
} }
v.Args = NewAmf0Object() v.Args = newAmf0Object()
if err = v.Args.UnmarshalBinary(p); err != nil { if err = v.Args.UnmarshalBinary(p); err != nil {
return errors.WithMessage(err, "unmarshal args") return errors.WithMessage(err, "unmarshal args")
} }
@ -1149,8 +1199,8 @@ func (v *ConnectAppPacket) UnmarshalBinary(data []byte) (err error) {
func (v *ConnectAppPacket) TcUrl() string { func (v *ConnectAppPacket) TcUrl() string {
if v.CommandObject != nil { if v.CommandObject != nil {
if v, ok := v.CommandObject.Get("tcUrl").(*amf0String); ok { if v, ok := v.CommandObject.Get("tcUrl").(Amf0String); ok {
return string(*v) return v.String()
} }
} }
return "" return ""
@ -1172,9 +1222,9 @@ func NewConnectAppResPacket(tid amf0Number) *ConnectAppResPacket {
func (v *ConnectAppResPacket) SrsID() string { func (v *ConnectAppResPacket) SrsID() string {
if v.Args != nil { if v.Args != nil {
if v, ok := v.Args.Get("data").(*amf0EcmaArray); ok { if v, ok := v.Args.Get("data").(Amf0EcmaArray); ok {
if v, ok := v.Get("srs_id").(*amf0String); ok { if v, ok := v.Get("srs_id").(Amf0String); ok {
return string(*v) return v.String()
} }
} }
} }
@ -1197,7 +1247,7 @@ func (v *ConnectAppResPacket) UnmarshalBinary(data []byte) (err error) {
type variantCallPacket struct { type variantCallPacket struct {
CommandName amf0String CommandName amf0String
TransactionID amf0Number TransactionID amf0Number
CommandObject amf0Any // object or null CommandObject Amf0Any // object or null
} }
func (v *variantCallPacket) BetterCid() chunkID { func (v *variantCallPacket) BetterCid() chunkID {
@ -1273,7 +1323,7 @@ func (v *variantCallPacket) MarshalBinary() (data []byte, err error) {
// @remark onStatus packet is a call packet. // @remark onStatus packet is a call packet.
type CallPacket struct { type CallPacket struct {
variantCallPacket variantCallPacket
Args amf0Any // optional or object or null Args Amf0Any // optional or object or null
} }
func NewCallPacket() *CallPacket { func NewCallPacket() *CallPacket {
@ -1282,9 +1332,9 @@ func NewCallPacket() *CallPacket {
func (v *CallPacket) ArgsCode() string { func (v *CallPacket) ArgsCode() string {
if v.Args != nil { if v.Args != nil {
if v, ok := v.Args.(*amf0Object); ok { if v, ok := v.Args.(Amf0Object); ok {
if code, ok := v.Get("code").(*amf0String); ok { if code, ok := v.Get("code").(Amf0String); ok {
return string(*code) return code.String()
} }
} }
} }
@ -1370,6 +1420,10 @@ func NewCreateStreamResPacket(tid amf0Number) *CreateStreamResPacket {
return v return v
} }
func (v *CreateStreamResPacket) SetStreamID(streamID int) {
v.StreamID = amf0Number(streamID)
}
func (v *CreateStreamResPacket) Size() int { func (v *CreateStreamResPacket) Size() int {
return v.variantCallPacket.Size() + v.StreamID.Size() return v.variantCallPacket.Size() + v.StreamID.Size()
} }
@ -1407,15 +1461,16 @@ func (v *CreateStreamResPacket) MarshalBinary() (data []byte, err error) {
// Please read @doc rtmp_specification_1.0.pdf, @page 64, @section 4.2.6. Publish // Please read @doc rtmp_specification_1.0.pdf, @page 64, @section 4.2.6. Publish
type PublishPacket struct { type PublishPacket struct {
variantCallPacket variantCallPacket
StreamName amf0String StreamName Amf0String
StreamType amf0String StreamType Amf0String
} }
func NewPublishPacket() *PublishPacket { func NewPublishPacket() *PublishPacket {
v := &PublishPacket{} v := &PublishPacket{}
v.CommandName = commandPublish v.CommandName = commandPublish
v.CommandObject = NewAmf0Null() v.CommandObject = NewAmf0Null()
v.StreamType = "live" v.StreamName = NewAmf0String("")
v.StreamType = NewAmf0String("live")
return v return v
} }
@ -1431,11 +1486,13 @@ func (v *PublishPacket) UnmarshalBinary(data []byte) (err error) {
} }
p = p[v.variantCallPacket.Size():] p = p[v.variantCallPacket.Size():]
v.StreamName = newAmf0String("")
if err = v.StreamName.UnmarshalBinary(p); err != nil { if err = v.StreamName.UnmarshalBinary(p); err != nil {
return errors.WithMessage(err, "unmarshal stream name") return errors.WithMessage(err, "unmarshal stream name")
} }
p = p[v.StreamName.Size():] p = p[v.StreamName.Size():]
v.StreamType = newAmf0String("")
if err = v.StreamType.UnmarshalBinary(p); err != nil { if err = v.StreamType.UnmarshalBinary(p); err != nil {
return errors.WithMessage(err, "unmarshal stream type") return errors.WithMessage(err, "unmarshal stream type")
} }
@ -1466,13 +1523,14 @@ func (v *PublishPacket) MarshalBinary() (data []byte, err error) {
// Please read @doc rtmp_specification_1.0.pdf, @page 54, @section 4.2.1. play // Please read @doc rtmp_specification_1.0.pdf, @page 54, @section 4.2.1. play
type PlayPacket struct { type PlayPacket struct {
variantCallPacket variantCallPacket
StreamName amf0String StreamName Amf0String
} }
func NewPlayPacket() *PlayPacket { func NewPlayPacket() *PlayPacket {
v := &PlayPacket{} v := &PlayPacket{}
v.CommandName = commandPlay v.CommandName = commandPlay
v.CommandObject = NewAmf0Null() v.CommandObject = NewAmf0Null()
v.StreamName = NewAmf0String("")
return v return v
} }
@ -1488,6 +1546,7 @@ func (v *PlayPacket) UnmarshalBinary(data []byte) (err error) {
} }
p = p[v.variantCallPacket.Size():] p = p[v.variantCallPacket.Size():]
v.StreamName = newAmf0String("")
if err = v.StreamName.UnmarshalBinary(p); err != nil { if err = v.StreamName.UnmarshalBinary(p); err != nil {
return errors.WithMessage(err, "unmarshal stream name") return errors.WithMessage(err, "unmarshal stream name")
} }

View File

@ -229,7 +229,7 @@ func (v *RTMPConnection) serve(ctx context.Context, conn *net.TCPConn) error {
response = identifyRes response = identifyRes
nextStreamID = 1 nextStreamID = 1
identifyRes.StreamID = *rtmp.NewAmf0Number(float64(nextStreamID)) identifyRes.SetStreamID(nextStreamID)
} else if pkt.CommandName == "getStreamLength" { } else if pkt.CommandName == "getStreamLength" {
// Ignore and do not reply these packets. // Ignore and do not reply these packets.
} else { } else {
@ -243,7 +243,7 @@ func (v *RTMPConnection) serve(ctx context.Context, conn *net.TCPConn) error {
identifyRes.Args = rtmp.NewAmf0Undefined() identifyRes.Args = rtmp.NewAmf0Undefined()
} }
case *rtmp.PublishPacket: case *rtmp.PublishPacket:
streamName = string(pkt.StreamName) streamName = pkt.StreamName.String()
clientType = RTMPClientTypePublisher clientType = RTMPClientTypePublisher
identifyRes := rtmp.NewCallPacket() identifyRes := rtmp.NewCallPacket()
@ -257,7 +257,7 @@ func (v *RTMPConnection) serve(ctx context.Context, conn *net.TCPConn) error {
data.Set("description", rtmp.NewAmf0String("Started publishing stream.")) data.Set("description", rtmp.NewAmf0String("Started publishing stream."))
identifyRes.Args = data identifyRes.Args = data
case *rtmp.PlayPacket: case *rtmp.PlayPacket:
streamName = string(pkt.StreamName) streamName = pkt.StreamName.String()
clientType = RTMPClientTypeViewer clientType = RTMPClientTypeViewer
identifyRes := rtmp.NewCallPacket() identifyRes := rtmp.NewCallPacket()
@ -352,7 +352,7 @@ func (v *RTMPConnection) serve(ctx context.Context, conn *net.TCPConn) error {
if err != nil { if err != nil {
return errors.Wrapf(err, "read message") return errors.Wrapf(err, "read message")
} }
//logger.Debug(ctx, "client<- %v %v %vB", m.MessageType, m.Timestamp, len(m.Payload)) //logger.Debug(ctx, "client<- %v %v %vB", m.MessageType(), m.Timestamp(), len(m.Payload()))
// TODO: Update the stream ID if not the same. // TODO: Update the stream ID if not the same.
if err := client.WriteMessage(ctx, m); err != nil { if err := client.WriteMessage(ctx, m); err != nil {
@ -375,7 +375,7 @@ func (v *RTMPConnection) serve(ctx context.Context, conn *net.TCPConn) error {
if err != nil { if err != nil {
return errors.Wrapf(err, "read message") return errors.Wrapf(err, "read message")
} }
//logger.Debug(ctx, "client-> %v %v %vB", m.MessageType, m.Timestamp, len(m.Payload)) //logger.Debug(ctx, "client-> %v %v %vB", m.MessageType(), m.Timestamp(), len(m.Payload()))
// TODO: Update the stream ID if not the same. // TODO: Update the stream ID if not the same.
if err := backend.client.WriteMessage(ctx, m); err != nil { if err := backend.client.WriteMessage(ctx, m); err != nil {
@ -421,7 +421,7 @@ type RTMPClientToBackend struct {
// The underlayer tcp client. // The underlayer tcp client.
tcpConn *net.TCPConn tcpConn *net.TCPConn
// The RTMP protocol client. // The RTMP protocol client.
client *rtmp.Protocol client rtmp.Protocol
// The stream type. // The stream type.
typ RTMPClientType typ RTMPClientType
} }
@ -527,7 +527,7 @@ func (v *RTMPClientToBackend) Connect(ctx context.Context, tcUrl, streamName str
return v.publish(ctx, client, streamName) return v.publish(ctx, client, streamName)
} }
func (v *RTMPClientToBackend) publish(ctx context.Context, client *rtmp.Protocol, streamName string) error { func (v *RTMPClientToBackend) publish(ctx context.Context, client rtmp.Protocol, streamName string) error {
if true { if true {
identifyReq := rtmp.NewCallPacket() identifyReq := rtmp.NewCallPacket()
identifyReq.CommandName = "releaseStream" identifyReq.CommandName = "releaseStream"
@ -592,8 +592,8 @@ func (v *RTMPClientToBackend) publish(ctx context.Context, client *rtmp.Protocol
publishStream := rtmp.NewPublishPacket() publishStream := rtmp.NewPublishPacket()
publishStream.TransactionID = 5 publishStream.TransactionID = 5
publishStream.CommandObject = rtmp.NewAmf0Null() publishStream.CommandObject = rtmp.NewAmf0Null()
publishStream.StreamName = *rtmp.NewAmf0String(streamName) publishStream.StreamName = rtmp.NewAmf0String(streamName)
publishStream.StreamType = *rtmp.NewAmf0String("live") publishStream.StreamType = rtmp.NewAmf0String("live")
if err := client.WritePacket(ctx, publishStream, currentStreamID); err != nil { if err := client.WritePacket(ctx, publishStream, currentStreamID); err != nil {
return errors.Wrapf(err, "publish") return errors.Wrapf(err, "publish")
} }
@ -609,8 +609,8 @@ func (v *RTMPClientToBackend) publish(ctx context.Context, client *rtmp.Protocol
return errors.Errorf("onStatus args not object") return errors.Errorf("onStatus args not object")
} else if code := rtmp.NewAmf0Converter(data.Get("code")).ToString(); code == nil { } else if code := rtmp.NewAmf0Converter(data.Get("code")).ToString(); code == nil {
return errors.Errorf("onStatus code not string") return errors.Errorf("onStatus code not string")
} else if *code != "NetStream.Publish.Start" { } else if code.String() != "NetStream.Publish.Start" {
return errors.Errorf("onStatus code=%v not NetStream.Publish.Start", *code) return errors.Errorf("onStatus code=%v not NetStream.Publish.Start", code.String())
} }
break break
} }
@ -620,7 +620,7 @@ func (v *RTMPClientToBackend) publish(ctx context.Context, client *rtmp.Protocol
return nil return nil
} }
func (v *RTMPClientToBackend) play(ctx context.Context, client *rtmp.Protocol, streamName string) error { func (v *RTMPClientToBackend) play(ctx context.Context, client rtmp.Protocol, streamName string) error {
var currentStreamID int var currentStreamID int
if true { if true {
createStream := rtmp.NewCreateStreamPacket() createStream := rtmp.NewCreateStreamPacket()
@ -642,7 +642,7 @@ func (v *RTMPClientToBackend) play(ctx context.Context, client *rtmp.Protocol, s
} }
playStream := rtmp.NewPlayPacket() playStream := rtmp.NewPlayPacket()
playStream.StreamName = *rtmp.NewAmf0String(streamName) playStream.StreamName = rtmp.NewAmf0String(streamName)
if err := client.WritePacket(ctx, playStream, currentStreamID); err != nil { if err := client.WritePacket(ctx, playStream, currentStreamID); err != nil {
return errors.Wrapf(err, "play") return errors.Wrapf(err, "play")
} }