//go:build !js // +build !js package main import ( "context" "fmt" "io" "log" "net/http" "os" "time" "connectrpc.com/connect" pb "github.com/chathaway-codes/home-sensors/v2/gen" servicepb "github.com/chathaway-codes/home-sensors/v2/gen/genconnect" "github.com/pion/webrtc/v3" "github.com/pion/webrtc/v3/pkg/media" "github.com/pion/webrtc/v3/pkg/media/h264reader" "google.golang.org/protobuf/proto" ) const ( videoFileName = "/home/charles/Downloads/simpsons_movie_1080p_hddvd_trailer/The Simpsons Movie - 1080p Trailer.mp4" oggPageDuration = time.Millisecond * 20 h264FrameDuration = time.Millisecond * 33 ) func withAuth[T any](token string, v *T) *connect.Request[T] { req := connect.NewRequest[T](v) req.Header().Add("authorization", "Bearer "+token) return req } func main() { //nolint ctx := context.Background() httpClient := &http.Client{} client := servicepb.NewSignalerServiceClient(httpClient, "http://localhost:8080/") authToken, err := client.CreateAuthToken(ctx, connect.NewRequest(&pb.CreateAuthTokenRequest{ Type: &pb.CreateAuthTokenRequest_Camera_{ Camera: &pb.CreateAuthTokenRequest_Camera{ Id: "movie", }, }, })) if err != nil { log.Fatalf("Failed to get auth token: %v", err) } token := authToken.Msg.GetToken() // Assert that we have an audio or video file _, err = os.Stat(videoFileName) haveVideoFile := !os.IsNotExist(err) iceConnectedCtx, iceConnectedCtxCancel := context.WithCancel(context.Background()) // Create a new RTCPeerConnection // Wait for a session request session, err := client.PopSession(ctx, withAuth(token, &pb.PopSessionRequest{})) if err != nil { log.Fatalf("error creating session: %v", err) } peerConnection, err := webrtc.NewPeerConnection(webrtc.Configuration{ ICEServers: []webrtc.ICEServer{ { URLs: []string{"stun:stun.l.google.com:19302"}, }, }, }) if err != nil { panic(err) } defer func() { if cErr := peerConnection.Close(); cErr != nil { fmt.Printf("cannot close peerConnection: %v\n", cErr) } }() if haveVideoFile { // Create a video track videoTrack, videoTrackErr := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264}, "video", "pion") if videoTrackErr != nil { panic(videoTrackErr) } rtpSender, videoTrackErr := peerConnection.AddTrack(videoTrack) if videoTrackErr != nil { panic(videoTrackErr) } // Read incoming RTCP packets // Before these packets are returned they are processed by interceptors. For things // like NACK this needs to be called. go func() { rtcpBuf := make([]byte, 1500) for { if _, _, rtcpErr := rtpSender.Read(rtcpBuf); rtcpErr != nil { return } } }() go func() { // Open a H264 file and start reading using our IVFReader file, h264Err := os.Open(videoFileName) if h264Err != nil { panic(h264Err) } h264, h264Err := h264reader.NewReader(file) if h264Err != nil { panic(h264Err) } // Wait for connection established <-iceConnectedCtx.Done() // Send our video file frame at a time. Pace our sending so we send it at the same speed it should be played back as. // This isn't required since the video is timestamped, but we will such much higher loss if we send all at once. // // It is important to use a time.Ticker instead of time.Sleep because // * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data // * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343) ticker := time.NewTicker(h264FrameDuration) for ; true; <-ticker.C { nal, h264Err := h264.NextNAL() if h264Err == io.EOF { fmt.Printf("All video frames parsed and sent") os.Exit(0) } if h264Err != nil { panic(h264Err) } if h264Err = videoTrack.WriteSample(media.Sample{Data: nal.Data, Duration: h264FrameDuration}); h264Err != nil { panic(h264Err) } } }() } // Set the handler for ICE connection state // This will notify you when the peer has connected/disconnected peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { fmt.Printf("Connection State has changed %s \n", connectionState.String()) if connectionState == webrtc.ICEConnectionStateConnected { iceConnectedCtxCancel() } }) // Set the handler for Peer connection state // This will notify you when the peer has connected/disconnected peerConnection.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { fmt.Printf("Peer Connection State has changed: %s\n", s.String()) if s == webrtc.PeerConnectionStateFailed { // Wait until PeerConnection has had no network activity for 30 seconds or another failure. It may be reconnected using an ICE Restart. // Use webrtc.PeerConnectionStateDisconnected if you are interested in detecting faster timeout. // Note that the PeerConnection may come back from PeerConnectionStateDisconnected. fmt.Println("Peer Connection has gone to failed exiting") os.Exit(0) } }) peerConnection.OnICECandidate(func(i *webrtc.ICECandidate) { if i == nil { return } c := i.ToJSON() client.CreateIceMessage(ctx, withAuth(token, &pb.CreateIceMessageRequest{ SessionIdentifier: session.Msg.GetId(), IceMessage: &pb.IceMessage{ Type: &pb.IceMessage_Candidate{ Candidate: &pb.IceCandidate{ Candidate: c.Candidate, SdpMid: c.SDPMid, SdpLineIndex: proto.Int32(int32(*c.SDPMLineIndex)), UsernameFragment: proto.String(*c.UsernameFragment), }, }, }, })) }) // Add ICE candidates from remote go func() { for { msg, err := client.PopIceMessage(ctx, withAuth(token, &pb.PopIceMessageRequest{ SessionIdentifier: session.Msg.GetId(), })) if err != nil { log.Fatalf("failed to pop ice message: %v", err) } switch msg.Msg.Type.(type) { case *pb.IceMessage_Candidate: candidate := msg.Msg.GetCandidate() var sdpMLine *uint16 if candidate.SdpLineIndex != nil { t := uint16(candidate.GetSdpLineIndex()) sdpMLine = &t } if err := peerConnection.AddICECandidate(webrtc.ICECandidateInit{ Candidate: candidate.GetCandidate(), SDPMid: candidate.SdpMid, SDPMLineIndex: sdpMLine, }); err != nil { log.Fatalf("Failed to add ice candidate: %v", err) } case *pb.IceMessage_Session: session := msg.Msg.GetSession() offer := webrtc.SessionDescription{ Type: webrtc.SDPType(session.SdpType), SDP: session.Sdp, } if err := peerConnection.SetLocalDescription(offer); err != nil { log.Fatalf("Failed to set location description: %v", err) } // Send back an answer answer, err := peerConnection.CreateAnswer(nil) if err != nil { log.Fatalf("Failed to create an answer: %v", err) } if err := peerConnection.SetRemoteDescription(answer); err != nil { log.Fatalf("Failed to set remote description: %v", err) } _, err = client.CreateIceMessage(ctx, withAuth(token, &pb.CreateIceMessageRequest{ IceMessage: &pb.IceMessage{ Type: &pb.IceMessage_Session{ Session: &pb.IceSessionDescription{ SdpType: int64(answer.Type), Sdp: answer.SDP, }, }, }, })) if err != nil { log.Fatalf("Failed to send answer: %v", err) } } } }() // Block forever select {} }