diff --git a/easydarwin.ini b/easydarwin.ini index 3dc044c1..a8839edf 100644 --- a/easydarwin.ini +++ b/easydarwin.ini @@ -5,8 +5,9 @@ default_password=admin [rtsp] port=554 -timeout=28800 -gop_cache_enable=1 -save_stream_to_mp4=0 -ffmpeg_path=/Users/jiaozebo/Downloads/ffmpeg-20180719-9cb3d8f-macos64-shared/bin/ffmpeg -mp4_dir_path=/Users/jiaozebo/Downloads/EasyDarwinGoMP4 \ No newline at end of file +timeout=28800; rtsp 超时时间,包括RTSP建立连接与数据收发。 +gop_cache_enable=1; 是否使能gop cache。如果使能,服务器会缓存最后一个I帧以及其后的非I帧,以提高播放速度。但是可能在高并发的情况下带来内存压力。 +save_stream_to_local=1; 是否使能推送的同事进行本地存储,使能后则可以进行录像查询与回放。 +ffmpeg_path=~/Downloads/ffmpeg-20180719-9cb3d8f-macos64-shared/bin/ffmpeg;easydarwin使用ffmpeg工具来进行存储。这里表示ffmpeg的可执行程序的路径 +m3u8_dir_path=~/Downloads/EasyDarwinGoM3u8;本地存储所将要保存的根目录。如果不存在,程序会尝试创建该目录。 +ts_duration_second=600;切片文件时长。本地存储时,将以该时间段为标准来生成ts文件,单位秒 \ No newline at end of file diff --git a/routers/routers.go b/routers/routers.go index a6275b10..f2724993 100644 --- a/routers/routers.go +++ b/routers/routers.go @@ -145,7 +145,7 @@ func Init() (err error) { { - mp4Path := utils.Conf().Section("rtsp").Key("mp4_dir_path").MustString("") + mp4Path := utils.Conf().Section("rtsp").Key("m3u8_dir_path").MustString("") if len(mp4Path) != 0 { Router.Use(static.Serve("/record", static.LocalFile(mp4Path, true))) } diff --git a/routers/streams.go b/routers/streams.go index 9766a358..1c4c072e 100644 --- a/routers/streams.go +++ b/routers/streams.go @@ -19,6 +19,20 @@ import ( "github.com/penggy/EasyGoLib/utils" ) +/** + * @apiDefine pull 拉流转推 + */ + +/** + * @api {get} /api/vi/stream/start 启动拉流 + * @apiGroup pull + * @apiName StreamStart + * @apiParam {String} url RTSP源地址 + * @apiParam {String} [customPath] 转推时的推送PATH + * @apiParam {Number} [IdleTimeout] 拉流时的超时时间 + * @apiParam {Number} [HeartbeatInterval] 拉流时的心跳间隔,毫秒为单位。如果心跳间隔不为0,那拉流时会向源地址以该间隔发送OPTION请求用来心跳保活 + * @apiSuccess (200) {String} ID 拉流的ID。后续可以通过该ID来停止拉流 + */ func (h *APIHandler) StreamStart(c *gin.Context) { type Form struct { URL string `form:"url" binding:"required"` @@ -58,6 +72,13 @@ func (h *APIHandler) StreamStart(c *gin.Context) { c.IndentedJSON(200, pusher.ID()) } +/** + * @api {get} /api/vi/stream/stop 停止拉流 + * @apiGroup pull + * @apiName StreamStop + * @apiParam {String} id 拉流的ID + * @apiUse simpleSuccess + */ func (h *APIHandler) StreamStop(c *gin.Context) { type Form struct { ID string `form:"id" binding:"required"` @@ -80,8 +101,33 @@ func (h *APIHandler) StreamStop(c *gin.Context) { c.AbortWithStatusJSON(http.StatusBadRequest, fmt.Sprintf("Pusher[%s] not found", form.ID)) } +/** + * @apiDefine record 录像 Record + */ + +/** + * @apiDefine fileInfo + * @apiSuccess (200) {String} duration 格式化好的录像时长 + * @apiSuccess (200) {Number} durationMillis 录像时长,毫秒为单位 + * @apiSuccess (200) {String} path 录像文件的相对路径,其绝对路径为:http[s]://host:port/record/[path]。 + * @apiSuccess (200) {String} folder 录像文件夹,录像文件夹以推流路径命名。 + */ + +/** + * @api {get} /api/vi/record/folders 获取所有录像文件夹 + * @apiGroup record + * @apiName RecordFolders + * @apiParam {Number} [start] 分页开始,从零开始 + * @apiParam {Number} [limit] 分页大小 + * @apiParam {String} [sort] 排序字段 + * @apiParam {String=ascending,descending} [order] 排序顺序 + * @apiParam {String} [q] 查询参数 + * @apiSuccess (200) {Number} total 总数 + * @apiSuccess (200) {Array} rows 文件夹列表 + * @apiSuccess (200) {String} rows.folder 录像文件夹名称 + */ func (h *APIHandler) RecordFolders(c *gin.Context) { - mp4Path := utils.Conf().Section("rtsp").Key("mp4_dir_path").MustString("") + mp4Path := utils.Conf().Section("rtsp").Key("m3u8_dir_path").MustString("") form := utils.NewPageForm() if err := c.Bind(form); err != nil { log.Printf("record folder bind err:%v", err) @@ -118,6 +164,22 @@ func (h *APIHandler) RecordFolders(c *gin.Context) { } +/** + * @api {get} /api/vi/record/files 获取所有录像文件 + * @apiGroup record + * @apiName RecordFiles + * @apiParam {Number} folder 录像文件所在的文件夹 + * @apiParam {Number} [start] 分页开始,从零开始 + * @apiParam {Number} [limit] 分页大小 + * @apiParam {String} [sort] 排序字段 + * @apiParam {String=ascending,descending} [order] 排序顺序 + * @apiParam {String} [q] 查询参数 + * @apiSuccess (200) {Number} total 总数 + * @apiSuccess (200) {Array} rows 文件列表 + * @apiSuccess (200) {String} rows.duration 格式化好的录像时长 + * @apiSuccess (200) {Number} rows.durationMillis 录像时长,毫秒为单位 + * @apiSuccess (200) {String} rows.path 录像文件的相对路径,录像文件为m3u8格式,将其放到video标签中便可直接播放。其绝对路径为:http[s]://host:port/record/[path]。 + */ func (h *APIHandler) RecordFiles(c *gin.Context) { type Form struct { utils.PageForm @@ -134,7 +196,7 @@ func (h *APIHandler) RecordFiles(c *gin.Context) { } files := make([]interface{}, 0) - mp4Path := utils.Conf().Section("rtsp").Key("mp4_dir_path").MustString("") + mp4Path := utils.Conf().Section("rtsp").Key("m3u8_dir_path").MustString("") if mp4Path != "" { ffmpeg_path := utils.Conf().Section("rtsp").Key("ffmpeg_path").MustString("") ffmpeg_folder, executable := filepath.Split(ffmpeg_path) @@ -162,6 +224,9 @@ func (h *APIHandler) RecordFiles(c *gin.Context) { if info.Name() == ".DS_Store" { return nil } + if !strings.HasSuffix(info.Name(), ".m3u8") { + return filepath.SkipDir + } cmd := exec.Command(ffprobe, "-i", path) cmdOutput := &bytes.Buffer{} //cmd.Stdout = cmdOutput @@ -185,8 +250,9 @@ func (h *APIHandler) RecordFiles(c *gin.Context) { millis, _ := strconv.Atoi(result[5]) duration += time.Duration(millis) * time.Millisecond } + *files = append(*files, map[string]interface{}{ - "name": info.Name(), + "path": path[len(mp4Path):], "durationMillis": duration / time.Millisecond, "duration": durationStr}) return nil diff --git a/rtsp/rtsp-client.go b/rtsp/rtsp-client.go index fb86c951..7300136e 100644 --- a/rtsp/rtsp-client.go +++ b/rtsp/rtsp-client.go @@ -119,7 +119,7 @@ func (client *RTSPClient) Start(timeout time.Duration) error { } client.Conn = conn - networkBuffer := utils.Conf().Section("rtsp").Key("network_buffer").MustInt(1048576) + networkBuffer := utils.Conf().Section("rtsp").Key("network_buffer").MustInt(204800) timeoutConn := RichConn{ conn, diff --git a/rtsp/rtsp-server.go b/rtsp/rtsp-server.go index ea6ba50a..8a64414a 100644 --- a/rtsp/rtsp-server.go +++ b/rtsp/rtsp-server.go @@ -2,16 +2,16 @@ package rtsp import ( "fmt" + "github.com/penggy/EasyGoLib/utils" "log" "net" "os" "os/exec" "path" + "strconv" "sync" "syscall" "time" - - "github.com/penggy/EasyGoLib/utils" ) type Server struct { @@ -49,14 +49,15 @@ func (server *Server) Start() (err error) { return } - localRecord := utils.Conf().Section("rtsp").Key("save_stream_to_mp4").MustInt(0) + localRecord := utils.Conf().Section("rtsp").Key("save_stream_to_local").MustInt(0) ffmpeg := utils.Conf().Section("rtsp").Key("ffmpeg_path").MustString("") - mp4Path := utils.Conf().Section("rtsp").Key("mp4_dir_path").MustString("") + m3u8_dir_path := utils.Conf().Section("rtsp").Key("m3u8_dir_path").MustString("") + ts_duration_second := utils.Conf().Section("rtsp").Key("ts_duration_second").MustInt(10 * 60) SaveStreamToLocal := false - if (len(ffmpeg) > 0) && localRecord > 0 && len(mp4Path) > 0 { - err := utils.EnsureDir(mp4Path) + if (len(ffmpeg) > 0) && localRecord > 0 && len(m3u8_dir_path) > 0 { + err := utils.EnsureDir(m3u8_dir_path) if err != nil { - log.Printf("Create mp4_dir_path[%s] err:%v.", mp4Path, err) + log.Printf("Create m3u8_dir_path[%s] err:%v.", m3u8_dir_path, err) } else { SaveStreamToLocal = true } @@ -75,14 +76,16 @@ func (server *Server) Start() (err error) { case pusher, addChnOk = <-server.addPusherCh: if SaveStreamToLocal { if addChnOk { - dir := path.Join(mp4Path, pusher.Path()) + dir := path.Join(m3u8_dir_path, pusher.Path(), time.Now().Format("20060102150405")) err := utils.EnsureDir(dir) if err != nil { log.Printf("EnsureDir:[%s] err:%v.", dir, err) continue } - path := path.Join(dir, fmt.Sprintf("%s.mp4", time.Now().Format("20060102150405"))) - cmd := exec.Command(ffmpeg, "-i", pusher.URL(), "-c:v", "copy", "-c:a", "copy", path) + path := path.Join(dir, fmt.Sprintf("out.m3u8")) + // ffmpeg -i ~/Downloads/720p.mp4 -s 640x360 -g 15 -c:a aac -hls_time 5 -hls_list_size 0 record.m3u8 + + cmd := exec.Command(ffmpeg, "-fflags", "genpts", "-rtsp_transport", "tcp", "-i", pusher.URL(), "-c:v", "copy", "-hls_time", strconv.Itoa(ts_duration_second), "-hls_list_size", "0", path) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr err = cmd.Start() @@ -90,7 +93,7 @@ func (server *Server) Start() (err error) { log.Printf("Start ffmpeg err:%v", err) } pusher2ffmpegMap[pusher] = cmd - log.Printf("add ffmpeg to pull stream from pusher[%v]", pusher) + log.Printf("add ffmpeg [%v] to pull stream from pusher[%v]", cmd, pusher) } else { log.Printf("addPusherChan closed") } diff --git a/rtsp/rtsp-session.go b/rtsp/rtsp-session.go index d0320344..eda4bff9 100644 --- a/rtsp/rtsp-session.go +++ b/rtsp/rtsp-session.go @@ -128,7 +128,7 @@ func (session *Session) String() string { } func NewSession(server *Server, conn net.Conn) *Session { - networkBuffer := utils.Conf().Section("rtsp").Key("network_buffer").MustInt(1048576) + networkBuffer := utils.Conf().Section("rtsp").Key("network_buffer").MustInt(204800) timeoutMillis := utils.Conf().Section("rtsp").Key("timeout").MustInt(0) timeoutTCPConn := &RichConn{conn, time.Duration(timeoutMillis) * time.Millisecond}