├── .gitignore ├── example ├── client_demo │ ├── .gitignore │ ├── Makefile │ └── main.go ├── server_demo │ ├── .gitignore │ ├── Makefile │ ├── main.go │ └── handler.go └── server_relay_demo │ ├── .gitignore │ ├── Makefile │ ├── README.md │ ├── main.go │ ├── relay_service.go │ ├── callback.go │ ├── pubsub.go │ └── handler.go ├── .golangci.yaml ├── context.go ├── internal └── error.go ├── go.mod ├── state_handler.go ├── chunk_streamer_writer.go ├── chunk_stream_reader.go ├── server_test.go ├── message ├── encoder_test.go ├── user_control_event_encoder_test.go ├── user_control_event_decoder_test.go ├── body_encoder.go ├── error.go ├── user_control_event.go ├── decoder_test.go ├── amf_convertible.go ├── net_stream_test.go ├── message_test.go ├── user_control_event_common_test.go ├── common_test.go ├── user_control_event_encoder.go ├── message.go ├── user_control_event_decoder.go ├── net_connection.go ├── encoder.go ├── net_stream.go ├── body_decoder_test.go ├── decoder.go └── body_decoder.go ├── server_data_publish_handler_test.go ├── .github └── workflows │ └── ci.yml ├── chunk_streamer_reader.go ├── chunk_stream_writer.go ├── streams_test.go ├── error.go ├── Makefile ├── server_data_play_handler.go ├── LICENCE.txt ├── server_conn.go ├── README.md ├── handler.go ├── client.go ├── handshake ├── encoder.go ├── decoder.go └── handshake.go ├── client_control_not_connected_handler.go ├── server_data_publish_handler.go ├── handler_test.go ├── response_preset.go ├── transactions.go ├── conn_test.go ├── streams.go ├── default_handler.go ├── stream_handler_test.go ├── server.go ├── go.sum ├── client_conn.go ├── conn_state.go ├── server_data_inactive_handler.go ├── server_control_not_connected_handler.go ├── server_client_test.go ├── server_control_connected_handler.go ├── conn.go ├── stream_handler.go ├── chunk_header.go ├── stream.go └── chunk_header_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | 3 | coverage.txt 4 | -------------------------------------------------------------------------------- /example/client_demo/.gitignore: -------------------------------------------------------------------------------- 1 | /client_demo 2 | -------------------------------------------------------------------------------- /example/server_demo/.gitignore: -------------------------------------------------------------------------------- 1 | /server_demo 2 | -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - nilerr 4 | -------------------------------------------------------------------------------- /example/server_relay_demo/.gitignore: -------------------------------------------------------------------------------- 1 | /server_relay_demo 2 | -------------------------------------------------------------------------------- /example/client_demo/Makefile: -------------------------------------------------------------------------------- 1 | PHONY: all 2 | all: 3 | go build -i -v -o client_demo . 4 | -------------------------------------------------------------------------------- /example/server_demo/Makefile: -------------------------------------------------------------------------------- 1 | PHONY: all 2 | all: 3 | go build -i -v -o server_demo . 4 | -------------------------------------------------------------------------------- /example/server_relay_demo/Makefile: -------------------------------------------------------------------------------- 1 | PHONY: all 2 | all: 3 | go build -i -v -o server_relay_demo . 4 | -------------------------------------------------------------------------------- /context.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2021- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | type StreamContext struct { 11 | StreamID uint32 12 | } 13 | -------------------------------------------------------------------------------- /internal/error.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package internal 9 | 10 | import ( 11 | "errors" 12 | ) 13 | 14 | var ErrChunkIsNotCompleted = errors.New("Chunk is not completed") 15 | var ErrPassThroughMsg = errors.New("Msg is passed through") 16 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/yutopp/go-rtmp 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/fortytw2/leaktest v1.2.0 7 | github.com/hashicorp/errwrap v1.1.0 // indirect 8 | github.com/hashicorp/go-multierror v1.1.0 9 | github.com/mitchellh/mapstructure v1.4.1 10 | github.com/pkg/errors v0.9.1 11 | github.com/sirupsen/logrus v1.7.0 12 | github.com/stretchr/testify v1.8.1 13 | github.com/yutopp/go-amf0 v0.1.0 14 | github.com/yutopp/go-flv v0.3.1 15 | golang.org/x/sys v0.3.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /example/server_relay_demo/README.md: -------------------------------------------------------------------------------- 1 | ## Server relay example 2 | 3 | A minimum example to relay RTMP streams on local. Streams will be published and can be subscribed per publish name. 4 | 5 | ### server console 6 | 7 | ``` 8 | make 9 | ./server_relay_demo 10 | ``` 11 | 12 | ### publisher console 13 | 14 | ``` 15 | curl -L https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/1080/Big_Buck_Bunny_1080_10s_1MB.mp4 -o movie.mp4 16 | ffmpeg -re -stream_loop -1 -i movie.mp4 -acodec copy -vcodec copy -f flv rtmp://localhost/appname/stream 17 | ``` 18 | 19 | ### subscriber console 20 | 21 | ``` 22 | ffplay rtmp://localhost/appname/stream 23 | ``` 24 | -------------------------------------------------------------------------------- /state_handler.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "github.com/yutopp/go-rtmp/message" 12 | ) 13 | 14 | type stateHandler interface { 15 | onMessage(chunkStreamID int, timestamp uint32, msg message.Message) error 16 | onData(chunkStreamID int, timestamp uint32, dataMsg *message.DataMessage, body interface{}) error 17 | onCommand(chunkStreamID int, timestamp uint32, cmdMsg *message.CommandMessage, body interface{}) error 18 | } 19 | -------------------------------------------------------------------------------- /chunk_streamer_writer.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "bufio" 12 | "io" 13 | ) 14 | 15 | type ChunkStreamerWriter struct { 16 | writer io.Writer 17 | } 18 | 19 | func (w *ChunkStreamerWriter) Write(buf []byte) (int, error) { 20 | return w.writer.Write(buf) 21 | } 22 | 23 | func (w *ChunkStreamerWriter) Flush() error { 24 | bufw, ok := w.writer.(*bufio.Writer) 25 | if !ok { 26 | return nil 27 | } 28 | return bufw.Flush() 29 | } 30 | -------------------------------------------------------------------------------- /chunk_stream_reader.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "bytes" 12 | ) 13 | 14 | type ChunkStreamReader struct { 15 | basicHeader chunkBasicHeader 16 | messageHeader chunkMessageHeader 17 | 18 | timestamp uint32 19 | timestampDelta uint32 20 | messageLength uint32 // max, 24bits 21 | messageTypeID byte 22 | messageStreamID uint32 23 | 24 | buf bytes.Buffer 25 | completed bool 26 | } 27 | 28 | func (r *ChunkStreamReader) Read(b []byte) (int, error) { 29 | return r.buf.Read(b) 30 | } 31 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "net" 12 | "testing" 13 | "time" 14 | 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | func TestServerCanClose(t *testing.T) { 19 | srv := NewServer(&ServerConfig{}) 20 | 21 | go func(ch <-chan time.Time) { 22 | <-ch 23 | err := srv.Close() 24 | require.Nil(t, err) 25 | }(time.After(1 * time.Second)) 26 | 27 | l, err := net.Listen("tcp", "127.0.0.1:") 28 | require.Nil(t, err) 29 | 30 | err = srv.Serve(l) 31 | require.Equal(t, ErrClosed, err) 32 | } 33 | -------------------------------------------------------------------------------- /message/encoder_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "bytes" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestEncodeCommon(t *testing.T) { 18 | for _, tc := range testCases { 19 | tc := tc // capture 20 | 21 | t.Run(tc.Name, func(t *testing.T) { 22 | t.Parallel() 23 | 24 | buf := new(bytes.Buffer) 25 | 26 | enc := NewEncoder(buf) 27 | err := enc.Encode(tc.Value) 28 | require.Nil(t, err) 29 | require.Equal(t, tc.Binary, buf.Bytes()) 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /server_data_publish_handler_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/yutopp/go-rtmp/message" 14 | ) 15 | 16 | func BenchmarkHandlePublisherVideoMessage(b *testing.B) { 17 | rwc := &rwcMock{} 18 | c := newConn(rwc, nil) 19 | 20 | s := newStream(42, c) 21 | s.handler.ChangeState(streamStateServerPublish) 22 | 23 | chunkStreamID := 0 24 | timestamp := uint32(0) 25 | msg := &message.VideoMessage{} 26 | 27 | b.ResetTimer() 28 | for i := 0; i < b.N; i++ { 29 | _ = s.handle(chunkStreamID, timestamp, msg) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /message/user_control_event_encoder_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "bytes" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestUserControlEventEncoderCommon(t *testing.T) { 18 | for _, tc := range uceTestCases { 19 | tc := tc // capture 20 | 21 | t.Run(tc.Name, func(t *testing.T) { 22 | t.Parallel() 23 | 24 | buf := new(bytes.Buffer) 25 | 26 | enc := NewUserControlEventEncoder(buf) 27 | err := enc.Encode(tc.Value) 28 | require.Nil(t, err) 29 | require.Equal(t, tc.Binary, buf.Bytes()) 30 | }) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /message/user_control_event_decoder_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "bytes" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestUserControlEventDecodeCommon(t *testing.T) { 18 | for _, tc := range uceTestCases { 19 | tc := tc // capture 20 | 21 | t.Run(tc.Name, func(t *testing.T) { 22 | t.Parallel() 23 | 24 | buf := bytes.NewReader(tc.Binary) 25 | dec := NewUserControlEventDecoder(buf) 26 | 27 | var msg UserCtrlEvent 28 | err := dec.Decode(&msg) 29 | require.Nil(t, err) 30 | require.Equal(t, tc.Value, msg) 31 | }) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | build: 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | go: [ '1.22.0', '1.21.0', '1.20', '1.19' ] 10 | name: ${{ matrix.go }} 11 | steps: 12 | - uses: actions/checkout@v4 13 | 14 | - name: Setup go 15 | uses: actions/setup-go@v5 16 | with: 17 | go-version: ${{ matrix.go }} 18 | 19 | - run: make download-ci-tools 20 | 21 | - env: 22 | REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | run: make lint-ci 24 | 25 | - run: make test 26 | 27 | - run: make vet 28 | 29 | - uses: codecov/codecov-action@v4 30 | with: 31 | files: ./coverage.txt 32 | flags: unittests 33 | name: codecov-umbrella-${{ matrix.go }} 34 | token: ${{ secrets.CODECOV_TOKEN }} 35 | verbose: true 36 | -------------------------------------------------------------------------------- /message/body_encoder.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "github.com/pkg/errors" 12 | "github.com/yutopp/go-amf0" 13 | ) 14 | 15 | func EncodeBodyAnyValues(e AMFEncoder, v AMFConvertible) error { 16 | if v == nil { 17 | return nil // Do nothing 18 | } 19 | 20 | var amfTy EncodingType 21 | switch e.(type) { 22 | case *amf0.Encoder: 23 | amfTy = EncodingTypeAMF0 24 | default: 25 | return errors.Errorf("Unsupported AMF Encoder: Type = %T", e) 26 | } 27 | 28 | args, err := v.ToArgs(amfTy) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | for _, arg := range args { 34 | if err := e.Encode(arg); err != nil { 35 | return err 36 | } 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /message/error.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "fmt" 12 | ) 13 | 14 | type UnknownDataBodyDecodeError struct { 15 | Name string 16 | Objs []interface{} 17 | } 18 | 19 | func (e *UnknownDataBodyDecodeError) Error() string { 20 | return fmt.Sprintf("UnknownDataBodyDecodeError: Name = %s, Objs = %+v", e.Name, e.Objs) 21 | } 22 | 23 | type UnknownCommandBodyDecodeError struct { 24 | Name string 25 | TransactionID int64 26 | Objs []interface{} 27 | } 28 | 29 | func (e *UnknownCommandBodyDecodeError) Error() string { 30 | return fmt.Sprintf("UnknownCommandMessageDecodeError: Name = %s, TransactionID = %d, Objs = %+v", 31 | e.Name, 32 | e.TransactionID, 33 | e.Objs, 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /chunk_streamer_reader.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "io" 12 | ) 13 | 14 | type ChunkStreamerReader struct { 15 | reader io.Reader 16 | totalReadBytes uint32 // TODO: Check overflow 17 | fragmentReadBytes uint32 18 | } 19 | 20 | func (r *ChunkStreamerReader) Read(b []byte) (int, error) { 21 | n, err := r.reader.Read(b) 22 | r.totalReadBytes += uint32(n) 23 | r.fragmentReadBytes += uint32(n) 24 | return n, err 25 | } 26 | 27 | func (r *ChunkStreamerReader) TotalReadBytes() uint32 { 28 | return r.totalReadBytes 29 | } 30 | 31 | func (r *ChunkStreamerReader) FragmentReadBytes() uint32 { 32 | return r.fragmentReadBytes 33 | } 34 | 35 | func (r *ChunkStreamerReader) ResetFragmentReadBytes() { 36 | r.fragmentReadBytes = 0 37 | } 38 | -------------------------------------------------------------------------------- /example/server_demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net" 6 | 7 | log "github.com/sirupsen/logrus" 8 | "github.com/yutopp/go-rtmp" 9 | ) 10 | 11 | func main() { 12 | tcpAddr, err := net.ResolveTCPAddr("tcp", ":1935") 13 | if err != nil { 14 | log.Panicf("Failed: %+v", err) 15 | } 16 | 17 | listener, err := net.ListenTCP("tcp", tcpAddr) 18 | if err != nil { 19 | log.Panicf("Failed: %+v", err) 20 | } 21 | 22 | srv := rtmp.NewServer(&rtmp.ServerConfig{ 23 | OnConnect: func(conn net.Conn) (io.ReadWriteCloser, *rtmp.ConnConfig) { 24 | l := log.StandardLogger() 25 | //l.SetLevel(logrus.DebugLevel) 26 | 27 | h := &Handler{} 28 | 29 | return conn, &rtmp.ConnConfig{ 30 | Handler: h, 31 | 32 | ControlState: rtmp.StreamControlStateConfig{ 33 | DefaultBandwidthWindowSize: 6 * 1024 * 1024 / 8, 34 | }, 35 | 36 | Logger: l, 37 | } 38 | }, 39 | }) 40 | if err := srv.Serve(listener); err != nil { 41 | log.Panicf("Failed: %+v", err) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /chunk_stream_writer.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "context" 12 | "sync" 13 | ) 14 | 15 | type ChunkStreamWriter struct { 16 | ChunkStreamReader 17 | 18 | doneCh chan struct{} 19 | closeCh chan struct{} 20 | lastErr error 21 | aqM sync.Mutex 22 | newChunk bool 23 | } 24 | 25 | func (w *ChunkStreamWriter) Write(b []byte) (int, error) { 26 | return w.buf.Write(b) 27 | } 28 | 29 | func (w *ChunkStreamWriter) Wait(ctx context.Context) error { 30 | w.aqM.Lock() 31 | defer w.aqM.Unlock() 32 | 33 | select { 34 | case <-w.doneCh: 35 | if w.lastErr != nil { 36 | return w.lastErr 37 | } 38 | 39 | w.doneCh = make(chan struct{}) 40 | return nil 41 | 42 | case <-w.closeCh: 43 | return w.lastErr 44 | 45 | case <-ctx.Done(): 46 | return ctx.Err() 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /streams_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestStreams(t *testing.T) { 17 | b := &rwcMock{} 18 | conn := newConn(b, &ConnConfig{ 19 | ControlState: StreamControlStateConfig{ 20 | MaxMessageStreams: 1, 21 | }, 22 | }) 23 | 24 | streams := newStreams(conn) 25 | 26 | s, err := streams.CreateIfAvailable() 27 | require.Nil(t, err) 28 | require.Equal(t, uint32(0), s.streamID) 29 | 30 | // Becomes error because number of max streams is 1 31 | _, err = streams.CreateIfAvailable() 32 | require.NotNil(t, err) 33 | 34 | err = streams.Delete(s.streamID) 35 | require.Nil(t, err) 36 | 37 | // Becomes error because the stream is already deleted 38 | err = streams.Delete(s.streamID) 39 | require.NotNil(t, err) 40 | } 41 | -------------------------------------------------------------------------------- /example/client_demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | log "github.com/sirupsen/logrus" 5 | 6 | "github.com/yutopp/go-rtmp" 7 | rtmpmsg "github.com/yutopp/go-rtmp/message" 8 | ) 9 | 10 | const ( 11 | chunkSize = 128 12 | ) 13 | 14 | func main() { 15 | client, err := rtmp.Dial("rtmp", "localhost:1935", &rtmp.ConnConfig{ 16 | Logger: log.StandardLogger(), 17 | }) 18 | if err != nil { 19 | log.Fatalf("Failed to dial: %+v", err) 20 | } 21 | defer client.Close() 22 | log.Infof("Client created") 23 | 24 | if err := client.Connect(nil); err != nil { 25 | log.Fatalf("Failed to connect: Err=%+v", err) 26 | } 27 | log.Infof("connected") 28 | 29 | stream, err := client.CreateStream(nil, chunkSize) 30 | if err != nil { 31 | log.Fatalf("Failed to create stream: Err=%+v", err) 32 | } 33 | defer stream.Close() 34 | 35 | if err := stream.Publish(&rtmpmsg.NetStreamPublish{ 36 | PublishingName: "testtesttesttest", 37 | PublishingType: "live", 38 | }); err != nil { 39 | log.Fatalf("Failed to send publish message: Err=%+v", err) 40 | } 41 | 42 | log.Infof("stream created") 43 | } 44 | -------------------------------------------------------------------------------- /example/server_relay_demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "net" 6 | 7 | log "github.com/sirupsen/logrus" 8 | "github.com/yutopp/go-rtmp" 9 | ) 10 | 11 | func main() { 12 | tcpAddr, err := net.ResolveTCPAddr("tcp", ":1935") 13 | if err != nil { 14 | log.Panicf("Failed: %+v", err) 15 | } 16 | 17 | listener, err := net.ListenTCP("tcp", tcpAddr) 18 | if err != nil { 19 | log.Panicf("Failed: %+v", err) 20 | } 21 | 22 | relayService := NewRelayService() 23 | 24 | srv := rtmp.NewServer(&rtmp.ServerConfig{ 25 | OnConnect: func(conn net.Conn) (io.ReadWriteCloser, *rtmp.ConnConfig) { 26 | l := log.StandardLogger() 27 | //l.SetLevel(logrus.DebugLevel) 28 | 29 | h := &Handler{ 30 | relayService: relayService, 31 | } 32 | 33 | return conn, &rtmp.ConnConfig{ 34 | Handler: h, 35 | 36 | ControlState: rtmp.StreamControlStateConfig{ 37 | DefaultBandwidthWindowSize: 6 * 1024 * 1024 / 8, 38 | }, 39 | 40 | Logger: l, 41 | } 42 | }, 43 | }) 44 | if err := srv.Serve(listener); err != nil { 45 | log.Panicf("Failed: %+v", err) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "fmt" 12 | 13 | "github.com/pkg/errors" 14 | 15 | "github.com/yutopp/go-rtmp/message" 16 | ) 17 | 18 | var ErrClosed = errors.New("Server is closed") 19 | 20 | type ConnectRejectedError struct { 21 | TransactionID int64 22 | Result *message.NetConnectionConnectResult 23 | } 24 | 25 | func (err *ConnectRejectedError) Error() string { 26 | return fmt.Sprintf( 27 | "Connect is rejected: TransactionID = %d, Result = %#v", 28 | err.TransactionID, 29 | err.Result, 30 | ) 31 | } 32 | 33 | type CreateStreamRejectedError struct { 34 | TransactionID int64 35 | Result *message.NetConnectionCreateStreamResult 36 | } 37 | 38 | func (err *CreateStreamRejectedError) Error() string { 39 | return fmt.Sprintf( 40 | "CreateStream is rejected: TransactionID = %d, Result = %#v", 41 | err.TransactionID, 42 | err.Result, 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /message/user_control_event.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | type UserCtrlEvent interface{} 11 | 12 | // UserCtrlEventStreamBegin (0) 13 | type UserCtrlEventStreamBegin struct { 14 | StreamID uint32 15 | } 16 | 17 | // UserCtrlEventStreamEOF (1) 18 | type UserCtrlEventStreamEOF struct { 19 | StreamID uint32 20 | } 21 | 22 | // UserCtrlEventStreamDry (2) 23 | type UserCtrlEventStreamDry struct { 24 | StreamID uint32 25 | } 26 | 27 | // UserCtrlEventSetBufferLength (3) 28 | type UserCtrlEventSetBufferLength struct { 29 | StreamID uint32 30 | LengthMs uint32 31 | } 32 | 33 | // UserCtrlEventStreamIsRecorded (4) 34 | type UserCtrlEventStreamIsRecorded struct { 35 | StreamID uint32 36 | } 37 | 38 | // UserCtrlEventPingRequest (6) 39 | type UserCtrlEventPingRequest struct { 40 | Timestamp uint32 41 | } 42 | 43 | // UserCtrlEventPingResponse (7) 44 | type UserCtrlEventPingResponse struct { 45 | Timestamp uint32 46 | } 47 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | FMT_DIRS:=$(shell go list -f {{.Dir}} ./...) 2 | 3 | .PHONY: all 4 | all: check test 5 | 6 | .PHONY: check 7 | check: fmt lint vet 8 | 9 | .PHONY: download-ci-tools 10 | download-ci-tools: 11 | go install golang.org/x/tools/cmd/goimports@latest 12 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.53.1 13 | curl -sSfL https://raw.githubusercontent.com/reviewdog/reviewdog/master/install.sh | sh -s v0.14.2 14 | 15 | .PHONY: fmt 16 | fmt: 17 | @gofmt -l -w -s $(FMT_DIRS) 18 | @goimports -w $(FMT_DIRS) 19 | 20 | .PHONY: lint 21 | lint: 22 | ./bin/golangci-lint run 23 | 24 | .PHONY: lint-ci 25 | lint-ci: 26 | ./bin/golangci-lint run | \ 27 | ./bin/reviewdog -f=golangci-lint -reporter=github-pr-review -filter-mode=nofilter 28 | 29 | .PHONY: vet 30 | vet: 31 | go vet $$(go list ./... | grep -v /vendor/) 32 | 33 | .PHONY: test 34 | test: 35 | go test -cover -coverprofile=coverage.txt -covermode=atomic -v -race -timeout 10s ./... 36 | 37 | .PHONY: bench 38 | bench: 39 | go test -bench . -benchmem -gcflags="-m -m -l" ./... 40 | 41 | .PHONY: example 42 | example: 43 | make -C example/server_demo 44 | make -C example/server_relay_demo 45 | make -C example/client_demo 46 | -------------------------------------------------------------------------------- /server_data_play_handler.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "github.com/yutopp/go-rtmp/internal" 12 | "github.com/yutopp/go-rtmp/message" 13 | ) 14 | 15 | var _ stateHandler = (*serverDataPlayHandler)(nil) 16 | 17 | // serverDataPlayHandler Handle data messages from a player at server side (NOT IMPLEMENTED). 18 | // 19 | // transitions: 20 | // | _ -> self 21 | type serverDataPlayHandler struct { 22 | sh *streamHandler 23 | } 24 | 25 | func (h *serverDataPlayHandler) onMessage( 26 | chunkStreamID int, 27 | timestamp uint32, 28 | msg message.Message, 29 | ) error { 30 | return internal.ErrPassThroughMsg 31 | } 32 | 33 | func (h *serverDataPlayHandler) onData( 34 | chunkStreamID int, 35 | timestamp uint32, 36 | dataMsg *message.DataMessage, 37 | body interface{}, 38 | ) error { 39 | return internal.ErrPassThroughMsg 40 | } 41 | 42 | func (h *serverDataPlayHandler) onCommand( 43 | chunkStreamID int, 44 | timestamp uint32, 45 | cmdMsg *message.CommandMessage, 46 | body interface{}, 47 | ) error { 48 | return internal.ErrPassThroughMsg 49 | } 50 | -------------------------------------------------------------------------------- /example/server_relay_demo/relay_service.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // TODO: Create this service per apps. 9 | // In this example, this instance is singleton. 10 | type RelayService struct { 11 | streams map[string]*Pubsub 12 | m sync.Mutex 13 | } 14 | 15 | func NewRelayService() *RelayService { 16 | return &RelayService{ 17 | streams: make(map[string]*Pubsub), 18 | } 19 | } 20 | 21 | func (s *RelayService) NewPubsub(key string) (*Pubsub, error) { 22 | s.m.Lock() 23 | defer s.m.Unlock() 24 | 25 | if _, ok := s.streams[key]; ok { 26 | return nil, fmt.Errorf("Already published: %s", key) 27 | } 28 | 29 | pubsub := NewPubsub(s, key) 30 | 31 | s.streams[key] = pubsub 32 | 33 | return pubsub, nil 34 | } 35 | 36 | func (s *RelayService) GetPubsub(key string) (*Pubsub, error) { 37 | s.m.Lock() 38 | defer s.m.Unlock() 39 | 40 | pubsub, ok := s.streams[key] 41 | if !ok { 42 | return nil, fmt.Errorf("Not published: %s", key) 43 | } 44 | 45 | return pubsub, nil 46 | } 47 | 48 | func (s *RelayService) RemovePubsub(key string) error { 49 | s.m.Lock() 50 | defer s.m.Unlock() 51 | 52 | if _, ok := s.streams[key]; !ok { 53 | return fmt.Errorf("Not published: %s", key) 54 | } 55 | 56 | delete(s.streams, key) 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /message/decoder_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "bytes" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestDecodeCommon(t *testing.T) { 18 | for _, tc := range testCases { 19 | tc := tc // capture 20 | 21 | t.Run(tc.Name, func(t *testing.T) { 22 | t.Parallel() 23 | 24 | buf := bytes.NewReader(tc.Binary) 25 | dec := NewDecoder(buf) 26 | 27 | var msg Message 28 | err := dec.Decode(tc.TypeID, &msg) 29 | require.Nil(t, err) 30 | assertEqualMessage(t, tc.Value, msg) 31 | }) 32 | } 33 | } 34 | 35 | func BenchmarkDecode5KBVideoMessage(b *testing.B) { 36 | sizes := []struct { 37 | name string 38 | len int 39 | }{ 40 | {"5KB", 5 * 1024}, 41 | {"2MB", 2 * 1024 * 1024}, 42 | } 43 | for _, size := range sizes { 44 | b.Run(size.name, func(b *testing.B) { 45 | buf := make([]byte, size.len) 46 | r := bytes.NewReader(buf) 47 | dec := NewDecoder(r) 48 | 49 | b.ResetTimer() 50 | for i := 0; i < b.N; i++ { 51 | r.Reset(buf) 52 | 53 | var msg Message 54 | _ = dec.Decode(TypeIDVideoMessage, &msg) 55 | } 56 | }) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /message/amf_convertible.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "io" 12 | 13 | "github.com/yutopp/go-amf0" 14 | ) 15 | 16 | type EncodingType uint8 17 | 18 | const ( 19 | EncodingTypeAMF0 EncodingType = 0 20 | EncodingTypeAMF3 EncodingType = 3 21 | ) 22 | 23 | type AMFConvertible interface { 24 | FromArgs(args ...interface{}) error 25 | ToArgs(ty EncodingType) ([]interface{}, error) 26 | } 27 | 28 | type AMFDecoder interface { 29 | Decode(interface{}) error 30 | Reset(r io.Reader) 31 | } 32 | 33 | func NewAMFDecoder(r io.Reader, encTy EncodingType) AMFDecoder { 34 | switch encTy { 35 | case EncodingTypeAMF3: 36 | panic("Unsupported encoding: AMF3") 37 | case EncodingTypeAMF0: 38 | return amf0.NewDecoder(r) 39 | default: 40 | panic("Unreachable") 41 | } 42 | } 43 | 44 | type AMFEncoder interface { 45 | Encode(interface{}) error 46 | Reset(w io.Writer) 47 | } 48 | 49 | func NewAMFEncoder(w io.Writer, encTy EncodingType) AMFEncoder { 50 | switch encTy { 51 | case EncodingTypeAMF3: 52 | panic("Unsupported encoding: AMF3") 53 | case EncodingTypeAMF0: 54 | return amf0.NewEncoder(w) 55 | default: 56 | panic("Unreachable") 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /LICENCE.txt: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /server_conn.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "github.com/pkg/errors" 12 | 13 | "github.com/yutopp/go-rtmp/handshake" 14 | ) 15 | 16 | // serverConn A wrapper of a connection. It prorives server-side specific features. 17 | type serverConn struct { 18 | conn *Conn 19 | } 20 | 21 | func newServerConn(conn *Conn) *serverConn { 22 | return &serverConn{ 23 | conn: conn, 24 | } 25 | } 26 | 27 | func (sc *serverConn) Serve() error { 28 | if err := handshake.HandshakeWithClient(sc.conn.rwc, sc.conn.rwc, &handshake.Config{ 29 | SkipHandshakeVerification: sc.conn.config.SkipHandshakeVerification, 30 | }); err != nil { 31 | return errors.Wrap(err, "Failed to handshake") 32 | } 33 | 34 | ctrlStream, err := sc.conn.streams.Create(ControlStreamID) 35 | if err != nil { 36 | return errors.Wrap(err, "Failed to create control stream") 37 | } 38 | ctrlStream.handler.ChangeState(streamStateServerNotConnected) 39 | 40 | sc.conn.streamer.controlStreamWriter = ctrlStream.Write 41 | 42 | if sc.conn.handler != nil { 43 | sc.conn.handler.OnServe(sc.conn) 44 | } 45 | 46 | return sc.conn.handleMessageLoop() 47 | } 48 | 49 | func (sc *serverConn) Close() error { 50 | return sc.conn.Close() 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-rtmp 2 | 3 | [![ci](https://github.com/yutopp/go-rtmp/workflows/ci/badge.svg)](https://github.com/yutopp/go-rtmp/actions?query=workflow%3Aci) 4 | [![codecov](https://codecov.io/gh/yutopp/go-rtmp/branch/master/graph/badge.svg?token=KXgQ1x8BQP)](https://codecov.io/gh/yutopp/go-rtmp) 5 | [![GoDoc](https://godoc.org/github.com/yutopp/go-rtmp?status.svg)](http://godoc.org/github.com/yutopp/go-rtmp) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/yutopp/go-rtmp)](https://goreportcard.com/report/github.com/yutopp/go-rtmp) 7 | [![license](https://img.shields.io/github/license/yutopp/go-rtmp.svg)](https://github.com/yutopp/go-rtmp/blob/master/LICENSE_1_0.txt) 8 | 9 | RTMP 1.0 server/client library written in Go. 10 | 11 | *Work in progress* 12 | 13 | ## Installation 14 | 15 | ``` 16 | go get github.com/yutopp/go-rtmp 17 | ``` 18 | 19 | See also [server_demo](https://github.com/yutopp/go-rtmp/tree/master/example/server_demo) and [client_demo](https://github.com/yutopp/go-rtmp/blob/master/example/client_demo/main.go). 20 | 21 | ## Documentation 22 | 23 | - [GoDoc](https://pkg.go.dev/github.com/yutopp/go-rtmp) 24 | - [REAL-TIME MESSAGING PROTOCOL (RTMP) SPECIFICATION](https://www.adobe.com/devnet/rtmp.html) 25 | 26 | 27 | ## NOTES 28 | 29 | ### How to limit bitrates or set timeouts 30 | 31 | - Please use [yutopp/go-iowrap](https://github.com/yutopp/go-iowrap). 32 | 33 | ## License 34 | 35 | [Boost Software License - Version 1.0](./LICENSE_1_0.txt) 36 | -------------------------------------------------------------------------------- /handler.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "io" 12 | 13 | "github.com/yutopp/go-rtmp/message" 14 | ) 15 | 16 | type Handler interface { 17 | OnServe(conn *Conn) 18 | OnConnect(timestamp uint32, cmd *message.NetConnectionConnect) error 19 | OnCreateStream(timestamp uint32, cmd *message.NetConnectionCreateStream) error 20 | OnReleaseStream(timestamp uint32, cmd *message.NetConnectionReleaseStream) error 21 | OnDeleteStream(timestamp uint32, cmd *message.NetStreamDeleteStream) error 22 | OnPublish(ctx *StreamContext, timestamp uint32, cmd *message.NetStreamPublish) error 23 | OnPlay(ctx *StreamContext, timestamp uint32, cmd *message.NetStreamPlay) error 24 | OnFCPublish(timestamp uint32, cmd *message.NetStreamFCPublish) error 25 | OnFCUnpublish(timestamp uint32, cmd *message.NetStreamFCUnpublish) error 26 | OnSetDataFrame(timestamp uint32, data *message.NetStreamSetDataFrame) error 27 | OnAudio(timestamp uint32, payload io.Reader) error 28 | OnVideo(timestamp uint32, payload io.Reader) error 29 | OnUnknownMessage(timestamp uint32, msg message.Message) error 30 | OnUnknownCommandMessage(timestamp uint32, cmd *message.CommandMessage) error 31 | OnUnknownDataMessage(timestamp uint32, data *message.DataMessage) error 32 | OnClose() 33 | } 34 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "crypto/tls" 12 | "net" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | func Dial(protocol, addr string, config *ConnConfig) (*ClientConn, error) { 18 | return DialWithDialer(&net.Dialer{}, protocol, addr, config) 19 | } 20 | 21 | func TLSDial(protocol, addr string, config *ConnConfig, tlsConfig *tls.Config) (*ClientConn, error) { 22 | return DialWithTLSDialer(&tls.Dialer{ 23 | NetDialer: &net.Dialer{}, 24 | Config: tlsConfig, 25 | }, protocol, addr, config) 26 | } 27 | 28 | func DialWithDialer(dialer *net.Dialer, protocol, addr string, config *ConnConfig) (*ClientConn, error) { 29 | if protocol != "rtmp" { 30 | return nil, errors.Errorf("Unknown protocol: %s", protocol) 31 | } 32 | 33 | rwc, err := dialer.Dial("tcp", addr) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return newClientConnWithSetup(rwc, config) 39 | } 40 | 41 | func DialWithTLSDialer(dialer *tls.Dialer, protocol, addr string, config *ConnConfig) (*ClientConn, error) { 42 | if protocol != "rtmps" { 43 | return nil, errors.Errorf("Unknown protocol: %s", protocol) 44 | } 45 | 46 | rwc, err := dialer.Dial("tcp", addr) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | return newClientConnWithSetup(rwc, config) 52 | } 53 | -------------------------------------------------------------------------------- /handshake/encoder.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package handshake 9 | 10 | import ( 11 | "encoding/binary" 12 | "io" 13 | ) 14 | 15 | type Encoder struct { 16 | w io.Writer 17 | } 18 | 19 | func NewEncoder(w io.Writer) *Encoder { 20 | return &Encoder{ 21 | w: w, 22 | } 23 | } 24 | 25 | func (e *Encoder) EncodeS0C0(h *S0C0) error { 26 | buf := [1]byte{byte(*h)} 27 | 28 | _, err := e.w.Write(buf[:]) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | return nil 34 | } 35 | 36 | func (e *Encoder) EncodeS1C1(h *S1C1) error { 37 | buf := [4]byte{} 38 | 39 | binary.BigEndian.PutUint32(buf[:], h.Time) 40 | if _, err := e.w.Write(buf[:]); err != nil { 41 | return err 42 | } 43 | 44 | if _, err := e.w.Write(h.Version[:]); err != nil { 45 | return err 46 | } 47 | 48 | if _, err := e.w.Write(h.Random[:]); err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (e *Encoder) EncodeS2C2(h *S2C2) error { 56 | buf := [4]byte{} 57 | 58 | binary.BigEndian.PutUint32(buf[:], h.Time) 59 | if _, err := e.w.Write(buf[:]); err != nil { 60 | return err 61 | } 62 | 63 | binary.BigEndian.PutUint32(buf[:], h.Time2) 64 | if _, err := e.w.Write(buf[:]); err != nil { 65 | return err 66 | } 67 | 68 | if _, err := e.w.Write(h.Random[:]); err != nil { 69 | return err 70 | } 71 | 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /client_control_not_connected_handler.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "github.com/yutopp/go-rtmp/internal" 12 | "github.com/yutopp/go-rtmp/message" 13 | ) 14 | 15 | var _ stateHandler = (*clientControlNotConnectedHandler)(nil) 16 | 17 | // clientControlNotConnectedHandler Handle control messages from a server in flow of connecting. 18 | // 19 | // transitions: 20 | // | "_result" -> controlStreamStateConnected 21 | // | _ -> self 22 | type clientControlNotConnectedHandler struct { 23 | sh *streamHandler 24 | } 25 | 26 | func (h *clientControlNotConnectedHandler) onMessage( 27 | chunkStreamID int, 28 | timestamp uint32, 29 | msg message.Message, 30 | ) error { 31 | return internal.ErrPassThroughMsg 32 | } 33 | 34 | func (h *clientControlNotConnectedHandler) onData( 35 | chunkStreamID int, 36 | timestamp uint32, 37 | dataMsg *message.DataMessage, 38 | body interface{}, 39 | ) error { 40 | return internal.ErrPassThroughMsg 41 | } 42 | 43 | func (h *clientControlNotConnectedHandler) onCommand( 44 | chunkStreamID int, 45 | timestamp uint32, 46 | cmdMsg *message.CommandMessage, 47 | body interface{}, 48 | ) error { 49 | l := h.sh.Logger() 50 | 51 | switch cmd := body.(type) { 52 | case *message.NetConnectionConnectResult: 53 | l.Info("ConnectResult") 54 | l.Infof("Result: Info = %+v, Props = %+v", cmd.Information, cmd.Properties) 55 | 56 | return nil 57 | 58 | default: 59 | return internal.ErrPassThroughMsg 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /handshake/decoder.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package handshake 9 | 10 | import ( 11 | "encoding/binary" 12 | "io" 13 | ) 14 | 15 | type Decoder struct { 16 | r io.Reader 17 | } 18 | 19 | func NewDecoder(r io.Reader) *Decoder { 20 | return &Decoder{ 21 | r: r, 22 | } 23 | } 24 | 25 | func (d *Decoder) DecodeS0C0(h *S0C0) error { 26 | buf := [1]byte{} 27 | 28 | if _, err := io.ReadAtLeast(d.r, buf[:], 1); err != nil { 29 | return err 30 | } 31 | *h = S0C0(buf[0]) 32 | 33 | return nil 34 | } 35 | 36 | func (d *Decoder) DecodeS1C1(h *S1C1) error { 37 | var buf [4]byte 38 | 39 | if _, err := io.ReadAtLeast(d.r, buf[:], len(buf)); err != nil { 40 | return err 41 | } 42 | h.Time = binary.BigEndian.Uint32(buf[:]) 43 | 44 | if _, err := io.ReadAtLeast(d.r, h.Version[:], len(h.Version)); err != nil { 45 | return err 46 | } 47 | 48 | if _, err := io.ReadAtLeast(d.r, h.Random[:], len(h.Random)); err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (d *Decoder) DecodeS2C2(h *S2C2) error { 56 | var buf [4]byte 57 | 58 | if _, err := io.ReadAtLeast(d.r, buf[:], len(buf)); err != nil { 59 | return err 60 | } 61 | h.Time = binary.BigEndian.Uint32(buf[:]) 62 | 63 | if _, err := io.ReadAtLeast(d.r, buf[:], len(buf)); err != nil { 64 | return err 65 | } 66 | h.Time2 = binary.BigEndian.Uint32(buf[:]) 67 | 68 | if _, err := io.ReadAtLeast(d.r, h.Random[:], len(h.Random)); err != nil { 69 | return err 70 | } 71 | 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /server_data_publish_handler.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "github.com/yutopp/go-rtmp/internal" 12 | "github.com/yutopp/go-rtmp/message" 13 | ) 14 | 15 | var _ stateHandler = (*serverDataPublishHandler)(nil) 16 | 17 | // serverDataPublishHandler Handle data messages from a publisher at server side. 18 | // 19 | // transitions: 20 | // | _ -> self 21 | type serverDataPublishHandler struct { 22 | sh *streamHandler 23 | } 24 | 25 | func (h *serverDataPublishHandler) onMessage( 26 | chunkStreamID int, 27 | timestamp uint32, 28 | msg message.Message, 29 | ) error { 30 | switch msg := msg.(type) { 31 | case *message.AudioMessage: 32 | return h.sh.stream.userHandler().OnAudio(timestamp, msg.Payload) 33 | 34 | case *message.VideoMessage: 35 | return h.sh.stream.userHandler().OnVideo(timestamp, msg.Payload) 36 | 37 | default: 38 | return internal.ErrPassThroughMsg 39 | } 40 | } 41 | 42 | func (h *serverDataPublishHandler) onData( 43 | chunkStreamID int, 44 | timestamp uint32, 45 | dataMsg *message.DataMessage, 46 | body interface{}, 47 | ) error { 48 | switch data := body.(type) { 49 | case *message.NetStreamSetDataFrame: 50 | return h.sh.stream.userHandler().OnSetDataFrame(timestamp, data) 51 | 52 | default: 53 | return internal.ErrPassThroughMsg 54 | } 55 | } 56 | 57 | func (h *serverDataPublishHandler) onCommand( 58 | chunkStreamID int, 59 | timestamp uint32, 60 | cmdMsg *message.CommandMessage, 61 | body interface{}, 62 | ) error { 63 | return internal.ErrPassThroughMsg 64 | } 65 | -------------------------------------------------------------------------------- /handler_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | 15 | "github.com/yutopp/go-rtmp/message" 16 | ) 17 | 18 | func TestHandlerCallback(t *testing.T) { 19 | b := &rwcMock{} 20 | 21 | closer := make(chan struct{}) 22 | handler := &testHandler{ 23 | t: t, 24 | closer: closer, 25 | } 26 | 27 | conn := newConn(b, &ConnConfig{ 28 | Handler: handler, 29 | 30 | SkipHandshakeVerification: true, 31 | 32 | ReaderBufferSize: 1234, 33 | WriterBufferSize: 1234, 34 | 35 | ControlState: StreamControlStateConfig{ 36 | DefaultChunkSize: 1234, 37 | MaxChunkSize: 1234, 38 | MaxChunkStreams: 1234, 39 | 40 | DefaultAckWindowSize: 1234, 41 | MaxAckWindowSize: 1234, 42 | 43 | DefaultBandwidthWindowSize: 1234, 44 | DefaultBandwidthLimitType: message.LimitTypeHard, 45 | MaxBandwidthWindowSize: 1234, 46 | 47 | MaxMessageStreams: 1234, 48 | MaxMessageSize: 1234, 49 | }, 50 | }) 51 | 52 | sconn := newServerConn(conn) 53 | go func() { 54 | <-closer 55 | sconn.Close() 56 | }() 57 | _ = sconn.Serve() 58 | } 59 | 60 | var _ Handler = (*testHandler)(nil) 61 | 62 | type testHandler struct { 63 | DefaultHandler 64 | t *testing.T 65 | closer chan struct{} 66 | } 67 | 68 | func (h *testHandler) OnServe(conn *Conn) { 69 | for _, s := range []*StreamControlState{conn.streamer.PeerState(), conn.streamer.SelfState()} { 70 | require.Equal(h.t, uint32(1234), s.ChunkSize()) 71 | require.Equal(h.t, uint32(1234), s.AckWindowSize()) 72 | require.Equal(h.t, int32(1234), s.BandwidthWindowSize()) 73 | require.Equal(h.t, message.LimitTypeHard, s.BandwidthLimitType()) 74 | } 75 | 76 | close(h.closer) // Finish testing 77 | } 78 | -------------------------------------------------------------------------------- /message/net_stream_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | type netStreamTestCase struct { 17 | Name string 18 | Box AMFConvertible 19 | 20 | Args []interface{} 21 | ExpectedMsg AMFConvertible 22 | 23 | FromErr error 24 | ToErr error 25 | } 26 | 27 | var netStreamTestCases = []netStreamTestCase{ 28 | { 29 | Name: "NetStreamPublish OK", 30 | Box: &NetStreamPublish{}, 31 | Args: []interface{}{nil, "aaa", "bbb"}, 32 | ExpectedMsg: &NetStreamPublish{ 33 | CommandObject: nil, 34 | PublishingName: "aaa", 35 | PublishingType: "bbb", 36 | }, 37 | }, 38 | { 39 | Name: "NetStreamReleaseStream OK", 40 | Box: &NetStreamReleaseStream{}, 41 | Args: []interface{}{nil, "theStream"}, // First argument is unknown 42 | ExpectedMsg: &NetStreamReleaseStream{ 43 | StreamName: "theStream", 44 | }, 45 | }, 46 | { 47 | Name: "NetStreamFCPublish OK", 48 | Box: &NetStreamFCPublish{}, 49 | Args: []interface{}{nil, "theStream"}, // First argument is unknown 50 | ExpectedMsg: &NetStreamFCPublish{ 51 | StreamName: "theStream", 52 | }, 53 | }, 54 | } 55 | 56 | func TestConvertNetStreamMessages(t *testing.T) { 57 | for _, tc := range netStreamTestCases { 58 | tc := tc // capture 59 | 60 | t.Run(tc.Name, func(t *testing.T) { 61 | t.Parallel() 62 | 63 | // Make a message from args 64 | err := tc.Box.FromArgs(tc.Args...) 65 | require.Equal(t, tc.FromErr, err) 66 | 67 | if err != nil { 68 | return 69 | } 70 | require.Equal(t, tc.ExpectedMsg, tc.Box) // Message <- Args0 71 | 72 | // Make args from message 73 | args, err := tc.Box.ToArgs(EncodingTypeAMF0) // TODO: fix interface... 74 | require.Equal(t, tc.ToErr, err) 75 | 76 | if err != nil { 77 | return 78 | } 79 | require.Equal(t, tc.Args, args) // Args0 <- Message 80 | }) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /response_preset.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "github.com/yutopp/go-rtmp/message" 12 | ) 13 | 14 | // ResponsePreset is an interface to provider server info. 15 | // Users of go-rtmp can obfuscate this information by modifying RPreset field of ConnConfig. 16 | type ResponsePreset interface { 17 | GetServerConnectResultProperties() message.NetConnectionConnectResultProperties 18 | GetServerConnectResultData() map[string]interface{} 19 | } 20 | 21 | // DefaultResponsePreset gives a default ServerInfo. 22 | type DefaultResponsePreset struct { 23 | ServerConnectResultProperties message.NetConnectionConnectResultProperties 24 | ServerConnectResultData map[string]interface{} 25 | } 26 | 27 | // NewDefaultResponsePreset gives an instance of DefaultResponsePreset 28 | func NewDefaultResponsePreset() *DefaultResponsePreset { 29 | return &DefaultResponsePreset{ 30 | // Sent to clients as result when Connect message is received 31 | ServerConnectResultProperties: message.NetConnectionConnectResultProperties{ 32 | FMSVer: "GO-RTMP/0,0,0,0", // TODO: fix 33 | Capabilities: 31, // TODO: fix 34 | Mode: 1, // TODO: fix 35 | }, 36 | // Sent to clients as result when Connect message is received 37 | ServerConnectResultData: map[string]interface{}{ 38 | "type": "go-rtmp", 39 | "version": "master", // TODO: fix 40 | }, 41 | } 42 | } 43 | 44 | // GetServerConnectResultProperties returns ServerConnectResultProperties. 45 | func (r *DefaultResponsePreset) GetServerConnectResultProperties() message.NetConnectionConnectResultProperties { 46 | return r.ServerConnectResultProperties 47 | } 48 | 49 | // GetServerConnectResultData returns ServerConnectResultData. 50 | func (r *DefaultResponsePreset) GetServerConnectResultData() map[string]interface{} { 51 | return r.ServerConnectResultData 52 | } 53 | 54 | var defaultResponsePreset ResponsePreset = NewDefaultResponsePreset() 55 | -------------------------------------------------------------------------------- /message/message_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "bytes" 12 | "io" 13 | "io/ioutil" 14 | "testing" 15 | 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func assertEqualMessage(t *testing.T, expected, actual Message) { 20 | require.Equal(t, expected.TypeID(), actual.TypeID()) 21 | 22 | switch expected := expected.(type) { 23 | case *AudioMessage: 24 | actual, ok := actual.(*AudioMessage) 25 | require.True(t, ok) 26 | 27 | assertEqualPayload(t, expected.Payload, actual.Payload) 28 | 29 | case *VideoMessage: 30 | actual, ok := actual.(*VideoMessage) 31 | require.True(t, ok) 32 | 33 | assertEqualPayload(t, expected.Payload, actual.Payload) 34 | 35 | case *DataMessage: 36 | actual, ok := actual.(*DataMessage) 37 | require.True(t, ok) 38 | 39 | require.Equal(t, expected.Name, actual.Name) 40 | require.Equal(t, expected.Encoding, actual.Encoding) 41 | assertEqualPayload(t, expected.Body, actual.Body) 42 | 43 | case *CommandMessage: 44 | actual, ok := actual.(*CommandMessage) 45 | require.True(t, ok) 46 | 47 | require.Equal(t, expected.CommandName, actual.CommandName) 48 | require.Equal(t, expected.TransactionID, actual.TransactionID) 49 | require.Equal(t, expected.Encoding, actual.Encoding) 50 | assertEqualPayload(t, expected.Body, actual.Body) 51 | 52 | default: 53 | require.Equal(t, expected, actual) 54 | } 55 | } 56 | 57 | func assertEqualPayload(t *testing.T, expected, actual io.Reader) { 58 | expectedBin, err := ioutil.ReadAll(expected) 59 | require.Nil(t, err) 60 | switch p := expected.(type) { 61 | case *bytes.Reader: 62 | defer func() { 63 | _, _ = p.Seek(0, io.SeekStart) // Restore test case states 64 | }() 65 | default: 66 | t.FailNow() 67 | } 68 | require.NotZero(t, len(expectedBin)) 69 | 70 | actualBin, err := ioutil.ReadAll(actual) 71 | require.Nil(t, err) 72 | require.NotZero(t, len(actualBin)) 73 | 74 | require.Equal(t, expectedBin, actualBin) 75 | } 76 | -------------------------------------------------------------------------------- /transactions.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "bytes" 12 | "io" 13 | "sync" 14 | 15 | "github.com/pkg/errors" 16 | 17 | "github.com/yutopp/go-rtmp/message" 18 | ) 19 | 20 | type transaction struct { 21 | commandName string 22 | encoding message.EncodingType 23 | body *bytes.Buffer 24 | lastErr error 25 | doneCh chan struct{} 26 | } 27 | 28 | func (t *transaction) Reply(commandName string, encoding message.EncodingType, body io.Reader) { 29 | t.commandName = commandName 30 | t.encoding = encoding 31 | t.body = new(bytes.Buffer) 32 | _, err := io.Copy(t.body, body) 33 | t.lastErr = err 34 | close(t.doneCh) 35 | } 36 | 37 | type transactions struct { 38 | transactions map[int64]*transaction 39 | m sync.RWMutex 40 | } 41 | 42 | func newTransactions() *transactions { 43 | return &transactions{ 44 | transactions: make(map[int64]*transaction), 45 | } 46 | } 47 | 48 | func (ts *transactions) Create(transactionID int64) (*transaction, error) { 49 | ts.m.Lock() 50 | defer ts.m.Unlock() 51 | 52 | _, ok := ts.transactions[transactionID] 53 | if ok { 54 | return nil, errors.Errorf("Transaction already exists: TransactionID = %d", transactionID) 55 | } 56 | 57 | ts.transactions[transactionID] = &transaction{ 58 | doneCh: make(chan struct{}), 59 | } 60 | 61 | return ts.transactions[transactionID], nil 62 | } 63 | 64 | func (ts *transactions) Delete(transactionID int64) error { 65 | ts.m.Lock() 66 | defer ts.m.Unlock() 67 | 68 | _, ok := ts.transactions[transactionID] 69 | if !ok { 70 | return errors.Errorf("Transaction not exists: TransactionID = %d", transactionID) 71 | } 72 | 73 | delete(ts.transactions, transactionID) 74 | 75 | return nil 76 | } 77 | 78 | func (ts *transactions) At(transactionID int64) (*transaction, error) { 79 | t, ok := ts.transactions[transactionID] 80 | if !ok { 81 | return nil, errors.Errorf("Transaction is not found: TransactionID = %d", transactionID) 82 | } 83 | 84 | return t, nil 85 | } 86 | -------------------------------------------------------------------------------- /conn_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "bytes" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/require" 15 | 16 | "github.com/yutopp/go-rtmp/message" 17 | ) 18 | 19 | func TestConnConfig(t *testing.T) { 20 | b := &rwcMock{} 21 | 22 | conn := newConn(b, &ConnConfig{ 23 | SkipHandshakeVerification: true, 24 | 25 | ReaderBufferSize: 1234, 26 | WriterBufferSize: 1234, 27 | 28 | ControlState: StreamControlStateConfig{ 29 | DefaultChunkSize: 1234, 30 | MaxChunkSize: 1234, 31 | MaxChunkStreams: 1234, 32 | 33 | DefaultAckWindowSize: 1234, 34 | MaxAckWindowSize: 1234, 35 | 36 | DefaultBandwidthWindowSize: 1234, 37 | DefaultBandwidthLimitType: message.LimitTypeHard, 38 | MaxBandwidthWindowSize: 1234, 39 | 40 | MaxMessageStreams: 1234, 41 | MaxMessageSize: 1234, 42 | }, 43 | }) 44 | 45 | require.Equal(t, true, conn.config.SkipHandshakeVerification) 46 | 47 | require.Equal(t, 1234, conn.config.ReaderBufferSize) 48 | require.Equal(t, 1234, conn.config.WriterBufferSize) 49 | 50 | require.Equal(t, uint32(1234), conn.config.ControlState.DefaultChunkSize) 51 | require.Equal(t, uint32(1234), conn.config.ControlState.MaxChunkSize) 52 | require.Equal(t, 1234, conn.config.ControlState.MaxChunkStreams) 53 | 54 | require.Equal(t, int32(1234), conn.config.ControlState.DefaultAckWindowSize) 55 | require.Equal(t, int32(1234), conn.config.ControlState.MaxAckWindowSize) 56 | 57 | require.Equal(t, int32(1234), conn.config.ControlState.DefaultBandwidthWindowSize) 58 | require.Equal(t, message.LimitTypeHard, conn.config.ControlState.DefaultBandwidthLimitType) 59 | require.Equal(t, int32(1234), conn.config.ControlState.MaxBandwidthWindowSize) 60 | 61 | require.Equal(t, uint32(1234), conn.config.ControlState.MaxMessageSize) 62 | require.Equal(t, 1234, conn.config.ControlState.MaxMessageStreams) 63 | } 64 | 65 | type rwcMock struct { 66 | bytes.Buffer 67 | Closed bool 68 | } 69 | 70 | func (m *rwcMock) Close() error { 71 | m.Closed = true 72 | return nil 73 | } 74 | -------------------------------------------------------------------------------- /streams.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "sync" 12 | 13 | "github.com/pkg/errors" 14 | ) 15 | 16 | // ControlStreamID StreamID 0 is a control stream 17 | const ControlStreamID = 0 18 | 19 | type streams struct { 20 | streams map[uint32]*Stream 21 | m sync.Mutex 22 | 23 | conn *Conn 24 | } 25 | 26 | func newStreams(conn *Conn) *streams { 27 | return &streams{ 28 | streams: make(map[uint32]*Stream), 29 | 30 | conn: conn, 31 | } 32 | } 33 | 34 | func (ss *streams) Create(streamID uint32) (*Stream, error) { 35 | ss.m.Lock() 36 | defer ss.m.Unlock() 37 | 38 | _, ok := ss.streams[streamID] 39 | if ok { 40 | return nil, errors.Errorf("Stream already exists: StreamID = %d", streamID) 41 | } 42 | if len(ss.streams) >= ss.conn.config.ControlState.MaxMessageStreams { 43 | return nil, errors.Errorf( 44 | "Creating message streams limit exceeded: Limit = %d", 45 | ss.conn.config.ControlState.MaxMessageStreams, 46 | ) 47 | } 48 | 49 | ss.streams[streamID] = newStream(streamID, ss.conn) 50 | 51 | return ss.streams[streamID], nil 52 | } 53 | 54 | func (ss *streams) CreateIfAvailable() (*Stream, error) { 55 | for i := 0; i < ss.conn.config.ControlState.MaxMessageStreams; i++ { 56 | s, err := ss.Create(uint32(i)) 57 | if err != nil { 58 | continue 59 | } 60 | return s, nil 61 | } 62 | 63 | return nil, errors.Errorf( 64 | "Creating streams limit exceeded: Limit = %d", 65 | ss.conn.config.ControlState.MaxMessageStreams, 66 | ) 67 | } 68 | 69 | func (ss *streams) Delete(streamID uint32) error { 70 | ss.m.Lock() 71 | defer ss.m.Unlock() 72 | 73 | s, ok := ss.streams[streamID] 74 | if !ok { 75 | return errors.Errorf("Stream not exists: StreamID = %d", streamID) 76 | } 77 | 78 | delete(ss.streams, s.streamID) 79 | 80 | s.assumeClosed() 81 | 82 | return nil 83 | } 84 | 85 | func (ss *streams) At(streamID uint32) (*Stream, error) { 86 | stream, ok := ss.streams[streamID] 87 | if !ok { 88 | return nil, errors.Errorf("Stream is not found: StreamID = %d", streamID) 89 | } 90 | 91 | return stream, nil 92 | } 93 | -------------------------------------------------------------------------------- /message/user_control_event_common_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | type uceTestCase struct { 11 | Name string 12 | Value UserCtrlEvent 13 | Binary []byte 14 | } 15 | 16 | var uceTestCases = []uceTestCase{ 17 | { 18 | Name: "StreamBegin", 19 | Value: &UserCtrlEventStreamBegin{ 20 | StreamID: 1234, 21 | }, 22 | Binary: []byte{ 23 | // ID=0 24 | 0x00, 0x00, 25 | // StreamID=1234 26 | 0x00, 0x00, 0x04, 0xd2, 27 | }, 28 | }, 29 | { 30 | Name: "StreamEOF", 31 | Value: &UserCtrlEventStreamEOF{ 32 | StreamID: 1234, 33 | }, 34 | Binary: []byte{ 35 | // ID=1 36 | 0x00, 0x01, 37 | // StreamID=1234 38 | 0x00, 0x00, 0x04, 0xd2, 39 | }, 40 | }, 41 | { 42 | Name: "StreamDry", 43 | Value: &UserCtrlEventStreamDry{ 44 | StreamID: 1234, 45 | }, 46 | Binary: []byte{ 47 | // ID=2 48 | 0x00, 0x02, 49 | // StreamID=1234 50 | 0x00, 0x00, 0x04, 0xd2, 51 | }, 52 | }, 53 | { 54 | Name: "SetBufferLength", 55 | Value: &UserCtrlEventSetBufferLength{ 56 | StreamID: 1234, 57 | LengthMs: 5678, 58 | }, 59 | Binary: []byte{ 60 | // ID=3 61 | 0x00, 0x03, 62 | // StreamID=1234 63 | 0x00, 0x00, 0x04, 0xd2, 64 | // LengthMs=5678 65 | 0x00, 0x00, 0x16, 0x2e, 66 | }, 67 | }, 68 | { 69 | Name: "StreamIsRecorded", 70 | Value: &UserCtrlEventStreamIsRecorded{ 71 | StreamID: 1234, 72 | }, 73 | Binary: []byte{ 74 | // ID=4 75 | 0x00, 0x04, 76 | // StreamID=1234 77 | 0x00, 0x00, 0x04, 0xd2, 78 | }, 79 | }, 80 | { 81 | Name: "PingRequest", 82 | Value: &UserCtrlEventPingRequest{ 83 | Timestamp: 1234, 84 | }, 85 | Binary: []byte{ 86 | // ID=6 87 | 0x00, 0x06, 88 | // Timestamp=1234 89 | 0x00, 0x00, 0x04, 0xd2, 90 | }, 91 | }, 92 | { 93 | Name: "PingResponse", 94 | Value: &UserCtrlEventPingResponse{ 95 | Timestamp: 1234, 96 | }, 97 | Binary: []byte{ 98 | // ID=7 99 | 0x00, 0x07, 100 | // Timestamp=1234 101 | 0x00, 0x00, 0x04, 0xd2, 102 | }, 103 | }, 104 | } 105 | -------------------------------------------------------------------------------- /default_handler.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "io" 12 | 13 | "github.com/yutopp/go-rtmp/message" 14 | ) 15 | 16 | var _ Handler = (*DefaultHandler)(nil) 17 | 18 | type DefaultHandler struct { 19 | } 20 | 21 | func (h *DefaultHandler) OnServe(conn *Conn) { 22 | } 23 | 24 | func (h *DefaultHandler) OnConnect(timestamp uint32, cmd *message.NetConnectionConnect) error { 25 | return nil 26 | } 27 | 28 | func (h *DefaultHandler) OnCreateStream(timestamp uint32, cmd *message.NetConnectionCreateStream) error { 29 | return nil 30 | } 31 | 32 | func (h *DefaultHandler) OnReleaseStream(timestamp uint32, cmd *message.NetConnectionReleaseStream) error { 33 | return nil 34 | } 35 | 36 | func (h *DefaultHandler) OnDeleteStream(timestamp uint32, cmd *message.NetStreamDeleteStream) error { 37 | return nil 38 | } 39 | 40 | func (h *DefaultHandler) OnPublish(_ *StreamContext, timestamp uint32, cmd *message.NetStreamPublish) error { 41 | return nil 42 | } 43 | 44 | func (h *DefaultHandler) OnPlay(_ *StreamContext, timestamp uint32, cmd *message.NetStreamPlay) error { 45 | return nil 46 | } 47 | 48 | func (h *DefaultHandler) OnFCPublish(timestamp uint32, cmd *message.NetStreamFCPublish) error { 49 | return nil 50 | } 51 | 52 | func (h *DefaultHandler) OnFCUnpublish(timestamp uint32, cmd *message.NetStreamFCUnpublish) error { 53 | return nil 54 | } 55 | 56 | func (h *DefaultHandler) OnSetDataFrame(timestamp uint32, data *message.NetStreamSetDataFrame) error { 57 | return nil 58 | } 59 | 60 | func (h *DefaultHandler) OnAudio(timestamp uint32, payload io.Reader) error { 61 | return nil 62 | } 63 | 64 | func (h *DefaultHandler) OnVideo(timestamp uint32, payload io.Reader) error { 65 | return nil 66 | } 67 | 68 | func (h *DefaultHandler) OnUnknownMessage(timestamp uint32, msg message.Message) error { 69 | return nil 70 | } 71 | 72 | func (h *DefaultHandler) OnUnknownCommandMessage(timestamp uint32, cmd *message.CommandMessage) error { 73 | return nil 74 | } 75 | 76 | func (h *DefaultHandler) OnUnknownDataMessage(timestamp uint32, data *message.DataMessage) error { 77 | return nil 78 | } 79 | 80 | func (h *DefaultHandler) OnClose() { 81 | } 82 | -------------------------------------------------------------------------------- /example/server_relay_demo/callback.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | 7 | flvtag "github.com/yutopp/go-flv/tag" 8 | "github.com/yutopp/go-rtmp" 9 | rtmpmsg "github.com/yutopp/go-rtmp/message" 10 | ) 11 | 12 | func onEventCallback(conn *rtmp.Conn, streamID uint32) func(flv *flvtag.FlvTag) error { 13 | return func(flv *flvtag.FlvTag) error { 14 | buf := new(bytes.Buffer) 15 | 16 | switch flv.Data.(type) { 17 | case *flvtag.AudioData: 18 | d := flv.Data.(*flvtag.AudioData) 19 | 20 | // Consume flv payloads (d) 21 | if err := flvtag.EncodeAudioData(buf, d); err != nil { 22 | return err 23 | } 24 | 25 | // TODO: Fix these values 26 | ctx := context.Background() 27 | chunkStreamID := 5 28 | return conn.Write(ctx, chunkStreamID, flv.Timestamp, &rtmp.ChunkMessage{ 29 | StreamID: streamID, 30 | Message: &rtmpmsg.AudioMessage{ 31 | Payload: buf, 32 | }, 33 | }) 34 | 35 | case *flvtag.VideoData: 36 | d := flv.Data.(*flvtag.VideoData) 37 | 38 | // Consume flv payloads (d) 39 | if err := flvtag.EncodeVideoData(buf, d); err != nil { 40 | return err 41 | } 42 | 43 | // TODO: Fix these values 44 | ctx := context.Background() 45 | chunkStreamID := 6 46 | return conn.Write(ctx, chunkStreamID, flv.Timestamp, &rtmp.ChunkMessage{ 47 | StreamID: streamID, 48 | Message: &rtmpmsg.VideoMessage{ 49 | Payload: buf, 50 | }, 51 | }) 52 | 53 | case *flvtag.ScriptData: 54 | d := flv.Data.(*flvtag.ScriptData) 55 | 56 | // Consume flv payloads (d) 57 | if err := flvtag.EncodeScriptData(buf, d); err != nil { 58 | return err 59 | } 60 | 61 | // TODO: hide these implementation 62 | amdBuf := new(bytes.Buffer) 63 | amfEnc := rtmpmsg.NewAMFEncoder(amdBuf, rtmpmsg.EncodingTypeAMF0) 64 | if err := rtmpmsg.EncodeBodyAnyValues(amfEnc, &rtmpmsg.NetStreamSetDataFrame{ 65 | Payload: buf.Bytes(), 66 | }); err != nil { 67 | return err 68 | } 69 | 70 | // TODO: Fix these values 71 | ctx := context.Background() 72 | chunkStreamID := 8 73 | return conn.Write(ctx, chunkStreamID, flv.Timestamp, &rtmp.ChunkMessage{ 74 | StreamID: streamID, 75 | Message: &rtmpmsg.DataMessage{ 76 | Name: "@setDataFrame", // TODO: fix 77 | Encoding: rtmpmsg.EncodingTypeAMF0, 78 | Body: amdBuf, 79 | }, 80 | }) 81 | 82 | default: 83 | panic("unreachable") 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /stream_handler_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestStreamHandlerChangeState(t *testing.T) { 17 | rwc := &rwcMock{} 18 | c := newConn(rwc, nil) 19 | s := newStream(42, c) 20 | 21 | s.handler.ChangeState(streamStateUnknown) 22 | require.Equal(t, s.handler.state, streamStateUnknown) 23 | require.Equal(t, s.handler.handler, nil) 24 | 25 | s.handler.ChangeState(streamStateServerNotConnected) 26 | require.Equal(t, s.handler.state, streamStateServerNotConnected) 27 | require.Equal(t, s.handler.handler, &serverControlNotConnectedHandler{sh: s.handler}) 28 | 29 | s.handler.ChangeState(streamStateServerConnected) 30 | require.Equal(t, s.handler.state, streamStateServerConnected) 31 | require.Equal(t, s.handler.handler, &serverControlConnectedHandler{sh: s.handler}) 32 | 33 | s.handler.ChangeState(streamStateServerInactive) 34 | require.Equal(t, s.handler.state, streamStateServerInactive) 35 | require.Equal(t, s.handler.handler, &serverDataInactiveHandler{sh: s.handler}) 36 | 37 | s.handler.ChangeState(streamStateServerPublish) 38 | require.Equal(t, s.handler.state, streamStateServerPublish) 39 | require.Equal(t, s.handler.handler, &serverDataPublishHandler{sh: s.handler}) 40 | 41 | s.handler.ChangeState(streamStateServerPlay) 42 | require.Equal(t, s.handler.state, streamStateServerPlay) 43 | require.Equal(t, s.handler.handler, &serverDataPlayHandler{sh: s.handler}) 44 | 45 | s.handler.ChangeState(streamStateClientNotConnected) 46 | require.Equal(t, s.handler.state, streamStateClientNotConnected) 47 | require.Equal(t, s.handler.handler, &clientControlNotConnectedHandler{sh: s.handler}) 48 | } 49 | 50 | func TestStreamStateString(t *testing.T) { 51 | require.Equal(t, "", streamStateUnknown.String()) 52 | require.Equal(t, "NotConnected(Server)", streamStateServerNotConnected.String()) 53 | require.Equal(t, "Connected(Server)", streamStateServerConnected.String()) 54 | require.Equal(t, "Inactive(Server)", streamStateServerInactive.String()) 55 | require.Equal(t, "Publish(Server)", streamStateServerPublish.String()) 56 | require.Equal(t, "Play(Server)", streamStateServerPlay.String()) 57 | require.Equal(t, "NotConnected(Client)", streamStateClientNotConnected.String()) 58 | require.Equal(t, "Connected(Client)", streamStateClientConnected.String()) 59 | } 60 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "io" 12 | "net" 13 | "sync" 14 | 15 | "github.com/pkg/errors" 16 | ) 17 | 18 | type Server struct { 19 | config *ServerConfig 20 | 21 | listener net.Listener 22 | mu sync.Mutex 23 | doneCh chan struct{} 24 | } 25 | 26 | type ServerConfig struct { 27 | OnConnect func(net.Conn) (io.ReadWriteCloser, *ConnConfig) 28 | } 29 | 30 | func NewServer(config *ServerConfig) *Server { 31 | return &Server{ 32 | config: config, 33 | } 34 | } 35 | 36 | func (srv *Server) Serve(l net.Listener) error { 37 | if err := srv.registerListener(l); err != nil { 38 | return errors.Wrap(err, "Already served") 39 | } 40 | 41 | defer l.Close() 42 | 43 | for { 44 | rwc, err := l.Accept() 45 | if err != nil { 46 | select { 47 | case <-srv.getDoneCh(): // closed 48 | return ErrClosed 49 | 50 | default: // do nothing 51 | } 52 | 53 | continue 54 | } 55 | 56 | go srv.handleConn(rwc) 57 | } 58 | } 59 | 60 | func (srv *Server) Close() error { 61 | srv.mu.Lock() 62 | defer srv.mu.Unlock() 63 | 64 | doneCh := srv.getDoneChLocked() 65 | select { 66 | case <-doneCh: // already closed 67 | return nil 68 | default: 69 | close(doneCh) 70 | } 71 | 72 | if srv.listener == nil { 73 | return nil 74 | } 75 | 76 | return srv.listener.Close() 77 | } 78 | 79 | func (srv *Server) registerListener(l net.Listener) error { 80 | srv.mu.Lock() 81 | defer srv.mu.Unlock() 82 | 83 | if srv.listener != nil { 84 | return errors.New("Listener is already registered") 85 | } 86 | 87 | srv.listener = l 88 | 89 | return nil 90 | } 91 | 92 | func (srv *Server) getDoneCh() chan struct{} { 93 | srv.mu.Lock() 94 | defer srv.mu.Unlock() 95 | 96 | return srv.getDoneChLocked() 97 | } 98 | 99 | func (srv *Server) getDoneChLocked() chan struct{} { 100 | if srv.doneCh == nil { 101 | srv.doneCh = make(chan struct{}) 102 | } 103 | 104 | return srv.doneCh 105 | } 106 | 107 | func (srv *Server) handleConn(conn net.Conn) { 108 | userConn, connConfig := srv.config.OnConnect(conn) 109 | 110 | c := newConn(userConn, connConfig) 111 | sc := &serverConn{ 112 | conn: c, 113 | } 114 | defer sc.Close() 115 | 116 | if err := sc.Serve(); err != nil { 117 | if err == io.EOF { 118 | c.logger.Infof("Server closed") 119 | return 120 | } 121 | c.logger.Errorf("Server closed by error: Err = %+v", err) 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/fortytw2/leaktest v1.2.0 h1:cj6GCiwJDH7l3tMHLjZDo0QqPtrXJiWSI9JgpeQKw+Q= 5 | github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= 6 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 7 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 8 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 9 | github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= 10 | github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= 11 | github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag= 12 | github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 13 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 14 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 15 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 16 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 17 | github.com/sirupsen/logrus v1.7.0 h1:ShrD1U9pZB12TX0cVy0DtePoCH97K8EtX+mg7ZARUtM= 18 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 19 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 20 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 21 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 22 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 23 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 24 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 25 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 26 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 27 | github.com/yutopp/go-amf0 v0.1.0 h1:a3UeBZG7nRF0zfvmPn2iAfNo1RGzUpHz1VyJD2oGrik= 28 | github.com/yutopp/go-amf0 v0.1.0/go.mod h1:QzDOBr9RV6sQh6E5GFEJROZbU0iQKijORBmprkb3FIk= 29 | github.com/yutopp/go-flv v0.3.1 h1:4ILK6OgCJgUNm2WOjaucWM5lUHE0+sLNPdjq3L0Xtjk= 30 | github.com/yutopp/go-flv v0.3.1/go.mod h1:pAlHPSVRMv5aCUKmGOS/dZn/ooTgnc09qOPmiUNMubs= 31 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= 33 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 35 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 36 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 37 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 38 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 39 | -------------------------------------------------------------------------------- /message/common_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "bytes" 12 | ) 13 | 14 | type testCase struct { 15 | Name string 16 | TypeID 17 | Value Message 18 | Binary []byte 19 | } 20 | 21 | var testCases = []testCase{ 22 | { 23 | Name: "SetChunkSize", 24 | TypeID: TypeIDSetChunkSize, 25 | Value: &SetChunkSize{ 26 | ChunkSize: 1024, 27 | }, 28 | Binary: []byte{ 29 | // ChunkSize 1024 (*31bit*, BigEndian) 30 | 0x00, 0x00, 0x04, 0x00, 31 | }, 32 | }, 33 | { 34 | Name: "AbortMessage", 35 | TypeID: TypeIDAbortMessage, 36 | Value: &AbortMessage{ 37 | ChunkStreamID: 1024, 38 | }, 39 | Binary: []byte{ 40 | // ChunkStreamID 1024 (32bit, BigEndian) 41 | 0x00, 0x00, 0x04, 0x00, 42 | }, 43 | }, 44 | { 45 | Name: "Ack", 46 | TypeID: TypeIDAck, 47 | Value: &Ack{ 48 | SequenceNumber: 1024, 49 | }, 50 | Binary: []byte{ 51 | // SequenceNumber 1024 (32bit, BigEndian) 52 | 0x00, 0x00, 0x04, 0x00, 53 | }, 54 | }, 55 | // TODO: UserCtrl 56 | { 57 | Name: "WinAckSize", 58 | TypeID: TypeIDWinAckSize, 59 | Value: &WinAckSize{ 60 | Size: 1024, 61 | }, 62 | Binary: []byte{ 63 | // Size 1024 (32bit, BigEndian) 64 | 0x00, 0x00, 0x04, 0x00, 65 | }, 66 | }, 67 | { 68 | Name: "SetPeerBandwidth", 69 | TypeID: TypeIDSetPeerBandwidth, 70 | Value: &SetPeerBandwidth{ 71 | Size: 1024, 72 | Limit: LimitTypeSoft, 73 | }, 74 | Binary: []byte{ 75 | // Size 1024 (32bit, BigEndian) 76 | 0x00, 0x00, 0x04, 0x00, 77 | // Limit Type 1(LimitTypeSoft, 8bit) 78 | 0x01, 79 | }, 80 | }, 81 | { 82 | Name: "AudioMessage", 83 | TypeID: TypeIDAudioMessage, 84 | Value: &AudioMessage{ 85 | Payload: bytes.NewReader([]byte("audio data")), 86 | }, 87 | Binary: []byte("audio data"), 88 | }, 89 | { 90 | Name: "VideoMessage", 91 | TypeID: TypeIDVideoMessage, 92 | Value: &VideoMessage{ 93 | Payload: bytes.NewReader([]byte("video data")), 94 | }, 95 | Binary: []byte("video data"), 96 | }, 97 | // TODO: DataMessageAMF3 98 | { 99 | Name: "DataMessageAMF0", 100 | TypeID: TypeIDDataMessageAMF0, 101 | Value: &DataMessage{ 102 | Name: "test", 103 | Encoding: EncodingTypeAMF0, 104 | Body: bytes.NewReader([]byte("test")), 105 | }, 106 | Binary: []byte{ 107 | // Name: AMF0 / string marker 108 | 0x02, 109 | // Name: AMF0 / string Length 4 110 | 0x00, 0x04, 111 | // Name: AMF0 / "test" string 112 | 0x74, 0x65, 0x73, 0x74, 113 | // RAW Binary: test 114 | 0x74, 0x65, 0x73, 0x74, 115 | }, 116 | }, 117 | // TODO: TypeIDSharedObjectMessageAMF3 118 | // TODO: TypeIDCommandMessageAMF3 119 | // TODO: TypeIDSharedObjectMessageAMF0 120 | { 121 | Name: "CommandMessageAMF0", 122 | TypeID: TypeIDCommandMessageAMF0, 123 | Value: &CommandMessage{ 124 | CommandName: "_result", 125 | TransactionID: 10, 126 | Encoding: EncodingTypeAMF0, 127 | Body: bytes.NewReader([]byte("test")), 128 | }, 129 | Binary: []byte{ 130 | // CommandName: AMF0 / string marker 131 | 0x02, 132 | // CommandName: AMF0 / string Length 133 | 0x00, 0x07, 134 | // CommandName: AMF0 / "_result" string 135 | 0x5f, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 136 | // TransactionID: AMF0 / number marker 137 | 0x00, 138 | // TransactionID: AMF0 / 10 number 139 | 0x40, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 140 | // RAW Binary: test 141 | 0x74, 0x65, 0x73, 0x74, 142 | }, 143 | }, 144 | // TODO: TypeIDAggregateMessage 145 | } 146 | -------------------------------------------------------------------------------- /example/server_relay_demo/pubsub.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "sync" 6 | 7 | flvtag "github.com/yutopp/go-flv/tag" 8 | ) 9 | 10 | type Pubsub struct { 11 | srv *RelayService 12 | name string 13 | 14 | pub *Pub 15 | subs []*Sub 16 | 17 | m sync.Mutex 18 | } 19 | 20 | func NewPubsub(srv *RelayService, name string) *Pubsub { 21 | return &Pubsub{ 22 | srv: srv, 23 | name: name, 24 | 25 | subs: make([]*Sub, 0), 26 | } 27 | } 28 | 29 | func (pb *Pubsub) Deregister() error { 30 | pb.m.Lock() 31 | defer pb.m.Unlock() 32 | 33 | for _, sub := range pb.subs { 34 | _ = sub.Close() 35 | } 36 | 37 | return pb.srv.RemovePubsub(pb.name) 38 | } 39 | 40 | func (pb *Pubsub) Pub() *Pub { 41 | pub := &Pub{ 42 | pb: pb, 43 | } 44 | 45 | pb.pub = pub 46 | 47 | return pub 48 | } 49 | 50 | func (pb *Pubsub) Sub() *Sub { 51 | pb.m.Lock() 52 | defer pb.m.Unlock() 53 | 54 | sub := &Sub{} 55 | 56 | // TODO: Implement more efficient resource management 57 | pb.subs = append(pb.subs, sub) 58 | 59 | return sub 60 | } 61 | 62 | type Pub struct { 63 | pb *Pubsub 64 | 65 | avcSeqHeader *flvtag.FlvTag 66 | lastKeyFrame *flvtag.FlvTag 67 | } 68 | 69 | // TODO: Should check codec types and so on. 70 | // In this example, checks only sequence headers and assume that AAC and AVC. 71 | func (p *Pub) Publish(flv *flvtag.FlvTag) error { 72 | switch flv.Data.(type) { 73 | case *flvtag.AudioData, *flvtag.ScriptData: 74 | for _, sub := range p.pb.subs { 75 | _ = sub.onEvent(cloneView(flv)) 76 | } 77 | 78 | case *flvtag.VideoData: 79 | d := flv.Data.(*flvtag.VideoData) 80 | if d.AVCPacketType == flvtag.AVCPacketTypeSequenceHeader { 81 | p.avcSeqHeader = flv 82 | } 83 | 84 | if d.FrameType == flvtag.FrameTypeKeyFrame { 85 | p.lastKeyFrame = flv 86 | } 87 | 88 | for _, sub := range p.pb.subs { 89 | if !sub.initialized { 90 | if p.avcSeqHeader != nil { 91 | _ = sub.onEvent(cloneView(p.avcSeqHeader)) 92 | } 93 | if p.lastKeyFrame != nil { 94 | _ = sub.onEvent(cloneView(p.lastKeyFrame)) 95 | } 96 | sub.initialized = true 97 | continue 98 | } 99 | 100 | _ = sub.onEvent(cloneView(flv)) 101 | } 102 | 103 | default: 104 | panic("unexpected") 105 | } 106 | 107 | return nil 108 | } 109 | 110 | func (p *Pub) Close() error { 111 | return p.pb.Deregister() 112 | } 113 | 114 | type Sub struct { 115 | initialized bool 116 | closed bool 117 | 118 | lastTimestamp uint32 119 | eventCallback func(*flvtag.FlvTag) error 120 | } 121 | 122 | func (s *Sub) onEvent(flv *flvtag.FlvTag) error { 123 | if s.closed { 124 | return nil 125 | } 126 | 127 | if flv.Timestamp != 0 && s.lastTimestamp == 0 { 128 | s.lastTimestamp = flv.Timestamp 129 | } 130 | flv.Timestamp -= s.lastTimestamp 131 | 132 | return s.eventCallback(flv) 133 | } 134 | 135 | func (s *Sub) Close() error { 136 | if s.closed { 137 | return nil 138 | } 139 | 140 | s.closed = true 141 | 142 | return nil 143 | } 144 | 145 | func cloneView(flv *flvtag.FlvTag) *flvtag.FlvTag { 146 | // Need to clone the view because Binary data will be consumed 147 | v := *flv 148 | 149 | switch flv.Data.(type) { 150 | case *flvtag.AudioData: 151 | dCloned := *v.Data.(*flvtag.AudioData) 152 | v.Data = &dCloned 153 | 154 | dCloned.Data = bytes.NewBuffer(dCloned.Data.(*bytes.Buffer).Bytes()) 155 | 156 | case *flvtag.VideoData: 157 | dCloned := *v.Data.(*flvtag.VideoData) 158 | v.Data = &dCloned 159 | 160 | dCloned.Data = bytes.NewBuffer(dCloned.Data.(*bytes.Buffer).Bytes()) 161 | 162 | case *flvtag.ScriptData: 163 | dCloned := *v.Data.(*flvtag.ScriptData) 164 | v.Data = &dCloned 165 | 166 | default: 167 | panic("unreachable") 168 | } 169 | 170 | return &v 171 | } 172 | -------------------------------------------------------------------------------- /message/user_control_event_encoder.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "encoding/binary" 12 | "io" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | type UserControlEventEncoder struct { 18 | w io.Writer 19 | } 20 | 21 | func NewUserControlEventEncoder(w io.Writer) *UserControlEventEncoder { 22 | return &UserControlEventEncoder{ 23 | w: w, 24 | } 25 | } 26 | 27 | func (enc *UserControlEventEncoder) Encode(msg UserCtrlEvent) error { 28 | switch msg := msg.(type) { 29 | case *UserCtrlEventStreamBegin: 30 | return enc.encodeStreamBegin(msg) 31 | case *UserCtrlEventStreamEOF: 32 | return enc.encodeStreamEOF(msg) 33 | case *UserCtrlEventStreamDry: 34 | return enc.encodeStreamDry(msg) 35 | case *UserCtrlEventSetBufferLength: 36 | return enc.encodeSetBufferLength(msg) 37 | case *UserCtrlEventStreamIsRecorded: 38 | return enc.encodeStreamIsRecorded(msg) 39 | case *UserCtrlEventPingRequest: 40 | return enc.encodePingRequest(msg) 41 | case *UserCtrlEventPingResponse: 42 | return enc.encodePingResponse(msg) 43 | default: 44 | return errors.Errorf("Unsupported type for UserCtrl: Type = %T", msg) 45 | } 46 | } 47 | 48 | func (enc *UserControlEventEncoder) encodeStreamBegin(msg *UserCtrlEventStreamBegin) error { 49 | buf := make([]byte, 2+4) 50 | binary.BigEndian.PutUint16(buf[0:2], 0) // [0:2]: ID=0 51 | binary.BigEndian.PutUint32(buf[2:], msg.StreamID) // [2:6] 52 | 53 | _, err := enc.w.Write(buf) 54 | 55 | return err 56 | } 57 | 58 | func (enc *UserControlEventEncoder) encodeStreamEOF(msg *UserCtrlEventStreamEOF) error { 59 | buf := make([]byte, 2+4) 60 | binary.BigEndian.PutUint16(buf[0:2], 1) // [0:2]: ID=1 61 | binary.BigEndian.PutUint32(buf[2:], msg.StreamID) // [2:6] 62 | 63 | _, err := enc.w.Write(buf) 64 | 65 | return err 66 | } 67 | 68 | func (enc *UserControlEventEncoder) encodeStreamDry(msg *UserCtrlEventStreamDry) error { 69 | buf := make([]byte, 2+4) 70 | binary.BigEndian.PutUint16(buf[0:2], 2) // [0:2]: ID=2 71 | binary.BigEndian.PutUint32(buf[2:], msg.StreamID) // [2:6] 72 | 73 | _, err := enc.w.Write(buf) 74 | 75 | return err 76 | } 77 | 78 | func (enc *UserControlEventEncoder) encodeSetBufferLength(msg *UserCtrlEventSetBufferLength) error { 79 | buf := make([]byte, 2+4+4) 80 | binary.BigEndian.PutUint16(buf[0:2], 3) // [0:2]: ID=e 81 | binary.BigEndian.PutUint32(buf[2:6], msg.StreamID) // [2:6] 82 | binary.BigEndian.PutUint32(buf[6:10], msg.LengthMs) // [6:10] 83 | 84 | _, err := enc.w.Write(buf) 85 | 86 | return err 87 | } 88 | 89 | func (enc *UserControlEventEncoder) encodeStreamIsRecorded(msg *UserCtrlEventStreamIsRecorded) error { 90 | buf := make([]byte, 2+4) 91 | binary.BigEndian.PutUint16(buf[0:2], 4) // [0:2]: ID=4 92 | binary.BigEndian.PutUint32(buf[2:], msg.StreamID) // [2:6] 93 | 94 | _, err := enc.w.Write(buf) 95 | 96 | return err 97 | } 98 | 99 | func (enc *UserControlEventEncoder) encodePingRequest(msg *UserCtrlEventPingRequest) error { 100 | buf := make([]byte, 2+4) 101 | binary.BigEndian.PutUint16(buf[0:2], 6) // [0:2]: ID=6 102 | binary.BigEndian.PutUint32(buf[2:], msg.Timestamp) // [2:6] 103 | 104 | _, err := enc.w.Write(buf) 105 | 106 | return err 107 | } 108 | 109 | func (enc *UserControlEventEncoder) encodePingResponse(msg *UserCtrlEventPingResponse) error { 110 | buf := make([]byte, 2+4) 111 | binary.BigEndian.PutUint16(buf[0:2], 7) // [0:2]: ID=7 112 | binary.BigEndian.PutUint32(buf[2:], msg.Timestamp) // [2:6] 113 | 114 | _, err := enc.w.Write(buf) 115 | 116 | return err 117 | } 118 | -------------------------------------------------------------------------------- /client_conn.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "net" 12 | "sync" 13 | 14 | "github.com/pkg/errors" 15 | 16 | "github.com/yutopp/go-rtmp/handshake" 17 | "github.com/yutopp/go-rtmp/message" 18 | ) 19 | 20 | // ClientConn A wrapper of a connection. It prorives client-side specific features. 21 | type ClientConn struct { 22 | conn *Conn 23 | lastErr error 24 | m sync.RWMutex 25 | } 26 | 27 | func newClientConnWithSetup(c net.Conn, config *ConnConfig) (*ClientConn, error) { 28 | conn := newConn(c, config) 29 | 30 | if err := handshake.HandshakeWithServer(conn.rwc, conn.rwc, &handshake.Config{ 31 | SkipHandshakeVerification: conn.config.SkipHandshakeVerification, 32 | }); err != nil { 33 | _ = conn.Close() 34 | return nil, errors.Wrap(err, "Failed to handshake") 35 | } 36 | 37 | ctrlStream, err := conn.streams.Create(ControlStreamID) 38 | if err != nil { 39 | _ = conn.Close() 40 | return nil, errors.Wrap(err, "Failed to create control stream") 41 | } 42 | ctrlStream.handler.ChangeState(streamStateClientNotConnected) 43 | 44 | conn.streamer.controlStreamWriter = ctrlStream.Write 45 | 46 | cc := &ClientConn{ 47 | conn: conn, 48 | } 49 | go cc.startHandleMessageLoop() 50 | 51 | return cc, nil 52 | } 53 | 54 | func (cc *ClientConn) Close() error { 55 | return cc.conn.Close() 56 | } 57 | 58 | func (cc *ClientConn) LastError() error { 59 | cc.m.RLock() 60 | defer cc.m.RUnlock() 61 | 62 | return cc.lastErr 63 | } 64 | 65 | func (cc *ClientConn) Connect(body *message.NetConnectionConnect) error { 66 | if err := cc.controllable(); err != nil { 67 | return err 68 | } 69 | 70 | stream, err := cc.conn.streams.At(ControlStreamID) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | result, err := stream.Connect(body) 76 | if err != nil { 77 | return err // TODO: wrap an error 78 | } 79 | 80 | // TODO: check result 81 | _ = result 82 | 83 | return nil 84 | } 85 | 86 | func (cc *ClientConn) CreateStream(body *message.NetConnectionCreateStream, chunkSize uint32) (*Stream, error) { 87 | if err := cc.controllable(); err != nil { 88 | return nil, err 89 | } 90 | 91 | stream, err := cc.conn.streams.At(ControlStreamID) 92 | if err != nil { 93 | return nil, err 94 | } 95 | 96 | result, err := stream.CreateStream(body, chunkSize) 97 | if err != nil { 98 | return nil, err // TODO: wrap an error 99 | } 100 | 101 | // TODO: check result 102 | 103 | newStream, err := cc.conn.streams.Create(result.StreamID) 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | return newStream, nil 109 | } 110 | 111 | func (cc *ClientConn) DeleteStream(body *message.NetStreamDeleteStream) error { 112 | if err := cc.controllable(); err != nil { 113 | return err 114 | } 115 | 116 | ctrlStream, err := cc.conn.streams.At(ControlStreamID) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | // Check if stream id exists 122 | if _, err := cc.conn.streams.At(body.StreamID); err != nil { 123 | return err 124 | } 125 | 126 | if err := ctrlStream.DeleteStream(body); err != nil { 127 | return err 128 | } 129 | 130 | return cc.conn.streams.Delete(body.StreamID) 131 | } 132 | 133 | func (cc *ClientConn) startHandleMessageLoop() { 134 | if err := cc.conn.handleMessageLoop(); err != nil { 135 | cc.setLastError(err) 136 | } 137 | } 138 | 139 | func (cc *ClientConn) setLastError(err error) { 140 | cc.m.Lock() 141 | defer cc.m.Unlock() 142 | 143 | cc.lastErr = err 144 | } 145 | 146 | func (cc *ClientConn) controllable() error { 147 | err := cc.LastError() 148 | return errors.Wrap(err, "Client is in error state") 149 | } 150 | -------------------------------------------------------------------------------- /conn_state.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "math" 12 | 13 | "github.com/pkg/errors" 14 | "github.com/yutopp/go-rtmp/message" 15 | ) 16 | 17 | const DefaultChunkSize = 128 18 | const MaxChunkSize = 0xffffff // 5.4.1 19 | 20 | type StreamControlState struct { 21 | chunkSize uint32 22 | ackWindowSize int32 23 | bandwidthWindowSize int32 24 | bandwidthLimitType message.LimitType 25 | 26 | config *StreamControlStateConfig 27 | } 28 | 29 | type StreamControlStateConfig struct { 30 | DefaultChunkSize uint32 31 | MaxChunkSize uint32 32 | MaxChunkStreams int 33 | 34 | DefaultAckWindowSize int32 35 | MaxAckWindowSize int32 36 | 37 | DefaultBandwidthWindowSize int32 38 | DefaultBandwidthLimitType message.LimitType 39 | MaxBandwidthWindowSize int32 40 | 41 | MaxMessageSize uint32 42 | MaxMessageStreams int 43 | } 44 | 45 | func (cb *StreamControlStateConfig) normalize() *StreamControlStateConfig { 46 | c := StreamControlStateConfig(*cb) 47 | 48 | // chunks 49 | 50 | if c.DefaultChunkSize == 0 { 51 | c.DefaultChunkSize = DefaultChunkSize 52 | } 53 | 54 | if c.MaxChunkSize == 0 { 55 | c.MaxChunkSize = MaxChunkSize 56 | } 57 | 58 | if c.MaxChunkStreams == 0 { 59 | c.MaxChunkStreams = math.MaxUint32 60 | } 61 | 62 | // ack 63 | 64 | if c.DefaultAckWindowSize == 0 { 65 | c.DefaultAckWindowSize = math.MaxInt32 66 | } 67 | 68 | if c.MaxAckWindowSize == 0 { 69 | c.MaxAckWindowSize = math.MaxInt32 70 | } 71 | 72 | // bandwidth 73 | 74 | if c.DefaultBandwidthWindowSize == 0 { 75 | c.DefaultBandwidthWindowSize = math.MaxInt32 76 | } 77 | 78 | if c.MaxBandwidthWindowSize == 0 { 79 | c.MaxBandwidthWindowSize = math.MaxInt32 80 | } 81 | 82 | // message 83 | 84 | if c.MaxMessageStreams == 0 { 85 | c.MaxMessageStreams = math.MaxUint32 86 | } 87 | 88 | if c.MaxMessageSize == 0 { 89 | c.MaxMessageSize = MaxChunkSize // as same as chunk size 90 | } 91 | 92 | return &c 93 | } 94 | 95 | var defaultStreamControlStateConfig = (&StreamControlStateConfig{}).normalize() 96 | 97 | func NewStreamControlState(config *StreamControlStateConfig) *StreamControlState { 98 | if config == nil { 99 | config = defaultStreamControlStateConfig 100 | } 101 | 102 | return &StreamControlState{ 103 | chunkSize: config.DefaultChunkSize, 104 | ackWindowSize: config.DefaultAckWindowSize, 105 | bandwidthWindowSize: config.DefaultBandwidthWindowSize, 106 | bandwidthLimitType: config.DefaultBandwidthLimitType, 107 | 108 | config: config, 109 | } 110 | } 111 | 112 | func (s *StreamControlState) ChunkSize() uint32 { 113 | return s.chunkSize 114 | } 115 | 116 | func (s *StreamControlState) SetChunkSize(chunkSize uint32) error { 117 | if chunkSize > MaxChunkSize { 118 | chunkSize = MaxChunkSize 119 | } 120 | 121 | if chunkSize > s.config.MaxChunkSize { 122 | return errors.Errorf("Exceeded configured max chunk size: Limit = %d, Value = %d", s.config.MaxChunkSize, chunkSize) 123 | } 124 | 125 | s.chunkSize = chunkSize 126 | 127 | return nil 128 | } 129 | 130 | func (s *StreamControlState) AckWindowSize() int32 { 131 | return s.ackWindowSize 132 | } 133 | 134 | func (s *StreamControlState) SetAckWindowSize(ackWindowSize int32) error { 135 | if ackWindowSize > s.config.MaxAckWindowSize { 136 | return errors.Errorf("Exceeded configured max ack window size: Limit = %d, Value = %d", s.config.MaxAckWindowSize, ackWindowSize) 137 | } 138 | 139 | s.ackWindowSize = ackWindowSize 140 | 141 | return nil 142 | } 143 | 144 | func (s *StreamControlState) BandwidthWindowSize() int32 { 145 | return s.bandwidthWindowSize 146 | } 147 | 148 | func (s *StreamControlState) BandwidthLimitType() message.LimitType { 149 | return s.bandwidthLimitType 150 | } 151 | -------------------------------------------------------------------------------- /example/server_relay_demo/handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "log" 7 | 8 | "github.com/pkg/errors" 9 | flvtag "github.com/yutopp/go-flv/tag" 10 | "github.com/yutopp/go-rtmp" 11 | rtmpmsg "github.com/yutopp/go-rtmp/message" 12 | ) 13 | 14 | var _ rtmp.Handler = (*Handler)(nil) 15 | 16 | // Handler An RTMP connection handler 17 | type Handler struct { 18 | rtmp.DefaultHandler 19 | relayService *RelayService 20 | 21 | // 22 | conn *rtmp.Conn 23 | 24 | // 25 | pub *Pub 26 | sub *Sub 27 | } 28 | 29 | func (h *Handler) OnServe(conn *rtmp.Conn) { 30 | h.conn = conn 31 | } 32 | 33 | func (h *Handler) OnConnect(timestamp uint32, cmd *rtmpmsg.NetConnectionConnect) error { 34 | log.Printf("OnConnect: %#v", cmd) 35 | 36 | // TODO: check app name to distinguish stream names per apps 37 | // cmd.Command.App 38 | 39 | return nil 40 | } 41 | 42 | func (h *Handler) OnCreateStream(timestamp uint32, cmd *rtmpmsg.NetConnectionCreateStream) error { 43 | log.Printf("OnCreateStream: %#v", cmd) 44 | return nil 45 | } 46 | 47 | func (h *Handler) OnPublish(_ *rtmp.StreamContext, timestamp uint32, cmd *rtmpmsg.NetStreamPublish) error { 48 | log.Printf("OnPublish: %#v", cmd) 49 | 50 | if h.sub != nil { 51 | return errors.New("Cannot publish to this stream") 52 | } 53 | 54 | // (example) Reject a connection when PublishingName is empty 55 | if cmd.PublishingName == "" { 56 | return errors.New("PublishingName is empty") 57 | } 58 | 59 | pubsub, err := h.relayService.NewPubsub(cmd.PublishingName) 60 | if err != nil { 61 | return errors.Wrap(err, "Failed to create pubsub") 62 | } 63 | 64 | pub := pubsub.Pub() 65 | 66 | h.pub = pub 67 | 68 | return nil 69 | } 70 | 71 | func (h *Handler) OnPlay(ctx *rtmp.StreamContext, timestamp uint32, cmd *rtmpmsg.NetStreamPlay) error { 72 | if h.sub != nil { 73 | return errors.New("Cannot play on this stream") 74 | } 75 | 76 | pubsub, err := h.relayService.GetPubsub(cmd.StreamName) 77 | if err != nil { 78 | return errors.Wrap(err, "Failed to get pubsub") 79 | } 80 | 81 | sub := pubsub.Sub() 82 | sub.eventCallback = onEventCallback(h.conn, ctx.StreamID) 83 | 84 | h.sub = sub 85 | 86 | return nil 87 | } 88 | 89 | func (h *Handler) OnSetDataFrame(timestamp uint32, data *rtmpmsg.NetStreamSetDataFrame) error { 90 | r := bytes.NewReader(data.Payload) 91 | 92 | var script flvtag.ScriptData 93 | if err := flvtag.DecodeScriptData(r, &script); err != nil { 94 | log.Printf("Failed to decode script data: Err = %+v", err) 95 | return nil // ignore 96 | } 97 | 98 | log.Printf("SetDataFrame: Script = %#v", script) 99 | 100 | _ = h.pub.Publish(&flvtag.FlvTag{ 101 | TagType: flvtag.TagTypeScriptData, 102 | Timestamp: timestamp, 103 | Data: &script, 104 | }) 105 | 106 | return nil 107 | } 108 | 109 | func (h *Handler) OnAudio(timestamp uint32, payload io.Reader) error { 110 | var audio flvtag.AudioData 111 | if err := flvtag.DecodeAudioData(payload, &audio); err != nil { 112 | return err 113 | } 114 | 115 | flvBody := new(bytes.Buffer) 116 | if _, err := io.Copy(flvBody, audio.Data); err != nil { 117 | return err 118 | } 119 | audio.Data = flvBody 120 | 121 | _ = h.pub.Publish(&flvtag.FlvTag{ 122 | TagType: flvtag.TagTypeAudio, 123 | Timestamp: timestamp, 124 | Data: &audio, 125 | }) 126 | 127 | return nil 128 | } 129 | 130 | func (h *Handler) OnVideo(timestamp uint32, payload io.Reader) error { 131 | var video flvtag.VideoData 132 | if err := flvtag.DecodeVideoData(payload, &video); err != nil { 133 | return err 134 | } 135 | 136 | // Need deep copy because payload will be recycled 137 | flvBody := new(bytes.Buffer) 138 | if _, err := io.Copy(flvBody, video.Data); err != nil { 139 | return err 140 | } 141 | video.Data = flvBody 142 | 143 | _ = h.pub.Publish(&flvtag.FlvTag{ 144 | TagType: flvtag.TagTypeVideo, 145 | Timestamp: timestamp, 146 | Data: &video, 147 | }) 148 | 149 | return nil 150 | } 151 | 152 | func (h *Handler) OnClose() { 153 | log.Printf("OnClose") 154 | 155 | if h.pub != nil { 156 | _ = h.pub.Close() 157 | } 158 | 159 | if h.sub != nil { 160 | _ = h.sub.Close() 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /handshake/handshake.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package handshake 9 | 10 | import ( 11 | "bytes" 12 | "crypto/rand" 13 | "io" 14 | "time" 15 | 16 | "github.com/pkg/errors" 17 | ) 18 | 19 | type S0C0 byte // RTMP Version 20 | 21 | type S1C1 struct { 22 | Time uint32 23 | Version [4]byte 24 | Random [1528]byte 25 | } 26 | 27 | type S2C2 struct { 28 | Time uint32 29 | Time2 uint32 30 | Random [1528]byte 31 | } 32 | 33 | var RTMPVersion = 3 34 | 35 | var Version = [4]byte{0, 0, 0, 0} // TODO: fix 36 | 37 | var timeNow = time.Now // For mock 38 | 39 | type Config struct { 40 | SkipHandshakeVerification bool 41 | } 42 | 43 | func HandshakeWithClient(r io.Reader, w io.Writer, config *Config) error { 44 | d := NewDecoder(r) 45 | e := NewEncoder(w) 46 | 47 | // Recv C0 48 | var c0 S0C0 49 | if err := d.DecodeS0C0(&c0); err != nil { 50 | return err 51 | } 52 | 53 | // TODO: check c0 RTMP version 54 | 55 | // Send S0 56 | s0 := S0C0(RTMPVersion) 57 | if err := e.EncodeS0C0(&s0); err != nil { 58 | return err 59 | } 60 | 61 | // Send S1 62 | s1 := S1C1{ 63 | Time: uint32(timeNow().UnixNano() / int64(time.Millisecond)), 64 | } 65 | copy(s1.Version[:], Version[:]) 66 | if _, err := rand.Read(s1.Random[:]); err != nil { // Random Seq 67 | return err 68 | } 69 | if err := e.EncodeS1C1(&s1); err != nil { 70 | return err 71 | } 72 | 73 | // Recv C1 74 | var c1 S1C1 75 | if err := d.DecodeS1C1(&c1); err != nil { 76 | return err 77 | } 78 | 79 | // TODO: check c1 Client version. e.g. [9 0 124 2] 80 | 81 | // Send S2 82 | s2 := S2C2{ 83 | Time: c1.Time, 84 | Time2: uint32(timeNow().UnixNano() / int64(time.Millisecond)), 85 | } 86 | copy(s2.Random[:], c1.Random[:]) // echo c1 random 87 | if err := e.EncodeS2C2(&s2); err != nil { 88 | return err 89 | } 90 | 91 | // Recv C2 92 | var c2 S2C2 93 | if err := d.DecodeS2C2(&c2); err != nil { 94 | return err 95 | } 96 | 97 | if config.SkipHandshakeVerification { 98 | return nil 99 | } 100 | 101 | // Check random echo 102 | if !bytes.Equal(c2.Random[:], s1.Random[:]) { 103 | return errors.New("Random echo is not matched") 104 | } 105 | 106 | return nil 107 | } 108 | 109 | func HandshakeWithServer(r io.Reader, w io.Writer, config *Config) error { 110 | d := NewDecoder(r) 111 | e := NewEncoder(w) 112 | 113 | // Send C0 114 | c0 := S0C0(RTMPVersion) 115 | if err := e.EncodeS0C0(&c0); err != nil { 116 | return errors.Wrap(err, "Failed to encode c0") 117 | } 118 | 119 | // Send C1 120 | c1 := S1C1{ 121 | Time: uint32(timeNow().UnixNano() / int64(time.Millisecond)), 122 | } 123 | copy(c1.Version[:], Version[:]) 124 | if _, err := rand.Read(c1.Random[:]); err != nil { // Random Seq 125 | return err 126 | } 127 | if err := e.EncodeS1C1(&c1); err != nil { 128 | return errors.Wrap(err, "Failed to encode c1") 129 | } 130 | 131 | // Recv S0 132 | var s0 S0C0 133 | if err := d.DecodeS0C0(&s0); err != nil { 134 | return errors.Wrap(err, "Failed to decode s0") 135 | } 136 | 137 | // TODO: check s0 RTMP version 138 | 139 | // Recv S1 140 | var s1 S1C1 141 | if err := d.DecodeS1C1(&s1); err != nil { 142 | return errors.Wrap(err, "Failed to decode s1") 143 | } 144 | 145 | // TODO: check s1 Server version. e.g. [9 0 124 2] 146 | 147 | // Recv S2 148 | var s2 S2C2 149 | if err := d.DecodeS2C2(&s2); err != nil { 150 | return errors.Wrap(err, "Failed to decode s2") 151 | } 152 | 153 | // Send C2 154 | c2 := S2C2{ 155 | Time: c1.Time, 156 | Time2: uint32(timeNow().UnixNano() / int64(time.Millisecond)), 157 | } 158 | copy(c2.Random[:], s1.Random[:]) // echo s1 random 159 | if err := e.EncodeS2C2(&c2); err != nil { 160 | return errors.Wrap(err, "Failed to encode c2") 161 | } 162 | 163 | if config.SkipHandshakeVerification { 164 | return nil 165 | } 166 | 167 | // Check random echo 168 | if !bytes.Equal(s2.Random[:], c1.Random[:]) { 169 | return errors.New("Random echo is not matched") 170 | } 171 | 172 | return nil 173 | } 174 | -------------------------------------------------------------------------------- /message/message.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "io" 12 | ) 13 | 14 | type TypeID byte 15 | 16 | const ( 17 | TypeIDSetChunkSize TypeID = 1 18 | TypeIDAbortMessage TypeID = 2 19 | TypeIDAck TypeID = 3 20 | TypeIDUserCtrl TypeID = 4 21 | TypeIDWinAckSize TypeID = 5 22 | TypeIDSetPeerBandwidth TypeID = 6 23 | TypeIDAudioMessage TypeID = 8 24 | TypeIDVideoMessage TypeID = 9 25 | TypeIDDataMessageAMF3 TypeID = 15 26 | TypeIDSharedObjectMessageAMF3 TypeID = 16 27 | TypeIDCommandMessageAMF3 TypeID = 17 28 | TypeIDDataMessageAMF0 TypeID = 18 29 | TypeIDSharedObjectMessageAMF0 TypeID = 19 30 | TypeIDCommandMessageAMF0 TypeID = 20 31 | TypeIDAggregateMessage TypeID = 22 32 | ) 33 | 34 | // Message 35 | type Message interface { 36 | TypeID() TypeID 37 | } 38 | 39 | // SetChunkSize (1) 40 | type SetChunkSize struct { 41 | ChunkSize uint32 42 | } 43 | 44 | func (m *SetChunkSize) TypeID() TypeID { 45 | return TypeIDSetChunkSize 46 | } 47 | 48 | // AbortMessage (2) 49 | type AbortMessage struct { 50 | ChunkStreamID uint32 51 | } 52 | 53 | func (m *AbortMessage) TypeID() TypeID { 54 | return TypeIDAbortMessage 55 | } 56 | 57 | // Ack (3) 58 | type Ack struct { 59 | SequenceNumber uint32 60 | } 61 | 62 | func (m *Ack) TypeID() TypeID { 63 | return TypeIDAck 64 | } 65 | 66 | // UserCtrl (4) 67 | type UserCtrl struct { 68 | Event UserCtrlEvent 69 | } 70 | 71 | func (m *UserCtrl) TypeID() TypeID { 72 | return TypeIDUserCtrl 73 | } 74 | 75 | // WinAckSize (5) 76 | type WinAckSize struct { 77 | Size int32 78 | } 79 | 80 | func (m *WinAckSize) TypeID() TypeID { 81 | return TypeIDWinAckSize 82 | } 83 | 84 | // SetPeerBandwidth (6) 85 | type LimitType uint8 86 | 87 | const ( 88 | LimitTypeHard LimitType = iota 89 | LimitTypeSoft 90 | LimitTypeDynamic 91 | ) 92 | 93 | type SetPeerBandwidth struct { 94 | Size int32 95 | Limit LimitType 96 | } 97 | 98 | func (m *SetPeerBandwidth) TypeID() TypeID { 99 | return TypeIDSetPeerBandwidth 100 | } 101 | 102 | // AudioMessage(8) 103 | type AudioMessage struct { 104 | Payload io.Reader 105 | } 106 | 107 | func (m *AudioMessage) TypeID() TypeID { 108 | return TypeIDAudioMessage 109 | } 110 | 111 | // VideoMessage(9) 112 | type VideoMessage struct { 113 | Payload io.Reader 114 | } 115 | 116 | func (m *VideoMessage) TypeID() TypeID { 117 | return TypeIDVideoMessage 118 | } 119 | 120 | // DataMessage (15, 18) 121 | type DataMessage struct { 122 | Name string 123 | Encoding EncodingType 124 | Body io.Reader 125 | } 126 | 127 | func (m *DataMessage) TypeID() TypeID { 128 | switch m.Encoding { 129 | case EncodingTypeAMF3: 130 | return TypeIDDataMessageAMF3 131 | case EncodingTypeAMF0: 132 | return TypeIDDataMessageAMF0 133 | default: 134 | panic("Unreachable") 135 | } 136 | } 137 | 138 | // SharedObjectMessage (16, 19) 139 | type SharedObjectMessage struct { 140 | } 141 | 142 | type SharedObjectMessageAMF3 struct { 143 | SharedObjectMessage 144 | } 145 | 146 | func (m *SharedObjectMessageAMF3) TypeID() TypeID { 147 | return TypeIDSharedObjectMessageAMF3 148 | } 149 | 150 | type SharedObjectMessageAMF0 struct { 151 | SharedObjectMessage 152 | } 153 | 154 | func (m *SharedObjectMessageAMF0) TypeID() TypeID { 155 | return TypeIDSharedObjectMessageAMF0 156 | } 157 | 158 | // CommandMessage (17, 20) 159 | type CommandMessage struct { 160 | CommandName string 161 | TransactionID int64 162 | Encoding EncodingType 163 | Body io.Reader 164 | } 165 | 166 | func (m *CommandMessage) TypeID() TypeID { 167 | switch m.Encoding { 168 | case EncodingTypeAMF3: 169 | return TypeIDCommandMessageAMF3 170 | case EncodingTypeAMF0: 171 | return TypeIDCommandMessageAMF0 172 | default: 173 | panic("Unreachable") 174 | } 175 | } 176 | 177 | // AggregateMessage (22) 178 | type AggregateMessage struct { 179 | } 180 | 181 | func (m *AggregateMessage) TypeID() TypeID { 182 | return TypeIDAggregateMessage 183 | } 184 | -------------------------------------------------------------------------------- /message/user_control_event_decoder.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "encoding/binary" 12 | "io" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | type UserControlEventDecoder struct { 18 | r io.Reader 19 | } 20 | 21 | func NewUserControlEventDecoder(r io.Reader) *UserControlEventDecoder { 22 | return &UserControlEventDecoder{ 23 | r: r, 24 | } 25 | } 26 | 27 | func (dec *UserControlEventDecoder) Decode(msg *UserCtrlEvent) error { 28 | buf := make([]byte, 2) 29 | if _, err := io.ReadAtLeast(dec.r, buf, 2); err != nil { 30 | return err 31 | } 32 | 33 | eventType := binary.BigEndian.Uint16(buf) 34 | switch eventType { 35 | case 0: // UserCtrlEventStreamBegin 36 | return dec.decodeStreamBegin(msg) 37 | case 1: // UserCtrlEventStreamEOF 38 | return dec.decodeStreamEOF(msg) 39 | case 2: // UserCtrlEventStreamDry 40 | return dec.decodeStreamDry(msg) 41 | case 3: // UserCtrlEventSetBufferLength 42 | return dec.decodeSetBufferLength(msg) 43 | case 4: // UserCtrlEventStreamIsRecorded 44 | return dec.decodeStreamIsRecorded(msg) 45 | case 6: // UserCtrlEventPingRequest 46 | return dec.decodePingRequest(msg) 47 | case 7: // UserCtrlEventPingResponse 48 | return dec.decodePingResponse(msg) 49 | default: 50 | return errors.Errorf("Unsupported type for UserCtrl: TypeID = %d", eventType) 51 | } 52 | } 53 | 54 | func (dec *UserControlEventDecoder) decodeStreamBegin(msg *UserCtrlEvent) error { 55 | buf := make([]byte, 4) 56 | if _, err := io.ReadAtLeast(dec.r, buf, 4); err != nil { 57 | return err 58 | } 59 | 60 | streamID := binary.BigEndian.Uint32(buf) 61 | 62 | *msg = &UserCtrlEventStreamBegin{ 63 | StreamID: streamID, 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func (dec *UserControlEventDecoder) decodeStreamEOF(msg *UserCtrlEvent) error { 70 | buf := make([]byte, 4) 71 | if _, err := io.ReadAtLeast(dec.r, buf, 4); err != nil { 72 | return err 73 | } 74 | 75 | streamID := binary.BigEndian.Uint32(buf) 76 | 77 | *msg = &UserCtrlEventStreamEOF{ 78 | StreamID: streamID, 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func (dec *UserControlEventDecoder) decodeStreamDry(msg *UserCtrlEvent) error { 85 | buf := make([]byte, 4) 86 | if _, err := io.ReadAtLeast(dec.r, buf, 4); err != nil { 87 | return err 88 | } 89 | 90 | streamID := binary.BigEndian.Uint32(buf) 91 | 92 | *msg = &UserCtrlEventStreamDry{ 93 | StreamID: streamID, 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (dec *UserControlEventDecoder) decodeSetBufferLength(msg *UserCtrlEvent) error { 100 | buf := make([]byte, 8) 101 | if _, err := io.ReadAtLeast(dec.r, buf, 8); err != nil { 102 | return err 103 | } 104 | 105 | streamID := binary.BigEndian.Uint32(buf[0:4]) 106 | lengthMs := binary.BigEndian.Uint32(buf[4:8]) 107 | 108 | *msg = &UserCtrlEventSetBufferLength{ 109 | StreamID: streamID, 110 | LengthMs: lengthMs, 111 | } 112 | 113 | return nil 114 | } 115 | 116 | func (dec *UserControlEventDecoder) decodeStreamIsRecorded(msg *UserCtrlEvent) error { 117 | buf := make([]byte, 4) 118 | if _, err := io.ReadAtLeast(dec.r, buf, 4); err != nil { 119 | return err 120 | } 121 | 122 | streamID := binary.BigEndian.Uint32(buf) 123 | 124 | *msg = &UserCtrlEventStreamIsRecorded{ 125 | StreamID: streamID, 126 | } 127 | 128 | return nil 129 | } 130 | 131 | func (dec *UserControlEventDecoder) decodePingRequest(msg *UserCtrlEvent) error { 132 | buf := make([]byte, 4) 133 | if _, err := io.ReadAtLeast(dec.r, buf, 4); err != nil { 134 | return err 135 | } 136 | 137 | timestamp := binary.BigEndian.Uint32(buf) 138 | 139 | *msg = &UserCtrlEventPingRequest{ 140 | Timestamp: timestamp, 141 | } 142 | 143 | return nil 144 | } 145 | 146 | func (dec *UserControlEventDecoder) decodePingResponse(msg *UserCtrlEvent) error { 147 | buf := make([]byte, 4) 148 | if _, err := io.ReadAtLeast(dec.r, buf, 4); err != nil { 149 | return err 150 | } 151 | 152 | timestamp := binary.BigEndian.Uint32(buf) 153 | 154 | *msg = &UserCtrlEventPingResponse{ 155 | Timestamp: timestamp, 156 | } 157 | 158 | return nil 159 | } 160 | -------------------------------------------------------------------------------- /server_data_inactive_handler.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "github.com/pkg/errors" 12 | 13 | "github.com/yutopp/go-rtmp/internal" 14 | "github.com/yutopp/go-rtmp/message" 15 | ) 16 | 17 | var _ stateHandler = (*serverDataInactiveHandler)(nil) 18 | 19 | // serverDataInactiveHandler Handle data messages from a non operated client at server side. 20 | // 21 | // transitions: 22 | // | "publish" -> serverDataPublishHandler 23 | // | "play" -> serverDataPlayHandler 24 | // | _ -> self 25 | type serverDataInactiveHandler struct { 26 | sh *streamHandler 27 | } 28 | 29 | func (h *serverDataInactiveHandler) onMessage( 30 | chunkStreamID int, 31 | timestamp uint32, 32 | msg message.Message, 33 | ) error { 34 | return internal.ErrPassThroughMsg 35 | } 36 | 37 | func (h *serverDataInactiveHandler) onData( 38 | chunkStreamID int, 39 | timestamp uint32, 40 | dataMsg *message.DataMessage, 41 | body interface{}, 42 | ) error { 43 | return internal.ErrPassThroughMsg 44 | } 45 | 46 | func (h *serverDataInactiveHandler) onCommand( 47 | chunkStreamID int, 48 | timestamp uint32, 49 | cmdMsg *message.CommandMessage, 50 | body interface{}, 51 | ) error { 52 | l := h.sh.Logger() 53 | 54 | switch cmd := body.(type) { 55 | case *message.NetStreamPublish: 56 | l.Infof("Publisher is comming: %#v", cmd) 57 | 58 | streamCtx := &StreamContext{ 59 | StreamID: h.sh.stream.streamID, 60 | } 61 | if err := h.sh.stream.userHandler().OnPublish(streamCtx, timestamp, cmd); err != nil { 62 | // TODO: Support message.NetStreamOnStatusCodePublishBadName 63 | result := h.newOnStatus(message.NetStreamOnStatusCodePublishFailed, "Publish failed.") 64 | 65 | l.Infof("Reject a Publish request: Response = %#v, Err = %+v", result, err) 66 | if err1 := h.sh.stream.NotifyStatus(chunkStreamID, timestamp, result); err1 != nil { 67 | return errors.Wrapf(err, "Failed to reply response: Err = %+v", err1) 68 | } 69 | 70 | return err 71 | } 72 | 73 | result := h.newOnStatus(message.NetStreamOnStatusCodePublishStart, "Publish succeeded.") 74 | if err := h.sh.stream.NotifyStatus(chunkStreamID, timestamp, result); err != nil { 75 | return err 76 | } 77 | l.Infof("Publisher accepted") 78 | 79 | h.sh.ChangeState(streamStateServerPublish) 80 | 81 | return nil 82 | 83 | case *message.NetStreamPlay: 84 | l.Infof("Player is comming: %#v", cmd) 85 | 86 | streamCtx := &StreamContext{ 87 | StreamID: h.sh.stream.streamID, 88 | } 89 | if err := h.sh.stream.userHandler().OnPlay(streamCtx, timestamp, cmd); err != nil { 90 | result := h.newOnStatus(message.NetStreamOnStatusCodePlayFailed, "Play failed.") 91 | 92 | l.Infof("Reject a Play request: Response = %#v, Err = %+v", result, err) 93 | if err1 := h.sh.stream.NotifyStatus(chunkStreamID, timestamp, result); err1 != nil { 94 | return errors.Wrapf(err, "Failed to reply response: Err = %+v", err1) 95 | } 96 | 97 | return err 98 | } 99 | 100 | result := h.newOnStatus(message.NetStreamOnStatusCodePlayStart, "Play succeeded.") 101 | if err := h.sh.stream.NotifyStatus(chunkStreamID, timestamp, result); err != nil { 102 | return err 103 | } 104 | l.Infof("Player accepted") 105 | 106 | h.sh.ChangeState(streamStateServerPlay) 107 | 108 | return nil 109 | 110 | default: 111 | return internal.ErrPassThroughMsg 112 | } 113 | } 114 | 115 | func (h *serverDataInactiveHandler) newOnStatus( 116 | code message.NetStreamOnStatusCode, 117 | description string, 118 | ) *message.NetStreamOnStatus { 119 | // https://helpx.adobe.com/adobe-media-server/ssaslr/netstream-class.html#netstream_onstatus 120 | level := message.NetStreamOnStatusLevelStatus 121 | switch code { 122 | case message.NetStreamOnStatusCodeConnectFailed: 123 | fallthrough 124 | case message.NetStreamOnStatusCodePlayFailed: 125 | fallthrough 126 | case message.NetStreamOnStatusCodePublishBadName, message.NetStreamOnStatusCodePublishFailed: 127 | level = message.NetStreamOnStatusLevelError 128 | } 129 | 130 | return &message.NetStreamOnStatus{ 131 | InfoObject: message.NetStreamOnStatusInfoObject{ 132 | Level: level, 133 | Code: code, 134 | Description: description, 135 | }, 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /example/server_demo/handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | 11 | "github.com/pkg/errors" 12 | "github.com/yutopp/go-flv" 13 | flvtag "github.com/yutopp/go-flv/tag" 14 | "github.com/yutopp/go-rtmp" 15 | rtmpmsg "github.com/yutopp/go-rtmp/message" 16 | ) 17 | 18 | var _ rtmp.Handler = (*Handler)(nil) 19 | 20 | // Handler An RTMP connection handler 21 | type Handler struct { 22 | rtmp.DefaultHandler 23 | flvFile *os.File 24 | flvEnc *flv.Encoder 25 | } 26 | 27 | func (h *Handler) OnServe(conn *rtmp.Conn) { 28 | } 29 | 30 | func (h *Handler) OnConnect(timestamp uint32, cmd *rtmpmsg.NetConnectionConnect) error { 31 | log.Printf("OnConnect: %#v", cmd) 32 | return nil 33 | } 34 | 35 | func (h *Handler) OnCreateStream(timestamp uint32, cmd *rtmpmsg.NetConnectionCreateStream) error { 36 | log.Printf("OnCreateStream: %#v", cmd) 37 | return nil 38 | } 39 | 40 | func (h *Handler) OnPublish(_ *rtmp.StreamContext, timestamp uint32, cmd *rtmpmsg.NetStreamPublish) error { 41 | log.Printf("OnPublish: %#v", cmd) 42 | 43 | // (example) Reject a connection when PublishingName is empty 44 | if cmd.PublishingName == "" { 45 | return errors.New("PublishingName is empty") 46 | } 47 | 48 | // Record streams as FLV! 49 | p := filepath.Join( 50 | os.TempDir(), 51 | filepath.Clean(filepath.Join("/", fmt.Sprintf("%s.flv", cmd.PublishingName))), 52 | ) 53 | f, err := os.OpenFile(p, os.O_CREATE|os.O_WRONLY, 0666) 54 | if err != nil { 55 | return errors.Wrap(err, "Failed to create flv file") 56 | } 57 | h.flvFile = f 58 | 59 | enc, err := flv.NewEncoder(f, flv.FlagsAudio|flv.FlagsVideo) 60 | if err != nil { 61 | _ = f.Close() 62 | return errors.Wrap(err, "Failed to create flv encoder") 63 | } 64 | h.flvEnc = enc 65 | 66 | return nil 67 | } 68 | 69 | func (h *Handler) OnSetDataFrame(timestamp uint32, data *rtmpmsg.NetStreamSetDataFrame) error { 70 | r := bytes.NewReader(data.Payload) 71 | 72 | var script flvtag.ScriptData 73 | if err := flvtag.DecodeScriptData(r, &script); err != nil { 74 | log.Printf("Failed to decode script data: Err = %+v", err) 75 | return nil // ignore 76 | } 77 | 78 | log.Printf("SetDataFrame: Script = %#v", script) 79 | 80 | if err := h.flvEnc.Encode(&flvtag.FlvTag{ 81 | TagType: flvtag.TagTypeScriptData, 82 | Timestamp: timestamp, 83 | Data: &script, 84 | }); err != nil { 85 | log.Printf("Failed to write script data: Err = %+v", err) 86 | } 87 | 88 | return nil 89 | } 90 | 91 | func (h *Handler) OnAudio(timestamp uint32, payload io.Reader) error { 92 | var audio flvtag.AudioData 93 | if err := flvtag.DecodeAudioData(payload, &audio); err != nil { 94 | return err 95 | } 96 | 97 | flvBody := new(bytes.Buffer) 98 | if _, err := io.Copy(flvBody, audio.Data); err != nil { 99 | return err 100 | } 101 | audio.Data = flvBody 102 | 103 | log.Printf("FLV Audio Data: Timestamp = %d, SoundFormat = %+v, SoundRate = %+v, SoundSize = %+v, SoundType = %+v, AACPacketType = %+v, Data length = %+v", 104 | timestamp, 105 | audio.SoundFormat, 106 | audio.SoundRate, 107 | audio.SoundSize, 108 | audio.SoundType, 109 | audio.AACPacketType, 110 | len(flvBody.Bytes()), 111 | ) 112 | 113 | if err := h.flvEnc.Encode(&flvtag.FlvTag{ 114 | TagType: flvtag.TagTypeAudio, 115 | Timestamp: timestamp, 116 | Data: &audio, 117 | }); err != nil { 118 | log.Printf("Failed to write audio: Err = %+v", err) 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func (h *Handler) OnVideo(timestamp uint32, payload io.Reader) error { 125 | var video flvtag.VideoData 126 | if err := flvtag.DecodeVideoData(payload, &video); err != nil { 127 | return err 128 | } 129 | 130 | flvBody := new(bytes.Buffer) 131 | if _, err := io.Copy(flvBody, video.Data); err != nil { 132 | return err 133 | } 134 | video.Data = flvBody 135 | 136 | log.Printf("FLV Video Data: Timestamp = %d, FrameType = %+v, CodecID = %+v, AVCPacketType = %+v, CT = %+v, Data length = %+v", 137 | timestamp, 138 | video.FrameType, 139 | video.CodecID, 140 | video.AVCPacketType, 141 | video.CompositionTime, 142 | len(flvBody.Bytes()), 143 | ) 144 | 145 | if err := h.flvEnc.Encode(&flvtag.FlvTag{ 146 | TagType: flvtag.TagTypeVideo, 147 | Timestamp: timestamp, 148 | Data: &video, 149 | }); err != nil { 150 | log.Printf("Failed to write video: Err = %+v", err) 151 | } 152 | 153 | return nil 154 | } 155 | 156 | func (h *Handler) OnClose() { 157 | log.Printf("OnClose") 158 | 159 | if h.flvFile != nil { 160 | _ = h.flvFile.Close() 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /server_control_not_connected_handler.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "github.com/pkg/errors" 12 | 13 | "github.com/yutopp/go-rtmp/internal" 14 | "github.com/yutopp/go-rtmp/message" 15 | ) 16 | 17 | var _ stateHandler = (*serverControlNotConnectedHandler)(nil) 18 | 19 | // serverControlNotConnectedHandler Handle control messages from a client which has not send connect at server side. 20 | // 21 | // transitions: 22 | // | "connect" -> controlStreamStateConnected 23 | // | _ -> self 24 | type serverControlNotConnectedHandler struct { 25 | sh *streamHandler 26 | } 27 | 28 | func (h *serverControlNotConnectedHandler) onMessage( 29 | chunkStreamID int, 30 | timestamp uint32, 31 | msg message.Message, 32 | ) error { 33 | return internal.ErrPassThroughMsg 34 | } 35 | 36 | func (h *serverControlNotConnectedHandler) onData( 37 | chunkStreamID int, 38 | timestamp uint32, 39 | dataMsg *message.DataMessage, 40 | body interface{}, 41 | ) error { 42 | return internal.ErrPassThroughMsg 43 | } 44 | 45 | func (h *serverControlNotConnectedHandler) onCommand( 46 | chunkStreamID int, 47 | timestamp uint32, 48 | cmdMsg *message.CommandMessage, 49 | body interface{}, 50 | ) (err error) { 51 | l := h.sh.Logger() 52 | 53 | switch cmd := body.(type) { 54 | case *message.NetConnectionConnect: 55 | l.Info("Connect") 56 | defer func() { 57 | if err != nil { 58 | result := h.newConnectErrorResult() 59 | 60 | l.Infof("Connect(Error): ResponseBody = %#v, Err = %+v", result, err) 61 | if err1 := h.sh.stream.ReplyConnect(chunkStreamID, timestamp, result); err1 != nil { 62 | err = errors.Wrapf(err, "Failed to reply response: Err = %+v", err1) 63 | } 64 | } 65 | }() 66 | 67 | if err := h.sh.stream.userHandler().OnConnect(timestamp, cmd); err != nil { 68 | return err 69 | } 70 | 71 | l.Infof("Set win ack size: Size = %+v", h.sh.stream.streamer().SelfState().AckWindowSize()) 72 | if err := h.sh.stream.WriteWinAckSize(ctrlMsgChunkStreamID, timestamp, &message.WinAckSize{ 73 | Size: h.sh.stream.streamer().SelfState().AckWindowSize(), 74 | }); err != nil { 75 | return err 76 | } 77 | 78 | l.Infof("Set peer bandwidth: Size = %+v, Limit = %+v", 79 | h.sh.stream.streamer().SelfState().BandwidthWindowSize(), 80 | h.sh.stream.streamer().SelfState().BandwidthLimitType(), 81 | ) 82 | if err := h.sh.stream.WriteSetPeerBandwidth(ctrlMsgChunkStreamID, timestamp, &message.SetPeerBandwidth{ 83 | Size: h.sh.stream.streamer().SelfState().BandwidthWindowSize(), 84 | Limit: h.sh.stream.streamer().SelfState().BandwidthLimitType(), 85 | }); err != nil { 86 | return err 87 | } 88 | 89 | l.Infof("Stream Begin: ID = %d", h.sh.stream.streamID) 90 | if err := h.sh.stream.WriteUserCtrl(ctrlMsgChunkStreamID, timestamp, &message.UserCtrl{ 91 | Event: &message.UserCtrlEventStreamBegin{ 92 | StreamID: h.sh.stream.streamID, 93 | }, 94 | }); err != nil { 95 | return err 96 | } 97 | 98 | result := h.newConnectSuccessResult() 99 | 100 | l.Infof("Connect: ResponseBody = %#v", result) 101 | if err := h.sh.stream.ReplyConnect(chunkStreamID, timestamp, result); err != nil { 102 | return err 103 | } 104 | l.Info("Connected") 105 | 106 | h.sh.ChangeState(streamStateServerConnected) 107 | 108 | return nil 109 | 110 | default: 111 | return internal.ErrPassThroughMsg 112 | } 113 | } 114 | 115 | func (h *serverControlNotConnectedHandler) newConnectSuccessResult() *message.NetConnectionConnectResult { 116 | rPreset := h.sh.stream.conn.config.RPreset 117 | if rPreset == nil { 118 | rPreset = defaultResponsePreset 119 | } 120 | return &message.NetConnectionConnectResult{ 121 | Properties: rPreset.GetServerConnectResultProperties(), 122 | Information: message.NetConnectionConnectResultInformation{ 123 | Level: "status", 124 | Code: message.NetConnectionConnectCodeSuccess, 125 | Description: "Connection succeeded.", 126 | Data: rPreset.GetServerConnectResultData(), 127 | }, 128 | } 129 | } 130 | 131 | func (h *serverControlNotConnectedHandler) newConnectErrorResult() *message.NetConnectionConnectResult { 132 | rPreset := h.sh.stream.conn.config.RPreset 133 | if rPreset == nil { 134 | rPreset = defaultResponsePreset 135 | } 136 | return &message.NetConnectionConnectResult{ 137 | Properties: rPreset.GetServerConnectResultProperties(), 138 | Information: message.NetConnectionConnectResultInformation{ 139 | Level: "error", 140 | Code: message.NetConnectionConnectCodeFailed, 141 | Description: "Connection failed.", 142 | Data: rPreset.GetServerConnectResultData(), 143 | }, 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /server_client_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "fmt" 12 | "io" 13 | "net" 14 | "testing" 15 | 16 | "github.com/sirupsen/logrus" 17 | "github.com/stretchr/testify/require" 18 | "github.com/yutopp/go-amf0" 19 | 20 | "github.com/yutopp/go-rtmp/message" 21 | ) 22 | 23 | const ( 24 | chunkSize = 128 25 | ) 26 | 27 | type serverCanAcceptConnectHandler struct { 28 | DefaultHandler 29 | } 30 | 31 | func TestServerCanAcceptConnect(t *testing.T) { 32 | config := &ConnConfig{ 33 | Handler: &serverCanAcceptConnectHandler{}, 34 | Logger: logrus.StandardLogger(), 35 | } 36 | 37 | prepareConnection(t, config, func(c *ClientConn) { 38 | err := c.Connect(nil) 39 | require.Nil(t, err) 40 | }) 41 | } 42 | 43 | type serverCanRejectConnectHandler struct { 44 | DefaultHandler 45 | } 46 | 47 | func (h *serverCanRejectConnectHandler) OnConnect(_ uint32, _ *message.NetConnectionConnect) error { 48 | return fmt.Errorf("Reject") 49 | } 50 | 51 | func TestServerCanRejectConnect(t *testing.T) { 52 | config := &ConnConfig{ 53 | Handler: &serverCanRejectConnectHandler{}, 54 | Logger: logrus.StandardLogger(), 55 | } 56 | 57 | prepareConnection(t, config, func(c *ClientConn) { 58 | err := c.Connect(nil) 59 | require.Equal(t, &ConnectRejectedError{ 60 | TransactionID: 1, 61 | Result: &message.NetConnectionConnectResult{ 62 | Properties: message.NetConnectionConnectResultProperties{ 63 | FMSVer: "GO-RTMP/0,0,0,0", 64 | Capabilities: 31, 65 | Mode: 1, 66 | }, 67 | Information: message.NetConnectionConnectResultInformation{ 68 | Level: "error", 69 | Code: "NetConnection.Connect.Failed", 70 | Description: "Connection failed.", 71 | Data: amf0.ECMAArray{"type": "go-rtmp", "version": "master"}, 72 | }, 73 | }, 74 | }, err) 75 | }) 76 | } 77 | 78 | type serverCanAcceptCreateStreamHandler struct { 79 | DefaultHandler 80 | } 81 | 82 | func TestServerCanAcceptCreateStream(t *testing.T) { 83 | config := &ConnConfig{ 84 | Handler: &serverCanAcceptCreateStreamHandler{}, 85 | Logger: logrus.StandardLogger(), 86 | ControlState: StreamControlStateConfig{ 87 | MaxMessageStreams: 2, // Control and another 1 stream 88 | }, 89 | } 90 | 91 | prepareConnection(t, config, func(c *ClientConn) { 92 | err := c.Connect(nil) 93 | require.Nil(t, err) 94 | 95 | s0, err := c.CreateStream(nil, chunkSize) 96 | require.Nil(t, err) 97 | defer s0.Close() 98 | 99 | // Rejected because a number of message streams is exceeded the limits 100 | s1, err := c.CreateStream(nil, chunkSize) 101 | require.Equal(t, &CreateStreamRejectedError{ 102 | TransactionID: 2, 103 | Result: &message.NetConnectionCreateStreamResult{ 104 | StreamID: 0, 105 | }, 106 | }, err) 107 | defer s1.Close() 108 | }) 109 | } 110 | 111 | type serverCanAcceptDeleteStreamHandler struct { 112 | DefaultHandler 113 | } 114 | 115 | func TestServerCanAcceptDeleteStream(t *testing.T) { 116 | config := &ConnConfig{ 117 | Handler: &serverCanAcceptDeleteStreamHandler{}, 118 | Logger: logrus.StandardLogger(), 119 | ControlState: StreamControlStateConfig{ 120 | MaxMessageStreams: 2, // Control and another 1 stream 121 | }, 122 | } 123 | 124 | prepareConnection(t, config, func(c *ClientConn) { 125 | err := c.Connect(nil) 126 | require.Nil(t, err) 127 | 128 | s0, err := c.CreateStream(nil, chunkSize) 129 | require.NoError(t, err) 130 | defer s0.Close() 131 | 132 | t.Run("Cannot delete a stream which does not exist", func(t *testing.T) { 133 | err = c.DeleteStream(&message.NetStreamDeleteStream{ 134 | StreamID: 42, 135 | }) 136 | require.Error(t, err) 137 | }) 138 | 139 | t.Run("Can delete a stream", func(t *testing.T) { 140 | err = c.DeleteStream(&message.NetStreamDeleteStream{ 141 | StreamID: s0.streamID, 142 | }) 143 | require.NoError(t, err) 144 | }) 145 | }) 146 | } 147 | 148 | func prepareConnection(t *testing.T, config *ConnConfig, f func(c *ClientConn)) { 149 | // prepare server 150 | l, err := net.Listen("tcp", "127.0.0.1:") 151 | require.Nil(t, err) 152 | 153 | srv := NewServer(&ServerConfig{ 154 | OnConnect: func(conn net.Conn) (io.ReadWriteCloser, *ConnConfig) { 155 | return conn, config 156 | }, 157 | }) 158 | defer func() { 159 | err := srv.Close() 160 | require.Nil(t, err) 161 | }() 162 | 163 | go func() { 164 | err := srv.Serve(l) 165 | require.Equal(t, ErrClosed, err) 166 | }() 167 | 168 | // prepare client 169 | c, err := Dial("rtmp", l.Addr().String(), &ConnConfig{ 170 | Logger: logrus.StandardLogger(), 171 | }) 172 | require.Nil(t, err) 173 | defer func() { 174 | err := c.Close() 175 | require.Nil(t, err) 176 | }() 177 | 178 | f(c) 179 | } 180 | -------------------------------------------------------------------------------- /server_control_connected_handler.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "github.com/pkg/errors" 12 | 13 | "github.com/yutopp/go-rtmp/internal" 14 | "github.com/yutopp/go-rtmp/message" 15 | ) 16 | 17 | var _ stateHandler = (*serverControlConnectedHandler)(nil) 18 | 19 | // serverControlConnectedHandler Handle control messages from a client at server side. 20 | // 21 | // transitions: 22 | // | "createStream" -> spawn! serverDataInactiveHandler 23 | // | _ -> self 24 | type serverControlConnectedHandler struct { 25 | sh *streamHandler 26 | } 27 | 28 | func (h *serverControlConnectedHandler) onMessage( 29 | chunkStreamID int, 30 | timestamp uint32, 31 | msg message.Message, 32 | ) error { 33 | return internal.ErrPassThroughMsg 34 | } 35 | 36 | func (h *serverControlConnectedHandler) onData( 37 | chunkStreamID int, 38 | timestamp uint32, 39 | dataMsg *message.DataMessage, 40 | body interface{}, 41 | ) error { 42 | return internal.ErrPassThroughMsg 43 | } 44 | 45 | func (h *serverControlConnectedHandler) onCommand( 46 | chunkStreamID int, 47 | timestamp uint32, 48 | cmdMsg *message.CommandMessage, 49 | body interface{}, 50 | ) (err error) { 51 | l := h.sh.Logger() 52 | tID := cmdMsg.TransactionID 53 | 54 | switch cmd := body.(type) { 55 | case *message.NetConnectionCreateStream: 56 | l.Infof("Stream creating...: %#v", cmd) 57 | defer func() { 58 | if err != nil { 59 | result := h.newCreateStreamErrorResult() 60 | 61 | l.Infof("CreateStream(Error): ResponseBody = %#v, Err = %+v", result, err) 62 | if err1 := h.sh.stream.ReplyCreateStream(chunkStreamID, timestamp, tID, result); err1 != nil { 63 | err = errors.Wrapf(err, "Failed to reply response: Err = %+v", err1) 64 | } 65 | } 66 | }() 67 | 68 | if err := h.sh.stream.userHandler().OnCreateStream(timestamp, cmd); err != nil { 69 | return err 70 | } 71 | 72 | // Create a stream which handles messages for data(play, publish, video, audio, etc...) 73 | newStream, err := h.sh.stream.streams().conn.streams.CreateIfAvailable() 74 | if err != nil { 75 | l.Errorf("Failed to create stream: Err = %+v", err) 76 | 77 | result := h.newCreateStreamErrorResult() 78 | if err1 := h.sh.stream.ReplyCreateStream(chunkStreamID, timestamp, tID, result); err1 != nil { 79 | return errors.Wrapf(err, "Failed to reply response: Err = %+v", err1) 80 | } 81 | 82 | return nil // Keep the connection 83 | } 84 | newStream.handler.ChangeState(streamStateServerInactive) 85 | 86 | result := h.newCreateStreamSuccessResult(newStream.streamID) 87 | if err := h.sh.stream.ReplyCreateStream(chunkStreamID, timestamp, tID, result); err != nil { 88 | _ = h.sh.stream.streams().Delete(newStream.streamID) // TODO: error handling 89 | return err 90 | } 91 | 92 | l.Infof("Stream created...: NewStreamID = %d", newStream.streamID) 93 | 94 | return nil 95 | 96 | case *message.NetStreamDeleteStream: 97 | l.Infof("Stream deleting...: TargetStreamID = %d", cmd.StreamID) 98 | 99 | if err := h.sh.stream.userHandler().OnDeleteStream(timestamp, cmd); err != nil { 100 | return err 101 | } 102 | 103 | if err := h.sh.stream.streams().Delete(cmd.StreamID); err != nil { 104 | return err 105 | } 106 | 107 | // server does not send any response(7.2.2.3) 108 | 109 | l.Infof("Stream deleted: TargetStreamID = %d", cmd.StreamID) 110 | 111 | return nil 112 | 113 | case *message.NetConnectionReleaseStream: 114 | l.Infof("Release stream...: StreamName = %s", cmd.StreamName) 115 | 116 | if err := h.sh.stream.userHandler().OnReleaseStream(timestamp, cmd); err != nil { 117 | return err 118 | } 119 | 120 | // TODO: send _result? 121 | 122 | return nil 123 | 124 | case *message.NetStreamFCPublish: 125 | l.Infof("FCPublish stream...: StreamName = %s", cmd.StreamName) 126 | 127 | if err := h.sh.stream.userHandler().OnFCPublish(timestamp, cmd); err != nil { 128 | return err 129 | } 130 | 131 | // TODO: send _result? 132 | 133 | return nil 134 | 135 | case *message.NetStreamFCUnpublish: 136 | l.Infof("FCUnpublish stream...: StreamName = %s", cmd.StreamName) 137 | 138 | if err := h.sh.stream.userHandler().OnFCUnpublish(timestamp, cmd); err != nil { 139 | return err 140 | } 141 | 142 | // TODO: send _result? 143 | 144 | return nil 145 | 146 | default: 147 | return internal.ErrPassThroughMsg 148 | } 149 | } 150 | 151 | func (h *serverControlConnectedHandler) newCreateStreamSuccessResult( 152 | streamID uint32, 153 | ) *message.NetConnectionCreateStreamResult { 154 | return &message.NetConnectionCreateStreamResult{ 155 | StreamID: streamID, 156 | } 157 | } 158 | 159 | func (h *serverControlConnectedHandler) newCreateStreamErrorResult() *message.NetConnectionCreateStreamResult { 160 | return nil 161 | } 162 | -------------------------------------------------------------------------------- /message/net_connection.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "github.com/mitchellh/mapstructure" 12 | "github.com/pkg/errors" 13 | "github.com/yutopp/go-amf0" 14 | ) 15 | 16 | type NetConnectionConnectCode string 17 | 18 | const ( 19 | NetConnectionConnectCodeSuccess NetConnectionConnectCode = "NetConnection.Connect.Success" 20 | NetConnectionConnectCodeFailed NetConnectionConnectCode = "NetConnection.Connect.Failed" 21 | NetConnectionConnectCodeClosed NetConnectionConnectCode = "NetConnection.Connect.Closed" 22 | ) 23 | 24 | type NetConnectionConnect struct { 25 | Command NetConnectionConnectCommand 26 | } 27 | 28 | type NetConnectionConnectCommand struct { 29 | App string `mapstructure:"app" amf0:"app"` 30 | Type string `mapstructure:"type" amf0:"type"` 31 | FlashVer string `mapstructure:"flashVer" amf0:"flashVer"` 32 | TCURL string `mapstructure:"tcUrl" amf0:"tcUrl"` 33 | Fpad bool `mapstructure:"fpad" amf0:"fpad"` 34 | Capabilities int `mapstructure:"capabilities" amf0:"capabilities"` 35 | AudioCodecs int `mapstructure:"audioCodecs" amf0:"audioCodecs"` 36 | VideoCodecs int `mapstructure:"videoCodecs" amf0:"videoCodecs"` 37 | VideoFunction int `mapstructure:"videoFunction" amf0:"videoFunction"` 38 | ObjectEncoding EncodingType `mapstructure:"objectEncoding" amf0:"objectEncoding"` 39 | } 40 | 41 | func (t *NetConnectionConnect) FromArgs(args ...interface{}) error { 42 | command := args[0].(map[string]interface{}) 43 | if err := mapstructure.Decode(command, &t.Command); err != nil { 44 | return errors.Wrapf(err, "Failed to mapping NetConnectionConnect") 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func (t *NetConnectionConnect) ToArgs(ty EncodingType) ([]interface{}, error) { 51 | return []interface{}{ 52 | t.Command, 53 | }, nil 54 | } 55 | 56 | type NetConnectionConnectResult struct { 57 | Properties NetConnectionConnectResultProperties 58 | Information NetConnectionConnectResultInformation 59 | } 60 | 61 | type NetConnectionConnectResultProperties struct { 62 | FMSVer string `mapstructure:"fmsVer" amf0:"fmsVer"` // TODO: fix 63 | Capabilities int `mapstructure:"capabilities" amf0:"capabilities"` // TODO: fix 64 | Mode int `mapstructure:"mode" amf0:"mode"` // TODO: fix 65 | } 66 | 67 | type NetConnectionConnectResultInformation struct { 68 | Level string `mapstructure:"level" amf0:"level"` // TODO: fix 69 | Code NetConnectionConnectCode `mapstructure:"code" amf0:"code"` 70 | Description string `mapstructure:"description" amf0:"description"` 71 | Data amf0.ECMAArray `mapstructure:"data" amf0:"data"` 72 | } 73 | 74 | func (t *NetConnectionConnectResult) FromArgs(args ...interface{}) error { 75 | properties := args[0].(map[string]interface{}) 76 | if err := mapstructure.Decode(properties, &t.Properties); err != nil { 77 | return errors.Wrapf(err, "Failed to mapping NetConnectionConnectResultProperties") 78 | } 79 | 80 | information := args[1].(map[string]interface{}) 81 | if err := mapstructure.Decode(information, &t.Information); err != nil { 82 | return errors.Wrapf(err, "Failed to mapping NetConnectionConnectResultInformation") 83 | } 84 | 85 | return nil 86 | } 87 | 88 | func (t *NetConnectionConnectResult) ToArgs(ty EncodingType) ([]interface{}, error) { 89 | return []interface{}{ 90 | t.Properties, 91 | t.Information, 92 | }, nil 93 | } 94 | 95 | type NetConnectionCreateStream struct { 96 | } 97 | 98 | func (t *NetConnectionCreateStream) FromArgs(args ...interface{}) error { 99 | // args[0] // Will be nil... 100 | return nil 101 | } 102 | 103 | func (t *NetConnectionCreateStream) ToArgs(ty EncodingType) ([]interface{}, error) { 104 | return []interface{}{ 105 | nil, // Just null 106 | }, nil 107 | } 108 | 109 | // TODO: fix for error messages 110 | type NetConnectionCreateStreamResult struct { 111 | StreamID uint32 112 | } 113 | 114 | func (t *NetConnectionCreateStreamResult) FromArgs(args ...interface{}) error { 115 | // args[0] is unknown, ignore 116 | t.StreamID = args[1].(uint32) 117 | 118 | return nil 119 | } 120 | 121 | func (t *NetConnectionCreateStreamResult) ToArgs(ty EncodingType) ([]interface{}, error) { 122 | return []interface{}{ 123 | nil, // no command object 124 | t.StreamID, 125 | }, nil 126 | } 127 | 128 | type NetConnectionReleaseStream struct { 129 | StreamName string 130 | } 131 | 132 | func (t *NetConnectionReleaseStream) FromArgs(args ...interface{}) error { 133 | // args[0] is unknown, ignore 134 | t.StreamName = args[1].(string) 135 | 136 | return nil 137 | } 138 | 139 | func (t *NetConnectionReleaseStream) ToArgs(ty EncodingType) ([]interface{}, error) { 140 | return []interface{}{ 141 | nil, // no command object 142 | t.StreamName, 143 | }, nil 144 | } 145 | -------------------------------------------------------------------------------- /message/encoder.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "encoding/binary" 12 | "fmt" 13 | "io" 14 | ) 15 | 16 | type Encoder struct { 17 | w io.Writer 18 | } 19 | 20 | func NewEncoder(w io.Writer) *Encoder { 21 | return &Encoder{ 22 | w: w, 23 | } 24 | } 25 | 26 | func (enc *Encoder) Reset(w io.Writer) { 27 | enc.w = w 28 | } 29 | 30 | // Encode 31 | func (enc *Encoder) Encode(msg Message) error { 32 | switch msg := msg.(type) { 33 | case *SetChunkSize: 34 | return enc.encodeSetChunkSize(msg) 35 | case *AbortMessage: 36 | return enc.encodeAbortMessage(msg) 37 | case *Ack: 38 | return enc.encodeAck(msg) 39 | case *UserCtrl: 40 | return enc.encodeUserCtrl(msg) 41 | case *WinAckSize: 42 | return enc.encodeWinAckSize(msg) 43 | case *SetPeerBandwidth: 44 | return enc.encodeSetPeerBandwidth(msg) 45 | case *AudioMessage: 46 | return enc.encodeAudioMessage(msg) 47 | case *VideoMessage: 48 | return enc.encodeVideoMessage(msg) 49 | case *DataMessage: 50 | return enc.encodeDataMessage(msg) 51 | case *SharedObjectMessageAMF3: 52 | return enc.encodeSharedObjectMessageAMF3(msg) 53 | case *CommandMessage: 54 | return enc.encodeCommandMessage(msg) 55 | case *SharedObjectMessageAMF0: 56 | return enc.encodeSharedObjectMessageAMF0(msg) 57 | case *AggregateMessage: 58 | return enc.encodeAggregateMessage(msg) 59 | default: 60 | return fmt.Errorf("Unexpected message type(encode): ID = %d, Type = %T", msg.TypeID(), msg) 61 | } 62 | } 63 | 64 | func (enc *Encoder) encodeSetChunkSize(m *SetChunkSize) error { 65 | if m.ChunkSize < 1 || m.ChunkSize > 0x7fffffff { 66 | return fmt.Errorf("Invalid format: chunk size is out of range [1, 0x80000000)") 67 | } 68 | 69 | buf := make([]byte, 4) 70 | binary.BigEndian.PutUint32(buf, m.ChunkSize&0x7fffffff) // 0b0111,1111... 71 | 72 | if _, err := enc.w.Write(buf); err != nil { // TODO: length check 73 | return err 74 | } 75 | 76 | return nil 77 | } 78 | 79 | func (enc *Encoder) encodeAbortMessage(m *AbortMessage) error { 80 | buf := make([]byte, 4) 81 | binary.BigEndian.PutUint32(buf, m.ChunkStreamID) // [0:4] 82 | 83 | if _, err := enc.w.Write(buf); err != nil { // TODO: length check 84 | return err 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (enc *Encoder) encodeAck(m *Ack) error { 91 | buf := make([]byte, 4) 92 | binary.BigEndian.PutUint32(buf, m.SequenceNumber) // [0:4] 93 | 94 | if _, err := enc.w.Write(buf); err != nil { // TODO: length check 95 | return err 96 | } 97 | 98 | return nil 99 | } 100 | 101 | func (enc *Encoder) encodeUserCtrl(msg *UserCtrl) error { 102 | ucmEnc := NewUserControlEventEncoder(enc.w) 103 | return ucmEnc.Encode(msg.Event) 104 | } 105 | 106 | func (enc *Encoder) encodeWinAckSize(m *WinAckSize) error { 107 | buf := make([]byte, 4) 108 | binary.BigEndian.PutUint32(buf, uint32(m.Size)) // [0:4] 109 | 110 | if _, err := enc.w.Write(buf); err != nil { // TODO: length check 111 | return err 112 | } 113 | 114 | return nil 115 | } 116 | 117 | func (enc *Encoder) encodeSetPeerBandwidth(m *SetPeerBandwidth) error { 118 | buf := make([]byte, 5) 119 | binary.BigEndian.PutUint32(buf, uint32(m.Size)) // [0:4] 120 | buf[4] = byte(m.Limit) 121 | 122 | if _, err := enc.w.Write(buf); err != nil { // TODO: length check 123 | return err 124 | } 125 | 126 | return nil 127 | } 128 | 129 | func (enc *Encoder) encodeAudioMessage(m *AudioMessage) error { 130 | if _, err := io.Copy(enc.w, m.Payload); err != nil { 131 | return err 132 | } 133 | 134 | return nil 135 | } 136 | 137 | func (enc *Encoder) encodeVideoMessage(m *VideoMessage) error { 138 | if _, err := io.Copy(enc.w, m.Payload); err != nil { 139 | return err 140 | } 141 | 142 | return nil 143 | } 144 | 145 | func (enc *Encoder) encodeSharedObjectMessageAMF3(m *SharedObjectMessageAMF3) error { 146 | return fmt.Errorf("Not implemented: SharedObjectMessageAMF3") 147 | } 148 | 149 | func (enc *Encoder) encodeDataMessage(m *DataMessage) error { 150 | e := NewAMFEncoder(enc.w, m.Encoding) 151 | 152 | if err := e.Encode(m.Name); err != nil { 153 | return err 154 | } 155 | 156 | if _, err := io.Copy(enc.w, m.Body); err != nil { 157 | return err 158 | } 159 | 160 | return nil 161 | } 162 | 163 | func (enc *Encoder) encodeSharedObjectMessageAMF0(m *SharedObjectMessageAMF0) error { 164 | return fmt.Errorf("Not implemented: SharedObjectMessageAMF0") 165 | } 166 | 167 | func (enc *Encoder) encodeCommandMessage(m *CommandMessage) error { 168 | e := NewAMFEncoder(enc.w, m.Encoding) 169 | 170 | if err := e.Encode(m.CommandName); err != nil { 171 | return err 172 | } 173 | if err := e.Encode(m.TransactionID); err != nil { 174 | return err 175 | } 176 | 177 | if _, err := io.Copy(enc.w, m.Body); err != nil { 178 | return err 179 | } 180 | 181 | return nil 182 | } 183 | 184 | func (enc *Encoder) encodeAggregateMessage(m *AggregateMessage) error { 185 | return fmt.Errorf("Not implemented: AggregateMessage") 186 | } 187 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "bufio" 12 | "context" 13 | "io" 14 | "io/ioutil" 15 | "sync" 16 | 17 | "github.com/hashicorp/go-multierror" 18 | "github.com/pkg/errors" 19 | "github.com/sirupsen/logrus" 20 | 21 | "github.com/yutopp/go-rtmp/message" 22 | ) 23 | 24 | type Conn struct { 25 | rwc io.ReadWriteCloser 26 | bufr *bufio.Reader 27 | bufw *bufio.Writer 28 | streamer *ChunkStreamer 29 | streams *streams 30 | handler Handler 31 | 32 | config *ConnConfig 33 | logger logrus.FieldLogger 34 | 35 | ignoredMessages uint32 36 | 37 | m sync.Mutex 38 | isClosed bool 39 | } 40 | 41 | type ConnConfig struct { 42 | Handler Handler 43 | SkipHandshakeVerification bool 44 | 45 | IgnoreMessagesOnNotExistStream bool 46 | IgnoreMessagesOnNotExistStreamThreshold uint32 47 | 48 | ReaderBufferSize int 49 | WriterBufferSize int 50 | 51 | ControlState StreamControlStateConfig 52 | 53 | Logger logrus.FieldLogger 54 | RPreset ResponsePreset 55 | } 56 | 57 | func (cb *ConnConfig) normalize() *ConnConfig { 58 | c := ConnConfig(*cb) 59 | 60 | if c.Handler == nil { 61 | c.Handler = &DefaultHandler{} 62 | } 63 | 64 | if c.ReaderBufferSize == 0 { 65 | c.ReaderBufferSize = 4 * 1024 // 4KB (Default) 66 | } 67 | 68 | if c.WriterBufferSize == 0 { 69 | c.WriterBufferSize = 4 * 1024 // 4KB (Default) 70 | } 71 | 72 | c.ControlState = *c.ControlState.normalize() 73 | 74 | if c.Logger == nil { 75 | l := logrus.New() 76 | l.Out = ioutil.Discard 77 | 78 | c.Logger = l 79 | } 80 | 81 | return &c 82 | } 83 | 84 | func newConn(rwc io.ReadWriteCloser, config *ConnConfig) *Conn { 85 | if config == nil { 86 | config = &ConnConfig{} 87 | } 88 | config = config.normalize() 89 | 90 | conn := &Conn{ 91 | rwc: rwc, 92 | bufr: bufio.NewReaderSize(rwc, config.ReaderBufferSize), 93 | bufw: bufio.NewWriterSize(rwc, config.WriterBufferSize), 94 | handler: config.Handler, 95 | 96 | config: config, 97 | logger: config.Logger, 98 | } 99 | 100 | conn.streamer = NewChunkStreamer(conn.bufr, conn.bufw, &conn.config.ControlState) 101 | conn.streamer.logger = conn.logger 102 | 103 | conn.streams = newStreams(conn) 104 | 105 | return conn 106 | } 107 | 108 | func (c *Conn) GetChunkStreamer() *ChunkStreamer { 109 | return c.streamer 110 | } 111 | 112 | func (c *Conn) Close() error { 113 | c.m.Lock() 114 | defer c.m.Unlock() 115 | 116 | if c.isClosed { 117 | return nil 118 | } 119 | c.isClosed = true 120 | 121 | if c.handler != nil { 122 | c.handler.OnClose() 123 | } 124 | 125 | var result error 126 | if c.streamer != nil { 127 | c.streamer.waitWriters() 128 | if err := c.streamer.Close(); err != nil { 129 | result = multierror.Append(result, err) 130 | } 131 | } 132 | 133 | if err := c.rwc.Close(); err != nil { 134 | result = multierror.Append(result, err) 135 | } 136 | 137 | return result 138 | } 139 | 140 | func (c *Conn) Write(ctx context.Context, chunkStreamID int, timestamp uint32, cmsg *ChunkMessage) error { 141 | return c.streamer.Write(ctx, chunkStreamID, timestamp, cmsg) 142 | } 143 | 144 | func (c *Conn) handleMessageLoop() (err error) { 145 | defer func() { 146 | if r := recover(); r != nil { 147 | errTmp, ok := r.(error) 148 | if !ok { 149 | errTmp = errors.Errorf("Panic: %+v", r) 150 | } 151 | err = errors.WithStack(errTmp) 152 | } 153 | }() 154 | 155 | return c.runHandleMessageLoop() 156 | } 157 | 158 | func (c *Conn) runHandleMessageLoop() error { 159 | var cmsg ChunkMessage 160 | for { 161 | select { 162 | case <-c.streamer.Done(): 163 | return c.streamer.Err() 164 | 165 | default: 166 | chunkStreamID, timestamp, err := c.streamer.Read(&cmsg) 167 | if err != nil { 168 | return err 169 | } 170 | 171 | if err := c.handleMessage(chunkStreamID, timestamp, &cmsg); err != nil { 172 | return err // Shutdown the connection 173 | } 174 | } 175 | } 176 | } 177 | 178 | func (c *Conn) handleMessage(chunkStreamID int, timestamp uint32, cmsg *ChunkMessage) error { 179 | stream, err := c.streams.At(cmsg.StreamID) 180 | if err != nil { 181 | if c.config.IgnoreMessagesOnNotExistStream { 182 | c.logger.Warnf("Messages are received on not exist streams: StreamID = %d, MessageType = %T", 183 | cmsg.StreamID, 184 | cmsg.Message, 185 | ) 186 | 187 | if c.ignoredMessages < c.config.IgnoreMessagesOnNotExistStreamThreshold { 188 | c.ignoredMessages++ 189 | return nil 190 | } 191 | } 192 | 193 | return errors.Errorf("Specified stream is not created yet: StreamID = %d", cmsg.StreamID) 194 | } 195 | 196 | if err := stream.handle(chunkStreamID, timestamp, cmsg.Message); err != nil { 197 | switch err := err.(type) { 198 | case *message.UnknownDataBodyDecodeError, *message.UnknownCommandBodyDecodeError: 199 | // Ignore unknown messsage body 200 | c.logger.Warnf("Ignored unknown message body: Err = %+v", err) 201 | return nil 202 | } 203 | return err 204 | } 205 | 206 | return nil 207 | } 208 | -------------------------------------------------------------------------------- /stream_handler.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "sync" 12 | 13 | "github.com/pkg/errors" 14 | "github.com/sirupsen/logrus" 15 | 16 | "github.com/yutopp/go-rtmp/internal" 17 | "github.com/yutopp/go-rtmp/message" 18 | ) 19 | 20 | type streamState int 21 | 22 | const ( 23 | streamStateUnknown streamState = iota 24 | streamStateServerNotConnected 25 | streamStateServerConnected 26 | streamStateServerInactive 27 | streamStateServerPublish 28 | streamStateServerPlay 29 | streamStateClientNotConnected 30 | streamStateClientConnected 31 | ) 32 | 33 | func (s streamState) String() string { 34 | switch s { 35 | case streamStateServerNotConnected: 36 | return "NotConnected(Server)" 37 | case streamStateServerConnected: 38 | return "Connected(Server)" 39 | case streamStateServerInactive: 40 | return "Inactive(Server)" 41 | case streamStateServerPublish: 42 | return "Publish(Server)" 43 | case streamStateServerPlay: 44 | return "Play(Server)" 45 | case streamStateClientNotConnected: 46 | return "NotConnected(Client)" 47 | case streamStateClientConnected: 48 | return "Connected(Client)" 49 | default: 50 | return "" 51 | } 52 | } 53 | 54 | // streamHandler A handler per streams. 55 | // It holds a handler for each states and processes messages sent to the stream 56 | type streamHandler struct { 57 | stream *Stream 58 | handler stateHandler // A handler for each states 59 | state streamState 60 | 61 | loggerEntry *logrus.Entry 62 | m sync.Mutex 63 | } 64 | 65 | // newEntryHandler Create an incomplete new instance of entryHandler. 66 | // msgHandler fields must be assigned by a caller of this function 67 | func newStreamHandler(s *Stream) *streamHandler { 68 | return &streamHandler{ 69 | stream: s, 70 | } 71 | } 72 | 73 | func (h *streamHandler) Handle(chunkStreamID int, timestamp uint32, msg message.Message) error { 74 | l := h.Logger() 75 | 76 | switch msg := msg.(type) { 77 | case *message.DataMessage: 78 | return h.handleData(chunkStreamID, timestamp, msg) 79 | 80 | case *message.CommandMessage: 81 | return h.handleCommand(chunkStreamID, timestamp, msg) 82 | 83 | case *message.SetChunkSize: 84 | l.Infof("Handle SetChunkSize: Msg = %#v", msg) 85 | return h.stream.streamer().PeerState().SetChunkSize(msg.ChunkSize) 86 | 87 | case *message.WinAckSize: 88 | l.Infof("Handle WinAckSize: Msg = %#v", msg) 89 | return h.stream.streamer().PeerState().SetAckWindowSize(msg.Size) 90 | 91 | default: 92 | err := h.handler.onMessage(chunkStreamID, timestamp, msg) 93 | if err == internal.ErrPassThroughMsg { 94 | return h.stream.userHandler().OnUnknownMessage(timestamp, msg) 95 | } 96 | return err 97 | } 98 | } 99 | 100 | func (h *streamHandler) ChangeState(state streamState) { 101 | h.m.Lock() 102 | defer h.m.Unlock() 103 | 104 | prevState := h.State() 105 | 106 | switch state { 107 | case streamStateUnknown: 108 | return // DO NOTHING 109 | case streamStateServerNotConnected: 110 | h.handler = &serverControlNotConnectedHandler{sh: h} 111 | case streamStateServerConnected: 112 | h.handler = &serverControlConnectedHandler{sh: h} 113 | case streamStateServerInactive: 114 | h.handler = &serverDataInactiveHandler{sh: h} 115 | case streamStateServerPublish: 116 | h.handler = &serverDataPublishHandler{sh: h} 117 | case streamStateServerPlay: 118 | h.handler = &serverDataPlayHandler{sh: h} 119 | case streamStateClientNotConnected: 120 | h.handler = &clientControlNotConnectedHandler{sh: h} 121 | // case streamStateClientConnected: 122 | // h.handler = &serverControlConnectedHandler{sh: h} 123 | default: 124 | panic("Unexpected") 125 | } 126 | h.state = state 127 | 128 | l := h.Logger() 129 | l.Infof("Change state: From = %s, To = %s", prevState, h.State()) 130 | } 131 | 132 | func (h *streamHandler) State() streamState { 133 | return h.state 134 | } 135 | 136 | func (h *streamHandler) Logger() *logrus.Entry { 137 | if h.loggerEntry == nil { 138 | h.loggerEntry = h.stream.logger().WithFields(logrus.Fields{ 139 | "stream_id": h.stream.streamID, 140 | }) 141 | } 142 | 143 | h.loggerEntry.Data["state"] = h.State() 144 | 145 | return h.loggerEntry 146 | } 147 | 148 | func (h *streamHandler) handleData( 149 | chunkStreamID int, 150 | timestamp uint32, 151 | dataMsg *message.DataMessage, 152 | ) error { 153 | bodyDecoder := message.DataBodyDecoderFor(dataMsg.Name) 154 | 155 | amfDec := message.NewAMFDecoder(dataMsg.Body, dataMsg.Encoding) 156 | var value message.AMFConvertible 157 | if err := bodyDecoder(dataMsg.Body, amfDec, &value); err != nil { 158 | return err 159 | } 160 | 161 | err := h.handler.onData(chunkStreamID, timestamp, dataMsg, value) 162 | if err == internal.ErrPassThroughMsg { 163 | return h.stream.userHandler().OnUnknownDataMessage(timestamp, dataMsg) 164 | } 165 | return err 166 | } 167 | 168 | func (h *streamHandler) handleCommand( 169 | chunkStreamID int, 170 | timestamp uint32, 171 | cmdMsg *message.CommandMessage, 172 | ) error { 173 | switch cmdMsg.CommandName { 174 | case "_result", "_error": 175 | t, err := h.stream.transactions.At(cmdMsg.TransactionID) 176 | if err != nil { 177 | return errors.Wrap(err, "Got response to the unexpected transaction") 178 | } 179 | 180 | // Set result (NOTE: should use a mutex for it?) 181 | t.Reply(cmdMsg.CommandName, cmdMsg.Encoding, cmdMsg.Body) 182 | 183 | // Remove transacaction because this transaction is resolved 184 | if err := h.stream.transactions.Delete(cmdMsg.TransactionID); err != nil { 185 | return errors.Wrap(err, "Unexpected behavior: transaction is not found") 186 | } 187 | 188 | return nil 189 | 190 | // TODO: Support onStatus 191 | } 192 | 193 | amfDec := message.NewAMFDecoder(cmdMsg.Body, cmdMsg.Encoding) 194 | bodyDecoder := message.CmdBodyDecoderFor(cmdMsg.CommandName, cmdMsg.TransactionID) 195 | 196 | var value message.AMFConvertible 197 | if err := bodyDecoder(cmdMsg.Body, amfDec, &value); err != nil { 198 | return err 199 | } 200 | 201 | err := h.handler.onCommand(chunkStreamID, timestamp, cmdMsg, value) 202 | if err == internal.ErrPassThroughMsg { 203 | return h.stream.userHandler().OnUnknownCommandMessage(timestamp, cmdMsg) 204 | } 205 | 206 | return err 207 | } 208 | -------------------------------------------------------------------------------- /message/net_stream.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | type NetStreamPublish struct { 11 | CommandObject interface{} 12 | PublishingName string 13 | PublishingType string 14 | } 15 | 16 | func (t *NetStreamPublish) FromArgs(args ...interface{}) error { 17 | //command := args[0] // will be nil 18 | t.PublishingName = args[1].(string) 19 | t.PublishingType = args[2].(string) 20 | 21 | return nil 22 | } 23 | 24 | func (t *NetStreamPublish) ToArgs(ty EncodingType) ([]interface{}, error) { 25 | return []interface{}{ 26 | nil, // Always nil 27 | t.PublishingName, 28 | t.PublishingType, 29 | }, nil 30 | } 31 | 32 | type NetStreamPlay struct { 33 | CommandObject interface{} 34 | StreamName string 35 | Start int64 36 | } 37 | 38 | func (t *NetStreamPlay) FromArgs(args ...interface{}) error { 39 | //command := args[0] // will be nil 40 | t.StreamName = args[1].(string) 41 | t.Start = args[2].(int64) 42 | 43 | return nil 44 | } 45 | 46 | func (t *NetStreamPlay) ToArgs(ty EncodingType) ([]interface{}, error) { 47 | panic("Not implemented") 48 | } 49 | 50 | type NetStreamOnStatusLevel string 51 | 52 | const ( 53 | NetStreamOnStatusLevelStatus NetStreamOnStatusLevel = "status" 54 | NetStreamOnStatusLevelError NetStreamOnStatusLevel = "error" 55 | ) 56 | 57 | type NetStreamOnStatusCode string 58 | 59 | const ( 60 | NetStreamOnStatusCodeConnectSuccess NetStreamOnStatusCode = "NetStream.Connect.Success" 61 | NetStreamOnStatusCodeConnectFailed NetStreamOnStatusCode = "NetStream.Connect.Failed" 62 | NetStreamOnStatusCodeMuticastStreamReset NetStreamOnStatusCode = "NetStream.MulticastStream.Reset" 63 | NetStreamOnStatusCodePlayStart NetStreamOnStatusCode = "NetStream.Play.Start" 64 | NetStreamOnStatusCodePlayFailed NetStreamOnStatusCode = "NetStream.Play.Failed" 65 | NetStreamOnStatusCodePlayComplete NetStreamOnStatusCode = "NetStream.Play.Complete" 66 | NetStreamOnStatusCodePublishBadName NetStreamOnStatusCode = "NetStream.Publish.BadName" 67 | NetStreamOnStatusCodePublishFailed NetStreamOnStatusCode = "NetStream.Publish.Failed" 68 | NetStreamOnStatusCodePublishStart NetStreamOnStatusCode = "NetStream.Publish.Start" 69 | NetStreamOnStatusCodeUnpublishSuccess NetStreamOnStatusCode = "NetStream.Unpublish.Success" 70 | ) 71 | 72 | type NetStreamOnStatus struct { 73 | InfoObject NetStreamOnStatusInfoObject 74 | } 75 | 76 | type NetStreamOnStatusInfoObject struct { 77 | Level NetStreamOnStatusLevel 78 | Code NetStreamOnStatusCode 79 | Description string 80 | } 81 | 82 | func (t *NetStreamOnStatus) FromArgs(args ...interface{}) error { 83 | panic("Not implemented") 84 | } 85 | 86 | func (t *NetStreamOnStatus) ToArgs(ty EncodingType) ([]interface{}, error) { 87 | info := make(map[string]interface{}) 88 | info["level"] = t.InfoObject.Level 89 | info["code"] = t.InfoObject.Code 90 | info["description"] = t.InfoObject.Description 91 | 92 | return []interface{}{ 93 | nil, // Always nil 94 | info, 95 | }, nil 96 | } 97 | 98 | type NetStreamDeleteStream struct { 99 | StreamID uint32 100 | } 101 | 102 | func (t *NetStreamDeleteStream) FromArgs(args ...interface{}) error { 103 | // args[0] is unknown, ignore 104 | t.StreamID = args[1].(uint32) 105 | 106 | return nil 107 | } 108 | 109 | func (t *NetStreamDeleteStream) ToArgs(ty EncodingType) ([]interface{}, error) { 110 | return []interface{}{ 111 | nil, // no command object 112 | t.StreamID, 113 | }, nil 114 | } 115 | 116 | type NetStreamFCPublish struct { 117 | StreamName string 118 | } 119 | 120 | func (t *NetStreamFCPublish) FromArgs(args ...interface{}) error { 121 | // args[0] is unknown, ignore 122 | t.StreamName = args[1].(string) 123 | 124 | return nil 125 | } 126 | 127 | func (t *NetStreamFCPublish) ToArgs(ty EncodingType) ([]interface{}, error) { 128 | return []interface{}{ 129 | nil, // no command object 130 | t.StreamName, 131 | }, nil 132 | } 133 | 134 | type NetStreamFCUnpublish struct { 135 | StreamName string 136 | } 137 | 138 | func (t *NetStreamFCUnpublish) FromArgs(args ...interface{}) error { 139 | // args[0] is unknown, ignore 140 | t.StreamName = args[1].(string) 141 | 142 | return nil 143 | } 144 | 145 | func (t *NetStreamFCUnpublish) ToArgs(ty EncodingType) ([]interface{}, error) { 146 | return []interface{}{ 147 | nil, // no command object 148 | t.StreamName, 149 | }, nil 150 | } 151 | 152 | type NetStreamReleaseStream struct { 153 | StreamName string 154 | } 155 | 156 | func (t *NetStreamReleaseStream) FromArgs(args ...interface{}) error { 157 | // args[0] is unknown, ignore 158 | t.StreamName = args[1].(string) 159 | 160 | return nil 161 | } 162 | 163 | func (t *NetStreamReleaseStream) ToArgs(ty EncodingType) ([]interface{}, error) { 164 | return []interface{}{ 165 | nil, // no command object 166 | t.StreamName, 167 | }, nil 168 | } 169 | 170 | // NetStreamSetDataFrame - send data. AmfData is what will be encoded 171 | type NetStreamSetDataFrame struct { 172 | Payload []byte 173 | AmfData interface{} 174 | } 175 | 176 | func (t *NetStreamSetDataFrame) FromArgs(args ...interface{}) error { 177 | t.Payload = args[0].([]byte) 178 | return nil 179 | } 180 | 181 | func (t *NetStreamSetDataFrame) ToArgs(ty EncodingType) ([]interface{}, error) { 182 | return []interface{}{ 183 | "onMetaData", 184 | t.AmfData, 185 | }, nil 186 | } 187 | 188 | type NetStreamGetStreamLength struct { 189 | StreamName string 190 | } 191 | 192 | func (t *NetStreamGetStreamLength) FromArgs(args ...interface{}) error { 193 | // args[0] is unknown, ignore 194 | t.StreamName = args[1].(string) 195 | 196 | return nil 197 | } 198 | 199 | func (t *NetStreamGetStreamLength) ToArgs(ty EncodingType) ([]interface{}, error) { 200 | return []interface{}{ 201 | nil, // no command object 202 | t.StreamName, 203 | }, nil 204 | } 205 | 206 | type NetStreamPing struct { 207 | } 208 | 209 | func (t *NetStreamPing) FromArgs(args ...interface{}) error { 210 | // args[0] is unknown, ignore 211 | 212 | return nil 213 | } 214 | 215 | func (t *NetStreamPing) ToArgs(ty EncodingType) ([]interface{}, error) { 216 | return []interface{}{ 217 | nil, // no command object 218 | }, nil 219 | } 220 | 221 | type NetStreamCloseStream struct { 222 | } 223 | 224 | func (t *NetStreamCloseStream) FromArgs(args ...interface{}) error { 225 | // args[0] is unknown, ignore 226 | 227 | return nil 228 | } 229 | 230 | func (t *NetStreamCloseStream) ToArgs(ty EncodingType) ([]interface{}, error) { 231 | return []interface{}{ 232 | nil, // no command object 233 | }, nil 234 | } 235 | -------------------------------------------------------------------------------- /chunk_header.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "encoding/binary" 12 | "fmt" 13 | "io" 14 | ) 15 | 16 | type chunkBasicHeader struct { 17 | fmt byte 18 | chunkStreamID int /* [0, 65599] */ 19 | } 20 | 21 | func decodeChunkBasicHeader(r io.Reader, buf []byte, bh *chunkBasicHeader) error { 22 | if buf == nil || len(buf) < 3 { 23 | buf = make([]byte, 3) 24 | } 25 | 26 | if _, err := io.ReadAtLeast(r, buf[:1], 1); err != nil { 27 | return err 28 | } 29 | 30 | fmtTy := (buf[0] & 0xc0) >> 6 // 0b11000000 >> 6 31 | csID := int(buf[0] & 0x3f) // 0b00111111 32 | 33 | switch csID { 34 | case 0: 35 | if _, err := io.ReadAtLeast(r, buf[1:2], 1); err != nil { 36 | return err 37 | } 38 | csID = int(buf[1]) + 64 39 | 40 | case 1: 41 | if _, err := io.ReadAtLeast(r, buf[1:], 2); err != nil { 42 | return err 43 | } 44 | csID = int(buf[2])*256 + int(buf[1]) + 64 45 | } 46 | 47 | bh.fmt = fmtTy 48 | bh.chunkStreamID = csID 49 | 50 | return nil 51 | } 52 | 53 | func encodeChunkBasicHeader(w io.Writer, mh *chunkBasicHeader) error { 54 | buf := make([]byte, 3) 55 | buf[0] = byte(mh.fmt&0x03) << 6 // 0b00000011 << 6 56 | 57 | switch { 58 | case mh.chunkStreamID >= 2 && mh.chunkStreamID <= 63: 59 | buf[0] |= byte(mh.chunkStreamID & 0x3f) // 0x00111111 60 | _, err := w.Write(buf[:1]) // TODO: should check length? 61 | return err 62 | 63 | case mh.chunkStreamID >= 64 && mh.chunkStreamID <= 319: 64 | buf[0] |= byte(0 & 0x3f) // 0x00111111 65 | buf[1] = byte(mh.chunkStreamID - 64) 66 | _, err := w.Write(buf[:2]) // TODO: should check length? 67 | return err 68 | 69 | case mh.chunkStreamID >= 320 && mh.chunkStreamID <= 65599: 70 | buf[0] |= byte(1 & 0x3f) // 0x00111111 71 | buf[1] = byte(int(mh.chunkStreamID-64) % 256) 72 | buf[2] = byte(int(mh.chunkStreamID-64) / 256) 73 | _, err := w.Write(buf) // TODO: should check length? 74 | return err 75 | 76 | default: 77 | return fmt.Errorf("Chunk stream id is out of range: %d must be in range [2, 65599]", mh.chunkStreamID) 78 | } 79 | } 80 | 81 | type chunkMessageHeader struct { 82 | timestamp uint32 // fmt = 0 83 | timestampDelta uint32 // fmt = 1 | 2 84 | messageLength uint32 // fmt = 0 | 1 85 | messageTypeID byte // fmt = 0 | 1 86 | messageStreamID uint32 // fmt = 0 87 | } 88 | 89 | func decodeChunkMessageHeader(r io.Reader, fmt byte, buf []byte, mh *chunkMessageHeader) error { 90 | if buf == nil || len(buf) < 11 { 91 | buf = make([]byte, 11) 92 | } 93 | cache32bits := make([]byte, 4) 94 | 95 | switch fmt { 96 | case 0: 97 | if _, err := io.ReadAtLeast(r, buf[:11], 11); err != nil { 98 | return err 99 | } 100 | 101 | copy(cache32bits[1:], buf[0:3]) // 24bits BE 102 | mh.timestamp = binary.BigEndian.Uint32(cache32bits) 103 | copy(cache32bits[1:], buf[3:6]) // 24bits BE 104 | mh.messageLength = binary.BigEndian.Uint32(cache32bits) 105 | mh.messageTypeID = buf[6] // 8bits 106 | mh.messageStreamID = binary.LittleEndian.Uint32(buf[7:11]) // 32bits 107 | 108 | if mh.timestamp == 0xffffff { 109 | _, err := io.ReadAtLeast(r, cache32bits, 4) 110 | if err != nil { 111 | return err 112 | } 113 | mh.timestamp = binary.BigEndian.Uint32(cache32bits) 114 | } 115 | 116 | case 1: 117 | if _, err := io.ReadAtLeast(r, buf[:7], 7); err != nil { 118 | return err 119 | } 120 | 121 | copy(cache32bits[1:], buf[0:3]) // 24bits BE 122 | mh.timestampDelta = binary.BigEndian.Uint32(cache32bits) 123 | copy(cache32bits[1:], buf[3:6]) // 24bits BE 124 | mh.messageLength = binary.BigEndian.Uint32(cache32bits) 125 | mh.messageTypeID = buf[6] // 8bits 126 | 127 | if mh.timestampDelta == 0xffffff { 128 | _, err := io.ReadAtLeast(r, cache32bits, 4) 129 | if err != nil { 130 | return err 131 | } 132 | mh.timestampDelta = binary.BigEndian.Uint32(cache32bits) 133 | } 134 | 135 | case 2: 136 | if _, err := io.ReadAtLeast(r, buf[:3], 3); err != nil { 137 | return err 138 | } 139 | 140 | copy(cache32bits[1:], buf[0:3]) // 24bits BE 141 | mh.timestampDelta = binary.BigEndian.Uint32(cache32bits) 142 | 143 | if mh.timestampDelta == 0xffffff { 144 | _, err := io.ReadAtLeast(r, cache32bits, 4) 145 | if err != nil { 146 | return err 147 | } 148 | mh.timestampDelta = binary.BigEndian.Uint32(cache32bits) 149 | } 150 | 151 | case 3: 152 | // DO NOTHING 153 | 154 | default: 155 | panic("Unexpected fmt") 156 | } 157 | 158 | return nil 159 | } 160 | 161 | func encodeChunkMessageHeader(w io.Writer, fmt byte, mh *chunkMessageHeader) error { 162 | buf := make([]byte, 11+4) 163 | cache32bits := make([]byte, 4) 164 | ext := false 165 | 166 | switch fmt { 167 | case 0: 168 | buflen := 11 169 | ts := mh.timestamp 170 | if ts >= 0xffffff { 171 | ts = 0xffffff 172 | ext = true 173 | buflen += 4 174 | } 175 | 176 | binary.BigEndian.PutUint32(cache32bits, ts) 177 | copy(buf[0:3], cache32bits[1:]) // 24 bits BE 178 | binary.BigEndian.PutUint32(cache32bits, mh.messageLength) 179 | copy(buf[3:6], cache32bits[1:]) // 24 bits BE 180 | buf[6] = mh.messageTypeID // 8bits 181 | binary.LittleEndian.PutUint32(buf[7:11], mh.messageStreamID) 182 | 183 | if ext { 184 | binary.BigEndian.PutUint32(buf[11:], mh.timestamp) 185 | } 186 | 187 | _, err := w.Write(buf[:buflen]) // TODO: should check length? 188 | return err 189 | 190 | case 1: 191 | buflen := 7 192 | td := mh.timestampDelta 193 | if td >= 0xffffff { 194 | td = 0xffffff 195 | ext = true 196 | buflen += 4 197 | } 198 | 199 | binary.BigEndian.PutUint32(cache32bits, td) 200 | copy(buf[0:3], cache32bits[1:]) // 24bits BE 201 | binary.BigEndian.PutUint32(cache32bits, mh.messageLength) 202 | copy(buf[3:6], cache32bits[1:]) // 24bits BE 203 | buf[6] = mh.messageTypeID // 8bits 204 | 205 | if ext { 206 | binary.BigEndian.PutUint32(buf[7:], mh.timestampDelta) 207 | } 208 | 209 | _, err := w.Write(buf[:buflen]) // TODO: should check length? 210 | return err 211 | 212 | case 2: 213 | buflen := 3 214 | td := mh.timestampDelta 215 | if td >= 0xffffff { 216 | td = 0xffffff 217 | ext = true 218 | buflen += 4 219 | } 220 | 221 | binary.BigEndian.PutUint32(cache32bits, td) 222 | copy(buf[0:3], cache32bits[1:]) // 24bits BE 223 | 224 | if ext { 225 | binary.BigEndian.PutUint32(buf[3:], mh.timestampDelta) 226 | } 227 | 228 | _, err := w.Write(buf[:buflen]) // TODO: should check length? 229 | return err 230 | 231 | case 3: 232 | // DO NOTHING 233 | return nil 234 | 235 | default: 236 | panic("Unexpected fmt") 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /message/body_decoder_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "bytes" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/require" 15 | "github.com/yutopp/go-amf0" 16 | ) 17 | 18 | func TestDecodeDataMessageAtsetDataFrame(t *testing.T) { 19 | bin := []byte("payload") 20 | r := bytes.NewReader(bin) 21 | d := amf0.NewDecoder(r) 22 | 23 | var v AMFConvertible 24 | err := DataBodyDecoderFor("@setDataFrame")(r, d, &v) 25 | require.Nil(t, err) 26 | require.Equal(t, &NetStreamSetDataFrame{ 27 | Payload: bin, 28 | }, v) 29 | } 30 | 31 | func TestDecodeDataMessageUnknown(t *testing.T) { 32 | bin := []byte{ 33 | // nil 34 | 0x05, 35 | } 36 | r := bytes.NewReader(bin) 37 | d := amf0.NewDecoder(r) 38 | 39 | var v AMFConvertible 40 | err := DataBodyDecoderFor("hogehoge")(r, d, &v) 41 | require.Equal(t, &UnknownDataBodyDecodeError{ 42 | Name: "hogehoge", 43 | Objs: []interface{}{nil}, 44 | }, err) 45 | require.Nil(t, v) 46 | } 47 | 48 | func TestDecodeCmdMessageConnect(t *testing.T) { 49 | bin := []byte{ 50 | // nil 51 | 0x05, 52 | } 53 | r := bytes.NewReader(bin) 54 | d := amf0.NewDecoder(r) 55 | 56 | var v AMFConvertible 57 | err := CmdBodyDecoderFor("connect", 1)(r, d, &v) // Transaction is always 1 (7.2.1.1) 58 | require.Nil(t, err) 59 | require.Equal(t, &NetConnectionConnect{}, v) 60 | } 61 | 62 | func TestDecodeCmdMessageCreateStream(t *testing.T) { 63 | bin := []byte{ 64 | // nil 65 | 0x05, 66 | } 67 | r := bytes.NewReader(bin) 68 | d := amf0.NewDecoder(r) 69 | 70 | var v AMFConvertible 71 | err := CmdBodyDecoderFor("createStream", 42)(r, d, &v) 72 | require.Nil(t, err) 73 | require.Equal(t, &NetConnectionCreateStream{}, v) 74 | } 75 | 76 | func TestDecodeCmdMessageDeleteStream(t *testing.T) { 77 | bin := []byte{ 78 | // nil 79 | 0x05, 80 | // number: 42 81 | 0x00, 0x40, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 82 | } 83 | r := bytes.NewReader(bin) 84 | d := amf0.NewDecoder(r) 85 | 86 | var v AMFConvertible 87 | err := CmdBodyDecoderFor("deleteStream", 42)(r, d, &v) 88 | require.Nil(t, err) 89 | require.Equal(t, &NetStreamDeleteStream{ 90 | StreamID: 42, 91 | }, v) 92 | } 93 | 94 | func TestDecodeCmdMessagePublish(t *testing.T) { 95 | bin := []byte{ 96 | // nil 97 | 0x05, 98 | // string: abc 99 | 0x02, 0x00, 0x03, 0x61, 0x62, 0x63, 100 | // string: def 101 | 0x02, 0x00, 0x03, 0x64, 0x65, 0x66, 102 | } 103 | r := bytes.NewReader(bin) 104 | d := amf0.NewDecoder(r) 105 | 106 | var v AMFConvertible 107 | err := CmdBodyDecoderFor("publish", 42)(r, d, &v) 108 | require.Nil(t, err) 109 | require.Equal(t, &NetStreamPublish{ 110 | PublishingName: "abc", 111 | PublishingType: "def", 112 | }, v) 113 | } 114 | 115 | func TestDecodeCmdMessagePublishWithoutPublishingType(t *testing.T) { 116 | bin := []byte{ 117 | // nil 118 | 0x05, 119 | // string: abc 120 | 0x02, 0x00, 0x03, 0x61, 0x62, 0x63, 121 | } 122 | r := bytes.NewReader(bin) 123 | d := amf0.NewDecoder(r) 124 | 125 | var v AMFConvertible 126 | err := CmdBodyDecoderFor("publish", 42)(r, d, &v) 127 | require.Nil(t, err) 128 | require.Equal(t, &NetStreamPublish{ 129 | PublishingName: "abc", 130 | PublishingType: "live", 131 | }, v) 132 | } 133 | 134 | func TestDecodeCmdMessagePlay(t *testing.T) { 135 | bin := []byte{ 136 | // nil 137 | 0x05, 138 | // string: abc 139 | 0x02, 0x00, 0x03, 0x61, 0x62, 0x63, 140 | // number: 42 141 | 0x00, 0x40, 0x45, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 142 | } 143 | r := bytes.NewReader(bin) 144 | d := amf0.NewDecoder(r) 145 | 146 | var v AMFConvertible 147 | err := CmdBodyDecoderFor("play", 42)(r, d, &v) 148 | require.Nil(t, err) 149 | require.Equal(t, &NetStreamPlay{ 150 | StreamName: "abc", 151 | Start: 42, 152 | }, v) 153 | } 154 | 155 | func TestDecodeCmdMessageReleaseStream(t *testing.T) { 156 | bin := []byte{ 157 | // nil 158 | 0x05, 159 | // string: abc 160 | 0x02, 0x00, 0x03, 0x61, 0x62, 0x63, 161 | } 162 | r := bytes.NewReader(bin) 163 | d := amf0.NewDecoder(r) 164 | 165 | var v AMFConvertible 166 | err := CmdBodyDecoderFor("releaseStream", 42)(r, d, &v) 167 | require.Nil(t, err) 168 | require.Equal(t, &NetConnectionReleaseStream{ 169 | StreamName: "abc", 170 | }, v) 171 | } 172 | 173 | func TestDecodeCmdMessageFCPublish(t *testing.T) { 174 | bin := []byte{ 175 | // nil 176 | 0x05, 177 | // string: abc 178 | 0x02, 0x00, 0x03, 0x61, 0x62, 0x63, 179 | } 180 | r := bytes.NewReader(bin) 181 | d := amf0.NewDecoder(r) 182 | 183 | var v AMFConvertible 184 | err := CmdBodyDecoderFor("FCPublish", 42)(r, d, &v) 185 | require.Nil(t, err) 186 | require.Equal(t, &NetStreamFCPublish{ 187 | StreamName: "abc", 188 | }, v) 189 | } 190 | 191 | func TestDecodeCmdMessageFCUnpublish(t *testing.T) { 192 | bin := []byte{ 193 | // nil 194 | 0x05, 195 | // string: abc 196 | 0x02, 0x00, 0x03, 0x61, 0x62, 0x63, 197 | } 198 | r := bytes.NewReader(bin) 199 | d := amf0.NewDecoder(r) 200 | 201 | var v AMFConvertible 202 | err := CmdBodyDecoderFor("FCUnpublish", 42)(r, d, &v) 203 | require.Nil(t, err) 204 | require.Equal(t, &NetStreamFCUnpublish{ 205 | StreamName: "abc", 206 | }, v) 207 | } 208 | 209 | func TestDecodeCmdMessageGetStreamLength(t *testing.T) { 210 | bin := []byte{ 211 | // nil 212 | 0x05, 213 | // string: abc 214 | 0x02, 0x00, 0x03, 0x61, 0x62, 0x63, 215 | } 216 | r := bytes.NewReader(bin) 217 | d := amf0.NewDecoder(r) 218 | 219 | var v AMFConvertible 220 | err := CmdBodyDecoderFor("getStreamLength", 42)(r, d, &v) 221 | require.Nil(t, err) 222 | require.Equal(t, &NetStreamGetStreamLength{ 223 | StreamName: "abc", 224 | }, v) 225 | } 226 | 227 | func TestDecodeCmdMessagePing(t *testing.T) { 228 | bin := []byte{ 229 | // nil 230 | 0x05, 231 | } 232 | r := bytes.NewReader(bin) 233 | d := amf0.NewDecoder(r) 234 | 235 | var v AMFConvertible 236 | err := CmdBodyDecoderFor("ping", 42)(r, d, &v) 237 | require.Nil(t, err) 238 | require.Equal(t, &NetStreamPing{}, v) 239 | } 240 | 241 | func TestDecodeCmdMessageCloseStream(t *testing.T) { 242 | bin := []byte{ 243 | // nil 244 | 0x05, 245 | } 246 | r := bytes.NewReader(bin) 247 | d := amf0.NewDecoder(r) 248 | 249 | var v AMFConvertible 250 | err := CmdBodyDecoderFor("closeStream", 42)(r, d, &v) 251 | require.Nil(t, err) 252 | require.Equal(t, &NetStreamCloseStream{}, v) 253 | } 254 | 255 | func TestDecodeCmdMessageUnknown(t *testing.T) { 256 | bin := []byte{ 257 | // nil 258 | 0x05, 259 | } 260 | r := bytes.NewReader(bin) 261 | d := amf0.NewDecoder(r) 262 | 263 | var v AMFConvertible 264 | err := CmdBodyDecoderFor("hogehoge", 42)(r, d, &v) 265 | require.Equal(t, &UnknownCommandBodyDecodeError{ 266 | Name: "hogehoge", 267 | TransactionID: 42, 268 | Objs: []interface{}{nil}, 269 | }, err) 270 | require.Nil(t, v) 271 | } 272 | -------------------------------------------------------------------------------- /message/decoder.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "encoding/binary" 12 | "fmt" 13 | "io" 14 | 15 | "github.com/pkg/errors" 16 | 17 | "github.com/yutopp/go-amf0" 18 | ) 19 | 20 | type Decoder struct { 21 | r io.Reader 22 | } 23 | 24 | func NewDecoder(r io.Reader) *Decoder { 25 | return &Decoder{ 26 | r: r, 27 | } 28 | } 29 | 30 | func (dec *Decoder) Reset(r io.Reader) { 31 | dec.r = r 32 | } 33 | 34 | func (dec *Decoder) Decode(typeID TypeID, msg *Message) error { 35 | switch typeID { 36 | case TypeIDSetChunkSize: 37 | return dec.decodeSetChunkSize(msg) 38 | case TypeIDAbortMessage: 39 | return dec.decodeAbortMessage(msg) 40 | case TypeIDAck: 41 | return dec.decodeAck(msg) 42 | case TypeIDUserCtrl: 43 | return dec.decodeUserCtrl(msg) 44 | case TypeIDWinAckSize: 45 | return dec.decodeWinAckSize(msg) 46 | case TypeIDSetPeerBandwidth: 47 | return dec.decodeSetPeerBandwidth(msg) 48 | case TypeIDAudioMessage: 49 | return dec.decodeAudioMessage(msg) 50 | case TypeIDVideoMessage: 51 | return dec.decodeVideoMessage(msg) 52 | case TypeIDDataMessageAMF3: 53 | return dec.decodeDataMessageAMF3(msg) 54 | case TypeIDSharedObjectMessageAMF3: 55 | return dec.decodeSharedObjectMessageAMF3(msg) 56 | case TypeIDCommandMessageAMF3: 57 | return dec.decodeCommandMessageAMF3(msg) 58 | case TypeIDDataMessageAMF0: 59 | return dec.decodeDataMessageAMF0(msg) 60 | case TypeIDSharedObjectMessageAMF0: 61 | return dec.decodeSharedObjectMessageAMF0(msg) 62 | case TypeIDCommandMessageAMF0: 63 | return dec.decodeCommandMessageAMF0(msg) 64 | case TypeIDAggregateMessage: 65 | return dec.decodeAggregateMessage(msg) 66 | default: 67 | return fmt.Errorf("Unexpected message type(decode): ID = %d", typeID) 68 | } 69 | } 70 | 71 | func (dec *Decoder) decodeSetChunkSize(msg *Message) error { 72 | buf := make([]byte, 4) 73 | if _, err := io.ReadAtLeast(dec.r, buf, 4); err != nil { 74 | return err 75 | } 76 | 77 | total := binary.BigEndian.Uint32(buf) 78 | 79 | bit := (total & 0x80000000) >> 31 // 0b1000,0000... >> 31 80 | chunkSize := total & 0x7fffffff // 0b0111,1111... 81 | 82 | if bit != 0 { 83 | return fmt.Errorf("Invalid format: bit must be 0") 84 | } 85 | 86 | if chunkSize == 0 { 87 | return fmt.Errorf("Invalid format: chunk size is 0") 88 | } 89 | 90 | *msg = &SetChunkSize{ 91 | ChunkSize: chunkSize, 92 | } 93 | 94 | return nil 95 | } 96 | 97 | func (dec *Decoder) decodeAbortMessage(msg *Message) error { 98 | buf := make([]byte, 4) 99 | if _, err := io.ReadAtLeast(dec.r, buf, 4); err != nil { 100 | return err 101 | } 102 | 103 | chunkStreamID := binary.BigEndian.Uint32(buf) 104 | 105 | *msg = &AbortMessage{ 106 | ChunkStreamID: chunkStreamID, 107 | } 108 | 109 | return nil 110 | } 111 | 112 | func (dec *Decoder) decodeAck(msg *Message) error { 113 | buf := make([]byte, 4) 114 | if _, err := io.ReadAtLeast(dec.r, buf, 4); err != nil { 115 | return err 116 | } 117 | 118 | sequenceNumber := binary.BigEndian.Uint32(buf) 119 | 120 | *msg = &Ack{ 121 | SequenceNumber: sequenceNumber, 122 | } 123 | 124 | return nil 125 | } 126 | 127 | func (dec *Decoder) decodeUserCtrl(msg *Message) error { 128 | ucmDec := NewUserControlEventDecoder(dec.r) 129 | 130 | var event UserCtrlEvent 131 | if err := ucmDec.Decode(&event); err != nil { 132 | return errors.Wrapf(err, "Failed to decode UserCtrl") 133 | } 134 | 135 | *msg = &UserCtrl{ 136 | Event: event, 137 | } 138 | 139 | return nil 140 | } 141 | 142 | func (dec *Decoder) decodeWinAckSize(msg *Message) error { 143 | buf := make([]byte, 4) 144 | if _, err := io.ReadAtLeast(dec.r, buf, 4); err != nil { 145 | return err 146 | } 147 | 148 | size := int32(binary.BigEndian.Uint32(buf)) 149 | 150 | *msg = &WinAckSize{ 151 | Size: size, 152 | } 153 | 154 | return nil 155 | } 156 | 157 | func (dec *Decoder) decodeSetPeerBandwidth(msg *Message) error { 158 | buf := make([]byte, 5) 159 | if _, err := io.ReadAtLeast(dec.r, buf, 5); err != nil { 160 | return err 161 | } 162 | 163 | size := int32(binary.BigEndian.Uint32(buf[0:4])) 164 | limit := LimitType(buf[4]) 165 | 166 | *msg = &SetPeerBandwidth{ 167 | Size: size, 168 | Limit: limit, 169 | } 170 | 171 | return nil 172 | } 173 | 174 | func (dec *Decoder) decodeAudioMessage(msg *Message) error { 175 | *msg = &AudioMessage{ 176 | Payload: dec.r, // Share an ownership of the reader 177 | } 178 | 179 | return nil 180 | } 181 | 182 | func (dec *Decoder) decodeVideoMessage(msg *Message) error { 183 | *msg = &VideoMessage{ 184 | Payload: dec.r, // Share an ownership of the reader 185 | } 186 | 187 | return nil 188 | } 189 | 190 | func (dec *Decoder) decodeDataMessageAMF3(msg *Message) error { 191 | return fmt.Errorf("Not implemented: DataMessageAMF3") 192 | } 193 | 194 | func (dec *Decoder) decodeSharedObjectMessageAMF3(msg *Message) error { 195 | return fmt.Errorf("Not implemented: SharedObjectMessageAMF3") 196 | } 197 | 198 | func (dec *Decoder) decodeCommandMessageAMF3(msg *Message) error { 199 | return fmt.Errorf("Not implemented: CommandMessageAMF3") 200 | } 201 | 202 | func (dec *Decoder) decodeDataMessageAMF0(msg *Message) error { 203 | if err := dec.decodeDataMessage(msg, func(r io.Reader) (AMFDecoder, EncodingType) { 204 | return amf0.NewDecoder(r), EncodingTypeAMF0 205 | }); err != nil { 206 | return err 207 | } 208 | 209 | return nil 210 | } 211 | 212 | func (dec *Decoder) decodeSharedObjectMessageAMF0(msg *Message) error { 213 | return fmt.Errorf("Not implemented: SharedObjectMessageAMF0") 214 | } 215 | 216 | func (dec *Decoder) decodeCommandMessageAMF0(msg *Message) error { 217 | if err := dec.decodeCommandMessage(msg, func(r io.Reader) (AMFDecoder, EncodingType) { 218 | return amf0.NewDecoder(r), EncodingTypeAMF0 219 | }); err != nil { 220 | return err 221 | } 222 | 223 | return nil 224 | } 225 | 226 | func (dec *Decoder) decodeAggregateMessage(msg *Message) error { 227 | return fmt.Errorf("Not implemented: AggregateMessage") 228 | } 229 | 230 | func (dec *Decoder) decodeDataMessage(msg *Message, f func(r io.Reader) (AMFDecoder, EncodingType)) error { 231 | d, encTy := f(dec.r) 232 | 233 | var name string 234 | if err := d.Decode(&name); err != nil { 235 | return errors.Wrap(err, "Failed to decode name") 236 | } 237 | 238 | *msg = &DataMessage{ 239 | Name: name, 240 | Encoding: encTy, 241 | Body: dec.r, // Share an ownership of the reader 242 | } 243 | 244 | return nil 245 | } 246 | 247 | func (dec *Decoder) decodeCommandMessage(msg *Message, f func(r io.Reader) (AMFDecoder, EncodingType)) error { 248 | d, encTy := f(dec.r) 249 | 250 | var name string 251 | if err := d.Decode(&name); err != nil { 252 | return errors.Wrap(err, "Failed to decode name") 253 | } 254 | 255 | var transactionID int64 256 | if err := d.Decode(&transactionID); err != nil { 257 | return errors.Wrap(err, "Failed to decode transactionID") 258 | } 259 | 260 | *msg = &CommandMessage{ 261 | CommandName: name, 262 | TransactionID: transactionID, 263 | Encoding: encTy, 264 | Body: dec.r, // Share an ownership of the reader 265 | } 266 | 267 | return nil 268 | } 269 | -------------------------------------------------------------------------------- /stream.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "bytes" 12 | "context" 13 | "time" 14 | 15 | "github.com/pkg/errors" 16 | "github.com/sirupsen/logrus" 17 | 18 | "github.com/yutopp/go-rtmp/message" 19 | ) 20 | 21 | // Stream represents a logical message stream 22 | type Stream struct { 23 | streamID uint32 24 | encTy message.EncodingType 25 | transactions *transactions 26 | handler *streamHandler 27 | cmsg ChunkMessage 28 | 29 | conn *Conn 30 | } 31 | 32 | func newStream(streamID uint32, conn *Conn) *Stream { 33 | s := &Stream{ 34 | streamID: streamID, 35 | encTy: message.EncodingTypeAMF0, // Default AMF encoding type 36 | transactions: newTransactions(), 37 | cmsg: ChunkMessage{ 38 | StreamID: streamID, 39 | }, 40 | 41 | conn: conn, 42 | } 43 | s.handler = newStreamHandler(s) 44 | 45 | return s 46 | } 47 | 48 | func (s *Stream) StreamID() uint32 { 49 | return s.streamID 50 | } 51 | 52 | func (s *Stream) WriteWinAckSize(chunkStreamID int, timestamp uint32, msg *message.WinAckSize) error { 53 | return s.Write(chunkStreamID, timestamp, msg) 54 | } 55 | 56 | func (s *Stream) WriteSetPeerBandwidth(chunkStreamID int, timestamp uint32, msg *message.SetPeerBandwidth) error { 57 | return s.Write(chunkStreamID, timestamp, msg) 58 | } 59 | 60 | func (s *Stream) WriteUserCtrl(chunkStreamID int, timestamp uint32, msg *message.UserCtrl) error { 61 | return s.Write(chunkStreamID, timestamp, msg) 62 | } 63 | 64 | func (s *Stream) Connect( 65 | body *message.NetConnectionConnect, 66 | ) (*message.NetConnectionConnectResult, error) { 67 | transactionID := int64(1) // Always 1 (7.2.1.1) 68 | t, err := s.transactions.Create(transactionID) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | if body == nil { 74 | body = &message.NetConnectionConnect{} 75 | } 76 | 77 | chunkStreamID := 3 // TODO: fix 78 | err = s.writeCommandMessage( 79 | chunkStreamID, 0, // Timestamp is 0 80 | "connect", 81 | transactionID, 82 | body, 83 | ) 84 | if err != nil { 85 | return nil, err 86 | } 87 | 88 | // TODO: support timeout 89 | timeoutCtx := context.TODO() 90 | select { 91 | case <-timeoutCtx.Done(): 92 | return nil, timeoutCtx.Err() 93 | case <-t.doneCh: 94 | amfDec := message.NewAMFDecoder(t.body, t.encoding) 95 | 96 | var value message.AMFConvertible 97 | if err := message.DecodeBodyConnectResult(t.body, amfDec, &value); err != nil { 98 | return nil, errors.Wrap(err, "Failed to decode result") 99 | } 100 | result := value.(*message.NetConnectionConnectResult) 101 | 102 | if t.commandName == "_error" { 103 | return nil, &ConnectRejectedError{ 104 | TransactionID: transactionID, 105 | Result: result, 106 | } 107 | } 108 | 109 | return result, nil 110 | } 111 | 112 | //return nil, errors.New("Failed to get result") 113 | } 114 | 115 | func (s *Stream) ReplyConnect( 116 | chunkStreamID int, 117 | timestamp uint32, 118 | body *message.NetConnectionConnectResult, 119 | ) error { 120 | var commandName string 121 | switch body.Information.Code { 122 | case message.NetConnectionConnectCodeSuccess, message.NetConnectionConnectCodeClosed: 123 | commandName = "_result" 124 | case message.NetConnectionConnectCodeFailed: 125 | commandName = "_error" 126 | } 127 | 128 | return s.writeCommandMessage( 129 | chunkStreamID, timestamp, 130 | commandName, 131 | 1, // 7.2.1.2, flow.6 132 | body, 133 | ) 134 | } 135 | 136 | func (s *Stream) CreateStream(body *message.NetConnectionCreateStream, chunkSize uint32) (*message.NetConnectionCreateStreamResult, error) { 137 | oldChunkSize := s.conn.streamer.selfState.chunkSize 138 | if chunkSize > 0 && chunkSize != oldChunkSize { 139 | logrus.Infof("Changing chunkSize %d->%d", oldChunkSize, chunkSize) 140 | s.conn.streamer.selfState.chunkSize = chunkSize 141 | err := s.WriteSetChunkSize(chunkSize) 142 | if err != nil { 143 | return nil, err 144 | } 145 | } 146 | 147 | transactionID := int64(2) // TODO: fix 148 | t, err := s.transactions.Create(transactionID) 149 | if err != nil { 150 | return nil, err 151 | } 152 | 153 | if body == nil { 154 | body = &message.NetConnectionCreateStream{} 155 | } 156 | 157 | chunkStreamID := 3 // TODO: fix 158 | err = s.writeCommandMessage( 159 | chunkStreamID, 0, // TODO: fix, Timestamp is 0 160 | "createStream", 161 | transactionID, 162 | body, 163 | ) 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | // TODO: support timeout 169 | // TODO: check result 170 | timeoutCtx := context.TODO() 171 | select { 172 | case <-timeoutCtx.Done(): 173 | return nil, timeoutCtx.Err() 174 | case <-t.doneCh: 175 | amfDec := message.NewAMFDecoder(t.body, t.encoding) 176 | 177 | var value message.AMFConvertible 178 | if err := message.DecodeBodyCreateStreamResult(t.body, amfDec, &value); err != nil { 179 | return nil, errors.Wrap(err, "Failed to decode result") 180 | } 181 | result := value.(*message.NetConnectionCreateStreamResult) 182 | 183 | if t.commandName == "_error" { 184 | return nil, &CreateStreamRejectedError{ 185 | TransactionID: transactionID, 186 | Result: result, 187 | } 188 | } 189 | 190 | return result, nil 191 | } 192 | 193 | //return nil, errors.New("Failed to get result") 194 | } 195 | 196 | func (s *Stream) DeleteStream(body *message.NetStreamDeleteStream) error { 197 | chunkStreamID := 3 // TODO: fix 198 | 199 | return s.writeCommandMessage( 200 | chunkStreamID, 201 | 0, 202 | "deleteStream", 203 | 0, 204 | body, 205 | ) 206 | } 207 | 208 | func (s *Stream) ReplyCreateStream( 209 | chunkStreamID int, 210 | timestamp uint32, 211 | transactionID int64, 212 | body *message.NetConnectionCreateStreamResult, 213 | ) error { 214 | commandName := "_result" 215 | if body == nil { 216 | commandName = "_error" 217 | body = &message.NetConnectionCreateStreamResult{ 218 | StreamID: 0, // TODO: Change to error information object... 219 | } 220 | } 221 | 222 | return s.writeCommandMessage( 223 | chunkStreamID, timestamp, 224 | commandName, 225 | transactionID, 226 | body, 227 | ) 228 | } 229 | 230 | func (s *Stream) Publish( 231 | body *message.NetStreamPublish, 232 | ) error { 233 | if body == nil { 234 | body = &message.NetStreamPublish{} 235 | } 236 | 237 | chunkStreamID := 3 // TODO: fix 238 | return s.writeCommandMessage( 239 | chunkStreamID, 0, // TODO: fix, Timestamp is 0 240 | "publish", 241 | int64(0), // Always 0, 7.2.2.6 242 | body, 243 | ) 244 | } 245 | 246 | func (s *Stream) NotifyStatus( 247 | chunkStreamID int, 248 | timestamp uint32, 249 | body *message.NetStreamOnStatus, 250 | ) error { 251 | return s.writeCommandMessage( 252 | chunkStreamID, timestamp, 253 | "onStatus", 254 | 0, // 7.2.2 255 | body, 256 | ) 257 | } 258 | 259 | func (s *Stream) Close() error { 260 | s.assumeClosed() 261 | return nil // TODO: implement 262 | } 263 | 264 | func (s *Stream) assumeClosed() { 265 | // TODO: implement 266 | } 267 | 268 | func (s *Stream) writeCommandMessage( 269 | chunkStreamID int, 270 | timestamp uint32, 271 | commandName string, 272 | transactionID int64, 273 | body message.AMFConvertible, 274 | ) error { 275 | buf := new(bytes.Buffer) 276 | amfEnc := message.NewAMFEncoder(buf, s.encTy) 277 | if err := message.EncodeBodyAnyValues(amfEnc, body); err != nil { 278 | return err 279 | } 280 | 281 | return s.Write(chunkStreamID, timestamp, &message.CommandMessage{ 282 | CommandName: commandName, 283 | TransactionID: transactionID, 284 | Encoding: s.encTy, 285 | Body: buf, 286 | }) 287 | } 288 | 289 | func (s *Stream) WriteDataMessage( 290 | chunkStreamID int, 291 | timestamp uint32, 292 | name string, 293 | body message.AMFConvertible, 294 | ) error { 295 | buf := new(bytes.Buffer) 296 | amfEnc := message.NewAMFEncoder(buf, message.EncodingTypeAMF0) 297 | if err := message.EncodeBodyAnyValues(amfEnc, body); err != nil { 298 | return err 299 | } 300 | 301 | return s.Write(chunkStreamID, timestamp, &message.DataMessage{ 302 | Name: name, 303 | Encoding: message.EncodingTypeAMF0, 304 | Body: buf, 305 | }) 306 | } 307 | 308 | func (s *Stream) WriteSetChunkSize(chunkSize uint32) error { 309 | if chunkSize < 1 { 310 | return errors.New("chunksize < 1") 311 | } 312 | if chunkSize > 0x7fffffff { 313 | return errors.New("chunksize > 0x7fffffff") 314 | } 315 | msg := &message.SetChunkSize{ChunkSize: chunkSize} 316 | chunkStreamID := 2 // Correct according to 6.2 317 | var timeStamp uint32 = 0 // TODO. Send updated time 318 | return s.Write(chunkStreamID, timeStamp, msg) 319 | } 320 | 321 | func (s *Stream) Write(chunkStreamID int, timestamp uint32, msg message.Message) error { 322 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) // TODO: Fix 5s 323 | defer cancel() 324 | 325 | s.cmsg.Message = msg 326 | return s.streamer().Write(ctx, chunkStreamID, timestamp, &s.cmsg) 327 | } 328 | 329 | func (s *Stream) handle(chunkStreamID int, timestamp uint32, msg message.Message) error { 330 | return s.handler.Handle(chunkStreamID, timestamp, msg) 331 | } 332 | 333 | func (s *Stream) streams() *streams { 334 | return s.conn.streams 335 | } 336 | 337 | func (s *Stream) streamer() *ChunkStreamer { 338 | return s.conn.streamer 339 | } 340 | 341 | func (s *Stream) userHandler() Handler { 342 | return s.conn.handler 343 | } 344 | 345 | func (s *Stream) logger() logrus.FieldLogger { 346 | return s.conn.logger 347 | } 348 | -------------------------------------------------------------------------------- /chunk_header_test.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package rtmp 9 | 10 | import ( 11 | "bytes" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestChunkBasicHeader(t *testing.T) { 18 | type testCase struct { 19 | name string 20 | value *chunkBasicHeader 21 | binary []byte 22 | } 23 | testCases := []testCase{ 24 | { 25 | name: "cs normal 1", 26 | value: &chunkBasicHeader{ 27 | fmt: 1, 28 | chunkStreamID: 2, 29 | }, 30 | binary: []byte{ 31 | // 0b01 : fmt = 1 32 | // 000010 : csID = 2 33 | 0x42, 34 | }, 35 | }, 36 | { 37 | name: "cs normal 2", 38 | value: &chunkBasicHeader{ 39 | fmt: 2, 40 | chunkStreamID: 63, 41 | }, 42 | binary: []byte{ 43 | // 0b10 : fmt = 2 44 | // 111111 : csID = 63 45 | 0xbf, 46 | }, 47 | }, 48 | 49 | { 50 | name: "cs medium 1", 51 | value: &chunkBasicHeader{ 52 | fmt: 0, 53 | chunkStreamID: 64, 54 | }, 55 | binary: []byte{ 56 | // 0b00 : fmt = 0 57 | // 000000 : csID(marker) = 0 58 | // 59 | 0x00, 60 | // 0b00000000 : csID = 0 = 64 - 64 61 | 0x00, 62 | }, 63 | }, 64 | { 65 | name: "cs medium 2", 66 | value: &chunkBasicHeader{ 67 | fmt: 1, 68 | chunkStreamID: 319, 69 | }, 70 | binary: []byte{ 71 | // 0b01 : fmt = 1 72 | // 000000 : csID(marker) = 0 73 | 0x40, 74 | // 0b11111111 : csID = 255 = 319 - 64 75 | 0xff, 76 | }, 77 | }, 78 | 79 | { 80 | name: "cs large 1", 81 | value: &chunkBasicHeader{ 82 | fmt: 3, 83 | chunkStreamID: 320, 84 | }, 85 | binary: []byte{ 86 | // 0b11 : fmt = 3 87 | // 000001 : csID(marker) = 0 88 | // 89 | 0xc1, 90 | // 0b00000000,00000001 : csID = 256 = 320 - 64 91 | 0x00, 0x01, 92 | }, 93 | }, 94 | { 95 | name: "cs large 2", 96 | value: &chunkBasicHeader{ 97 | fmt: 0, 98 | chunkStreamID: 65599, 99 | }, 100 | binary: []byte{ 101 | // 0b00 : fmt = 0 102 | // 000001 : csID(marker) = 1 103 | 0x01, 104 | // 0b11111111,11111111 : csID = 65535 = 65599 - 64 105 | 0xff, 0xff, 106 | }, 107 | }, 108 | } 109 | 110 | t.Run("Encode", func(t *testing.T) { 111 | for _, tc := range testCases { 112 | tc := tc // capture 113 | 114 | t.Run(tc.name, func(t *testing.T) { 115 | t.Parallel() 116 | 117 | buf := new(bytes.Buffer) 118 | err := encodeChunkBasicHeader(buf, tc.value) 119 | require.Nil(t, err) 120 | require.Equal(t, tc.binary, buf.Bytes()) 121 | }) 122 | } 123 | }) 124 | 125 | t.Run("Decode", func(t *testing.T) { 126 | for _, tc := range testCases { 127 | tc := tc // capture 128 | 129 | t.Run(tc.name, func(t *testing.T) { 130 | t.Parallel() 131 | 132 | r := bytes.NewReader(tc.binary) 133 | var mh chunkBasicHeader 134 | err := decodeChunkBasicHeader(r, nil, &mh) 135 | require.Nil(t, err) 136 | require.Equal(t, tc.value, &mh) 137 | }) 138 | } 139 | }) 140 | } 141 | 142 | func TestChunkBasicHeaderError(t *testing.T) { 143 | t.Run("Out of range(over)", func(t *testing.T) { 144 | buf := new(bytes.Buffer) 145 | err := encodeChunkBasicHeader(buf, &chunkBasicHeader{ 146 | fmt: 3, 147 | chunkStreamID: 65600, 148 | }) 149 | require.EqualError(t, err, "Chunk stream id is out of range: 65600 must be in range [2, 65599]") 150 | }) 151 | 152 | t.Run("Out of range(under)", func(t *testing.T) { 153 | buf := new(bytes.Buffer) 154 | err := encodeChunkBasicHeader(buf, &chunkBasicHeader{ 155 | fmt: 3, 156 | chunkStreamID: 1, 157 | }) 158 | require.EqualError(t, err, "Chunk stream id is out of range: 1 must be in range [2, 65599]") 159 | }) 160 | } 161 | 162 | func TestChunkMessageHeader(t *testing.T) { 163 | basic := &chunkMessageHeader{ 164 | timestamp: 10, 165 | timestampDelta: 10, 166 | messageLength: 10, 167 | messageTypeID: 10, 168 | messageStreamID: 10, 169 | } 170 | 171 | extendedBoundary := &chunkMessageHeader{ 172 | timestamp: 16777215, 173 | timestampDelta: 16777215, 174 | messageLength: 20, 175 | messageTypeID: 20, 176 | messageStreamID: 20, 177 | } 178 | 179 | extended := &chunkMessageHeader{ 180 | timestamp: 16777216, 181 | timestampDelta: 16777216, 182 | messageLength: 30, 183 | messageTypeID: 30, 184 | messageStreamID: 30, 185 | } 186 | 187 | type testCase struct { 188 | name string 189 | fmt byte 190 | value *chunkMessageHeader 191 | binary []byte 192 | } 193 | testCases := []testCase{ 194 | { 195 | name: "basic fmt 0", 196 | fmt: 0, 197 | value: &chunkMessageHeader{ 198 | timestamp: basic.timestamp, 199 | messageLength: basic.messageLength, 200 | messageTypeID: basic.messageTypeID, 201 | messageStreamID: basic.messageStreamID, 202 | }, 203 | binary: []byte{ 204 | // Timestamp 10(BigEndian, 24bits) 205 | 0x00, 0x00, 0x0a, 206 | // MessageLength 10(BigEndian, 24bits) 207 | 0x00, 0x00, 0x0a, 208 | // MessageTypeID 10(8bits) 209 | 0x0a, 210 | // MessageStreamID 10(*LittleEndian*, 32bits) 211 | 0x0a, 0x00, 0x00, 0x00, 212 | }, 213 | }, 214 | { 215 | name: "basic fmt 1", 216 | fmt: 1, 217 | value: &chunkMessageHeader{ 218 | timestampDelta: basic.timestampDelta, 219 | messageLength: basic.messageLength, 220 | messageTypeID: basic.messageTypeID, 221 | }, 222 | binary: []byte{ 223 | // Timestamp Delta 10(BigEndian, 24bits) 224 | 0x00, 0x00, 0x0a, 225 | // MessageLength 10(BigEndian, 24bits) 226 | 0x00, 0x00, 0x0a, 227 | // MessageTypeID 10(8bits) 228 | 0x0a, 229 | }, 230 | }, 231 | { 232 | name: "basic fmt 2", 233 | fmt: 2, 234 | value: &chunkMessageHeader{ 235 | timestampDelta: basic.timestampDelta, 236 | }, 237 | binary: []byte{ 238 | // Timestamp Delta 10(BigEndian, 24bits) 239 | 0x00, 0x00, 0x0a, 240 | }, 241 | }, 242 | 243 | { 244 | name: "extended boundary fmt 0", 245 | fmt: 0, 246 | value: &chunkMessageHeader{ 247 | timestamp: extendedBoundary.timestamp, 248 | messageLength: extendedBoundary.messageLength, 249 | messageTypeID: extendedBoundary.messageTypeID, 250 | messageStreamID: extendedBoundary.messageStreamID, 251 | }, 252 | binary: []byte{ 253 | // Timestamp MARKER(BigEndian, 24bits) 254 | 0xff, 0xff, 0xff, 255 | // MessageLength 20(BigEndian, 24bits) 256 | 0x00, 0x00, 0x14, 257 | // MessageTypeID 20(8bits) 258 | 0x14, 259 | // MessageStreamID 20(*LittleEndian*, 32bits) 260 | 0x14, 0x00, 0x00, 0x00, 261 | // ExtendTimestamp 16777215(BigEndian, 32bits) 262 | 0x00, 0xff, 0xff, 0xff, 263 | }, 264 | }, 265 | { 266 | name: "extended boundary fmt 1", 267 | fmt: 1, 268 | value: &chunkMessageHeader{ 269 | timestampDelta: extendedBoundary.timestampDelta, 270 | messageLength: extendedBoundary.messageLength, 271 | messageTypeID: extendedBoundary.messageTypeID, 272 | }, 273 | binary: []byte{ 274 | // Timestamp Delta MARKER(BigEndian, 24bits) 275 | 0xff, 0xff, 0xff, 276 | // MessageLength 20(BigEndian, 24bits) 277 | 0x00, 0x00, 0x14, 278 | // MessageTypeID 20(8bits) 279 | 0x14, 280 | // ExtendTimestamp Delta 16777215(BigEndian, 32bits) 281 | 0x00, 0xff, 0xff, 0xff, 282 | }, 283 | }, 284 | { 285 | name: "extended boundary fmt 2", 286 | fmt: 2, 287 | value: &chunkMessageHeader{ 288 | timestampDelta: extendedBoundary.timestampDelta, 289 | }, 290 | binary: []byte{ 291 | // Timestamp Delta MARKER(BigEndian, 24bits) 292 | 0xff, 0xff, 0xff, 293 | // ExtendTimestamp Delta 0(BigEndian, 32bits) 294 | 0x00, 0xff, 0xff, 0xff, 295 | }, 296 | }, 297 | 298 | { 299 | name: "extended fmt 0", 300 | fmt: 0, 301 | value: &chunkMessageHeader{ 302 | timestamp: extended.timestamp, 303 | messageLength: extended.messageLength, 304 | messageTypeID: extended.messageTypeID, 305 | messageStreamID: extended.messageStreamID, 306 | }, 307 | binary: []byte{ 308 | // Timestamp MARKER(BigEndian, 24bits) 309 | 0xff, 0xff, 0xff, 310 | // MessageLength 30(BigEndian, 24bits) 311 | 0x00, 0x00, 0x1e, 312 | // MessageTypeID 30(8bits) 313 | 0x1e, 314 | // MessageStreamID 30(*LittleEndian*, 32bits) 315 | 0x1e, 0x00, 0x00, 0x00, 316 | // ExtendTimestamp 16777216(BigEndian, 32bits) 317 | 0x01, 0x00, 0x00, 0x00, 318 | }, 319 | }, 320 | { 321 | name: "extended fmt 1", 322 | fmt: 1, 323 | value: &chunkMessageHeader{ 324 | timestampDelta: extended.timestampDelta, 325 | messageLength: extended.messageLength, 326 | messageTypeID: extended.messageTypeID, 327 | }, 328 | binary: []byte{ 329 | // Timestamp Delta MARKER(BigEndian, 24bits) 330 | 0xff, 0xff, 0xff, 331 | // MessageLength 30(BigEndian, 24bits) 332 | 0x00, 0x00, 0x1e, 333 | // MessageTypeID 30(8bits) 334 | 0x1e, 335 | // ExtendTimestamp Delta 16777216(BigEndian, 32bits) 336 | 0x01, 0x00, 0x00, 0x00, 337 | }, 338 | }, 339 | { 340 | name: "extended fmt 2", 341 | fmt: 2, 342 | value: &chunkMessageHeader{ 343 | timestampDelta: extended.timestampDelta, 344 | }, 345 | binary: []byte{ 346 | // Timestamp Delta MARKER(BigEndian, 24bits) 347 | 0xff, 0xff, 0xff, 348 | // ExtendTimestamp Delta 16777216(BigEndian, 32bits) 349 | 0x01, 0x00, 0x00, 0x00, 350 | }, 351 | }, 352 | 353 | { 354 | name: "fmt 3", 355 | fmt: 3, 356 | value: &chunkMessageHeader{}, 357 | binary: []byte(nil), 358 | }, 359 | } 360 | 361 | t.Run("Encode", func(t *testing.T) { 362 | for _, tc := range testCases { 363 | tc := tc // capture 364 | 365 | t.Run(tc.name, func(t *testing.T) { 366 | t.Parallel() 367 | 368 | buf := new(bytes.Buffer) 369 | err := encodeChunkMessageHeader(buf, tc.fmt, tc.value) 370 | require.Nil(t, err) 371 | require.Equal(t, tc.binary, buf.Bytes()) 372 | }) 373 | } 374 | }) 375 | 376 | t.Run("Decode", func(t *testing.T) { 377 | for _, tc := range testCases { 378 | tc := tc // capture 379 | 380 | t.Run(tc.name, func(t *testing.T) { 381 | t.Parallel() 382 | 383 | r := bytes.NewReader(tc.binary) 384 | var mh chunkMessageHeader 385 | err := decodeChunkMessageHeader(r, tc.fmt, nil, &mh) 386 | require.Nil(t, err) 387 | require.Equal(t, tc.value, &mh) 388 | }) 389 | } 390 | }) 391 | } 392 | -------------------------------------------------------------------------------- /message/body_decoder.go: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2018- yutopp (yutopp@gmail.com) 3 | // 4 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 5 | // file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) 6 | // 7 | 8 | package message 9 | 10 | import ( 11 | "bytes" 12 | "io" 13 | 14 | "github.com/pkg/errors" 15 | ) 16 | 17 | type BodyDecoderFunc func(r io.Reader, e AMFDecoder, v *AMFConvertible) error 18 | 19 | var DataBodyDecoders = map[string]BodyDecoderFunc{ 20 | "@setDataFrame": DecodeBodyAtSetDataFrame, 21 | } 22 | 23 | func DataBodyDecoderFor(name string) BodyDecoderFunc { 24 | dec, ok := DataBodyDecoders[name] 25 | if ok { 26 | return dec 27 | } 28 | 29 | return func(_ io.Reader, d AMFDecoder, _ *AMFConvertible) error { 30 | objs := make([]interface{}, 0) 31 | for { 32 | var tmp interface{} 33 | if err := d.Decode(&tmp); err != nil { 34 | break 35 | } 36 | objs = append(objs, tmp) 37 | } 38 | 39 | return &UnknownDataBodyDecodeError{ 40 | Name: name, 41 | Objs: objs, 42 | } 43 | } 44 | } 45 | 46 | func DecodeBodyAtSetDataFrame(r io.Reader, _ AMFDecoder, v *AMFConvertible) error { 47 | buf := new(bytes.Buffer) 48 | if _, err := io.Copy(buf, r); err != nil { 49 | return errors.Wrap(err, "Failed to decode '@setDataFrame' args[0]") 50 | } 51 | 52 | var cmd NetStreamSetDataFrame 53 | if err := cmd.FromArgs(buf.Bytes()); err != nil { 54 | return errors.Wrap(err, "Failed to reconstruct '@setDataFrame'") 55 | } 56 | 57 | *v = &cmd 58 | 59 | return nil 60 | } 61 | 62 | var CmdBodyDecoders = map[string]BodyDecoderFunc{ 63 | "connect": DecodeBodyConnect, 64 | "createStream": DecodeBodyCreateStream, 65 | "deleteStream": DecodeBodyDeleteStream, 66 | "publish": DecodeBodyPublish, 67 | "play": DecodeBodyPlay, 68 | "releaseStream": DecodeBodyReleaseStream, 69 | "FCPublish": DecodeBodyFCPublish, 70 | "FCUnpublish": DecodeBodyFCUnpublish, 71 | "getStreamLength": DecodeBodyGetStreamLength, 72 | "ping": DecodeBodyPing, 73 | "closeStream": DecodeBodyCloseStream, 74 | } 75 | 76 | func CmdBodyDecoderFor(name string, transactionID int64) BodyDecoderFunc { 77 | dec, ok := CmdBodyDecoders[name] 78 | if ok { 79 | return dec 80 | } 81 | 82 | // TODO: support result 83 | 84 | return func(_ io.Reader, d AMFDecoder, _ *AMFConvertible) error { 85 | objs := make([]interface{}, 0) 86 | for { 87 | var tmp interface{} 88 | if err := d.Decode(&tmp); err != nil { 89 | break 90 | } 91 | objs = append(objs, tmp) 92 | } 93 | 94 | return &UnknownCommandBodyDecodeError{ 95 | Name: name, 96 | TransactionID: transactionID, 97 | Objs: objs, 98 | } 99 | } 100 | } 101 | 102 | func DecodeBodyConnect(_ io.Reader, d AMFDecoder, v *AMFConvertible) error { 103 | var object map[string]interface{} 104 | if err := d.Decode(&object); err != nil { 105 | return errors.Wrap(err, "Failed to decode 'connect' args[0]") 106 | } 107 | 108 | var cmd NetConnectionConnect 109 | if err := cmd.FromArgs(object); err != nil { 110 | return errors.Wrap(err, "Failed to reconstruct 'connect'") 111 | } 112 | 113 | *v = &cmd 114 | return nil 115 | } 116 | 117 | func DecodeBodyConnectResult(_ io.Reader, d AMFDecoder, v *AMFConvertible) error { 118 | var properties interface{} 119 | if err := d.Decode(&properties); err != nil { 120 | return errors.Wrap(err, "Failed to decode 'connect.result' args[0]") 121 | } 122 | 123 | var information interface{} 124 | if err := d.Decode(&information); err != nil { 125 | return errors.Wrap(err, "Failed to decode 'connect.result' args[1]") 126 | } 127 | 128 | var result NetConnectionConnectResult 129 | if err := result.FromArgs(properties, information); err != nil { 130 | return errors.Wrap(err, "Failed to reconstruct 'connect.result'") 131 | } 132 | 133 | *v = &result 134 | return nil 135 | } 136 | 137 | func DecodeBodyCreateStream(_ io.Reader, d AMFDecoder, v *AMFConvertible) error { 138 | var object interface{} 139 | if err := d.Decode(&object); err != nil { 140 | return errors.Wrap(err, "Failed to decode 'createStream' args[0]") 141 | } 142 | 143 | var cmd NetConnectionCreateStream 144 | if err := cmd.FromArgs(object); err != nil { 145 | return errors.Wrap(err, "Failed to reconstruct 'createStream'") 146 | } 147 | 148 | *v = &cmd 149 | return nil 150 | } 151 | 152 | func DecodeBodyCreateStreamResult(_ io.Reader, d AMFDecoder, v *AMFConvertible) error { 153 | var commandObject interface{} // maybe nil 154 | if err := d.Decode(&commandObject); err != nil { 155 | return errors.Wrap(err, "Failed to decode 'createStream.result' args[0]") 156 | } 157 | 158 | var streamID uint32 159 | if err := d.Decode(&streamID); err != nil { 160 | return errors.Wrap(err, "Failed to decode 'createStream.result' args[1]") 161 | } 162 | 163 | var data NetConnectionCreateStreamResult 164 | if err := data.FromArgs(commandObject, streamID); err != nil { 165 | return errors.Wrap(err, "Failed to reconstruct 'createStream.result'") 166 | } 167 | 168 | *v = &data 169 | return nil 170 | } 171 | 172 | func DecodeBodyDeleteStream(_ io.Reader, d AMFDecoder, v *AMFConvertible) error { 173 | var commandObject interface{} // maybe nil 174 | if err := d.Decode(&commandObject); err != nil { 175 | return errors.Wrap(err, "Failed to decode 'deleteStream' args[0]") 176 | } 177 | 178 | var streamID uint32 179 | if err := d.Decode(&streamID); err != nil { 180 | return errors.Wrap(err, "Failed to decode 'deleteStream' args[1]") 181 | } 182 | 183 | var data NetStreamDeleteStream 184 | if err := data.FromArgs(commandObject, streamID); err != nil { 185 | return errors.Wrap(err, "Failed to reconstruct 'deleteStream'") 186 | } 187 | 188 | *v = &data 189 | return nil 190 | } 191 | 192 | func DecodeBodyPublish(_ io.Reader, d AMFDecoder, v *AMFConvertible) error { 193 | var commandObject interface{} 194 | if err := d.Decode(&commandObject); err != nil { 195 | return errors.Wrap(err, "Failed to decode 'publish' args[0]") 196 | } 197 | var publishingName string 198 | if err := d.Decode(&publishingName); err != nil { 199 | return errors.Wrap(err, "Failed to decode 'publish' args[1]") 200 | } 201 | var publishingType string 202 | if err := d.Decode(&publishingType); err != nil { 203 | // value is optional 204 | if errors.Is(err, io.EOF) { 205 | publishingType = "live" 206 | } else { 207 | return errors.Wrap(err, "Failed to decode 'publish' args[2]") 208 | } 209 | } 210 | 211 | var cmd NetStreamPublish 212 | if err := cmd.FromArgs(commandObject, publishingName, publishingType); err != nil { 213 | return errors.Wrap(err, "Failed to reconstruct 'publish'") 214 | } 215 | 216 | *v = &cmd 217 | return nil 218 | } 219 | 220 | func DecodeBodyPlay(_ io.Reader, d AMFDecoder, v *AMFConvertible) error { 221 | var commandObject interface{} 222 | if err := d.Decode(&commandObject); err != nil { 223 | return errors.Wrap(err, "Failed to decode 'play' args[0]") 224 | } 225 | var streamName string 226 | if err := d.Decode(&streamName); err != nil { 227 | return errors.Wrap(err, "Failed to decode 'play' args[1]") 228 | } 229 | var start int64 230 | if err := d.Decode(&start); err != nil { 231 | // 232 | // io.EOF occurs when the start position is not specified. 233 | // 'NetStream.play(streamName,null)' 234 | // set start to 0 to avoid it. 235 | // 236 | if err != io.EOF { 237 | return errors.Wrap(err, "Failed to decode 'play' args[2]") 238 | } 239 | start = 0 240 | } 241 | 242 | var cmd NetStreamPlay 243 | if err := cmd.FromArgs(commandObject, streamName, start); err != nil { 244 | return errors.Wrap(err, "Failed to reconstruct 'play'") 245 | } 246 | 247 | *v = &cmd 248 | return nil 249 | } 250 | 251 | func DecodeBodyReleaseStream(_ io.Reader, d AMFDecoder, v *AMFConvertible) error { 252 | var commandObject interface{} // maybe nil 253 | if err := d.Decode(&commandObject); err != nil { 254 | return errors.Wrap(err, "Failed to decode 'releaseStream' args[0]") 255 | } 256 | var streamName string 257 | if err := d.Decode(&streamName); err != nil { 258 | return errors.Wrap(err, "Failed to decode 'releaseStream' args[1]") 259 | } 260 | 261 | var cmd NetConnectionReleaseStream 262 | if err := cmd.FromArgs(commandObject, streamName); err != nil { 263 | return errors.Wrap(err, "Failed to reconstruct 'releaseStream'") 264 | } 265 | 266 | *v = &cmd 267 | return nil 268 | } 269 | 270 | func DecodeBodyFCPublish(_ io.Reader, d AMFDecoder, v *AMFConvertible) error { 271 | var commandObject interface{} // maybe nil 272 | if err := d.Decode(&commandObject); err != nil { 273 | return errors.Wrap(err, "Failed to decode 'FCPublish' args[0]") 274 | } 275 | var streamName string 276 | if err := d.Decode(&streamName); err != nil { 277 | return errors.Wrap(err, "Failed to decode 'FCPublish' args[1]") 278 | } 279 | 280 | var cmd NetStreamFCPublish 281 | if err := cmd.FromArgs(commandObject, streamName); err != nil { 282 | return errors.Wrap(err, "Failed to reconstruct 'FCPublish'") 283 | } 284 | 285 | *v = &cmd 286 | return nil 287 | } 288 | 289 | func DecodeBodyFCUnpublish(_ io.Reader, d AMFDecoder, v *AMFConvertible) error { 290 | var commandObject interface{} // maybe nil 291 | if err := d.Decode(&commandObject); err != nil { 292 | return errors.Wrap(err, "Failed to decode 'FCUnpublish' args[0]") 293 | } 294 | var streamName string 295 | if err := d.Decode(&streamName); err != nil { 296 | return errors.Wrap(err, "Failed to decode 'FCUnpublish' args[1]") 297 | } 298 | 299 | var cmd NetStreamFCUnpublish 300 | if err := cmd.FromArgs(commandObject, streamName); err != nil { 301 | return errors.Wrap(err, "Failed to reconstruct 'FCUnpublish'") 302 | } 303 | 304 | *v = &cmd 305 | return nil 306 | } 307 | 308 | func DecodeBodyGetStreamLength(_ io.Reader, d AMFDecoder, v *AMFConvertible) error { 309 | var commandObject interface{} // maybe nil 310 | if err := d.Decode(&commandObject); err != nil { 311 | return errors.Wrap(err, "Failed to decode 'getStreamLength' args[0]") 312 | } 313 | var streamName string 314 | if err := d.Decode(&streamName); err != nil { 315 | return errors.Wrap(err, "Failed to decode 'getStreamLength' args[1]") 316 | } 317 | 318 | var cmd NetStreamGetStreamLength 319 | if err := cmd.FromArgs(commandObject, streamName); err != nil { 320 | return errors.Wrap(err, "Failed to reconstruct 'getStreamLength'") 321 | } 322 | 323 | *v = &cmd 324 | return nil 325 | } 326 | 327 | func DecodeBodyPing(_ io.Reader, d AMFDecoder, v *AMFConvertible) error { // NLE 328 | var commandObject interface{} // maybe nil 329 | if err := d.Decode(&commandObject); err != nil { 330 | return errors.Wrap(err, "Failed to decode 'ping' args[0]") 331 | } 332 | 333 | var cmd NetStreamPing 334 | if err := cmd.FromArgs(commandObject); err != nil { 335 | return errors.Wrap(err, "Failed to reconstruct 'ping'") 336 | } 337 | 338 | *v = &cmd 339 | return nil 340 | } 341 | 342 | func DecodeBodyCloseStream(_ io.Reader, d AMFDecoder, v *AMFConvertible) error { 343 | var commandObject interface{} // maybe nil 344 | if err := d.Decode(&commandObject); err != nil { 345 | return errors.Wrap(err, "Failed to decode 'closeStream' args[0]") 346 | } 347 | 348 | var cmd NetStreamCloseStream 349 | if err := cmd.FromArgs(commandObject); err != nil { 350 | return errors.Wrap(err, "Failed to reconstruct 'closeStream'") 351 | } 352 | 353 | *v = &cmd 354 | 355 | return nil 356 | } 357 | --------------------------------------------------------------------------------