diff --git a/actioncam.go b/actioncam.go index 6260582..85af090 100644 --- a/actioncam.go +++ b/actioncam.go @@ -13,6 +13,7 @@ import ( "path/filepath" "github.com/jonas-koeritz/actioncam/libipcamera" + "github.com/jonas-koeritz/actioncam/rtsp" "github.com/spf13/cobra" ) @@ -140,6 +141,28 @@ func main() { }, } + var rtsp = &cobra.Command{ + Use: "rtsp [Cameras IP Address]", + Short: "Start an RTSP-Server serving the cameras preview.", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + + rtspServer := rtsp.CreateServer("127.0.0.1", 8554, camera) + log.Printf("Created RTSP Server\n") + err := rtspServer.ListenAndServe() + + if err != nil { + log.Printf("ERROR starting RTSP Server: %s\n", err) + } + }, + PreRun: func(cmd *cobra.Command, args []string) { + camera = connectAndLogin(net.ParseIP(args[0]), int(port), username, password, verbose) + }, + PostRun: func(cmd *cobra.Command, args []string) { + camera.Disconnect() + }, + } + var cmd = &cobra.Command{ Use: "cmd [RAW Command] [Cameras IP Address]", Short: "Send a raw command to the camera", @@ -201,6 +224,7 @@ func main() { rootCmd.AddCommand(fetch) rootCmd.AddCommand(record) rootCmd.AddCommand(firmware) + rootCmd.AddCommand(rtsp) if err := rootCmd.Execute(); err != nil { log.Println(err) diff --git a/libipcamera/RTPRelay.go b/libipcamera/RTPRelay.go index b376e8a..6b73fa2 100644 --- a/libipcamera/RTPRelay.go +++ b/libipcamera/RTPRelay.go @@ -14,11 +14,12 @@ type RTPRelay struct { close bool targetIP net.IP targetPort int + listener net.PacketConn } // CreateRTPRelay creates a UDP listener that handles live data // from the camera and forwards it as an RTP stream -func CreateRTPRelay(targetAddress net.IP, targetPort int) RTPRelay { +func CreateRTPRelay(targetAddress net.IP, targetPort int) *RTPRelay { conn, err := net.ListenPacket("udp", ":6669") if err != nil { @@ -28,6 +29,7 @@ func CreateRTPRelay(targetAddress net.IP, targetPort int) RTPRelay { relay := RTPRelay{ targetIP: targetAddress, targetPort: targetPort, + listener: conn, } if err != nil { log.Printf("ERROR: %s\n", err) @@ -35,7 +37,7 @@ func CreateRTPRelay(targetAddress net.IP, targetPort int) RTPRelay { go handleCameraStream(relay, conn) - return relay + return &relay } func handleCameraStream(relay RTPRelay, conn net.PacketConn) { @@ -47,8 +49,11 @@ func handleCameraStream(relay RTPRelay, conn net.PacketConn) { IP: relay.targetIP, Port: relay.targetPort, } - rtpSource, _ := net.ResolveUDPAddr("udp", "127.0.0.1:5000") - rtpConn, _ := net.DialUDP("udp", rtpSource, &rtpTarget) + rtpSource, _ := net.ResolveUDPAddr("udp", "127.0.0.1") + rtpConn, err := net.DialUDP("udp", rtpSource, &rtpTarget) + if err != nil { + log.Printf("ERROR creating RTP sender: %s\n", err) + } var sequenceNumber uint16 var elapsed uint32 @@ -56,6 +61,11 @@ func handleCameraStream(relay RTPRelay, conn net.PacketConn) { frameBuffer := []byte{} for { + if relay.close { + rtpConn.Close() + break + } + conn.ReadFrom(buffer) packetReader := bytes.NewReader(buffer) binary.Read(packetReader, binary.BigEndian, &header) @@ -94,13 +104,12 @@ func handleCameraStream(relay RTPRelay, conn net.PacketConn) { log.Printf("Received Unknown Message: %+v\n", header) log.Printf("Payload:\n%s\n", hex.Dump(payload)) } - if relay.close { - break - } } + rtpConn.Close() } // Stop stops listening for packets func (r *RTPRelay) Stop() { + r.listener.Close() r.close = true } diff --git a/rtsp/RTSPServer.go b/rtsp/RTSPServer.go new file mode 100644 index 0000000..52bff62 --- /dev/null +++ b/rtsp/RTSPServer.go @@ -0,0 +1,159 @@ +package rtsp + +import ( + "bufio" + "fmt" + "log" + "net" + "strconv" + "strings" + + "github.com/jonas-koeritz/actioncam/libipcamera" +) + +// Server implements the RTSP protocol to serve a H.264 stream +type Server struct { + localIP string + localPort int + listener net.Listener + remoteRTPPort int + remoteIP string + rtpRelay *libipcamera.RTPRelay + camera *libipcamera.Camera + sdp string +} + +// CreateServer creates a new Server instance +func CreateServer(localIP string, port int, camera *libipcamera.Camera) *Server { + server := &Server{ + localIP: localIP, + localPort: port, + camera: camera, + remoteRTPPort: 0, + remoteIP: "", + sdp: "v=0\r\ns=ActionCamera\r\nm=video 0 RTP/AVP 99\r\na=rtpmap:99 H264/90000", + } + return server +} + +// ListenAndServe starts listening for connections and handles them +func (s *Server) ListenAndServe() error { + log.Printf("%+v\n", *s) + listener, err := net.Listen("tcp4", fmt.Sprintf("%s:%d", s.localIP, s.localPort)) + if err != nil { + return err + } + s.listener = listener + + log.Printf("RTSP Server waiting for connections on %s:%d\n", s.localIP, s.localPort) + + for { + conn, err := listener.Accept() + if err != nil { + log.Printf("ERROR accepting connection: %s\n", err) + } + + log.Printf("Accepted new RTSP Client %s\n", conn.RemoteAddr().String()) + + go s.handleClient(conn) + } +} + +func (s *Server) handleClient(conn net.Conn) error { + packet := make([]string, 0) + scanner := bufio.NewScanner(conn) + for scanner.Scan() { + line := scanner.Text() + if len(line) > 0 { + packet = append(packet, line) + } else { + s.handleRequest(packet, conn) + packet = make([]string, 0) + } + } + return nil +} + +func (s *Server) handleRequest(packet []string, conn net.Conn) { + fmt.Printf("C->S:\n%s\n", packet) + + request := strings.Split(packet[0], " ") + if len(request) != 3 { + log.Printf("Received invalid request") + return + } + + method := request[0] + headers := make(map[string]string, 0) + for _, header := range packet[1:] { + parts := strings.Split(header, ":") + if len(parts) >= 2 { + headers[parts[0]] = strings.Join(parts[1:], ":") + } + } + + switch method { + case "OPTIONS": + writeStatus(conn, 200, "OK") + replyCSeq(conn, headers) + conn.Write([]byte("Public: DESCRIBE, SETUP, PLAY, PAUSE, RECORD\r\n\r\n")) + case "DESCRIBE": + writeStatus(conn, 200, "OK") + replyCSeq(conn, headers) + writeHeader(conn, "Content-Type", "application/sdp") + writeHeader(conn, "Content-Length", fmt.Sprintf("%d", len(s.sdp))) + conn.Write([]byte(fmt.Sprintf("\r\n%s\r\n", s.sdp))) + case "SETUP": + transportDescription := strings.Split(headers["Transport"], ";") + rtpDescription := transportDescription[len(transportDescription)-1] + remoteRTPPort, err := strconv.ParseInt(strings.Split(strings.Split(rtpDescription, "=")[1], "-")[0], 10, 32) + if err != nil { + log.Printf("ERROR Parsing RTP description: %s\n", err) + return + } + s.remoteRTPPort = int(remoteRTPPort) + s.remoteIP = (conn.RemoteAddr().(*net.TCPAddr)).IP.String() + + log.Printf("Preparing to Stream to %s:%d\n", s.remoteIP, s.remoteRTPPort) + + writeStatus(conn, 200, "OK") + replyCSeq(conn, headers) + writeHeader(conn, "Transport", headers["Transport"]+";ssrc=0") + writeHeader(conn, "Session", "1") + conn.Write([]byte("\r\n")) + + case "PLAY": + s.rtpRelay = libipcamera.CreateRTPRelay(net.ParseIP(s.remoteIP), s.remoteRTPPort) + s.camera.StartPreviewStream() + + writeStatus(conn, 200, "OK") + replyCSeq(conn, headers) + writeHeader(conn, "Session", "1") + writeHeader(conn, "RTP-Info", "url="+request[1]+";seq=0;rtptime=0") + conn.Write([]byte("\r\n")) + case "TEARDOWN": + s.rtpRelay.Stop() + writeStatus(conn, 200, "OK") + replyCSeq(conn, headers) + conn.Write([]byte("\r\n")) + default: + return + } +} + +func writeStatus(conn net.Conn, status int, statusWord string) { + conn.Write([]byte(fmt.Sprintf("RTSP/1.0 %d %s\r\n", status, statusWord))) +} + +func replyCSeq(conn net.Conn, headers map[string]string) { + writeHeader(conn, "CSeq", headers["CSeq"]) +} + +func writeHeader(conn net.Conn, key, value string) { + conn.Write([]byte(fmt.Sprintf("%s: %s\r\n", key, value))) +} + +// Stop stops listening for connections +func (s *Server) Stop() { + s.listener.Close() +}