From 05f186bd6c61ac7a96a00323d74f84f53b900996 Mon Sep 17 00:00:00 2001 From: zijiren233 Date: Mon, 22 Jan 2024 00:42:19 +0800 Subject: [PATCH] Fix: play hls in safari --- go.mod | 2 +- go.sum | 2 + script/build.sh | 20 +++-- server/handlers/init.go | 13 +++- server/handlers/movie.go | 155 +++++++++++++++++++++++++++++++------ server/middlewares/auth.go | 40 +++++++++- 6 files changed, 194 insertions(+), 38 deletions(-) diff --git a/go.mod b/go.mod index bedea2b..bee80b6 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/zencoder/go-dash/v3 v3.0.3 github.com/zijiren233/gencontainer v0.0.0-20240108151314-9632e4fe47e7 github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb - github.com/zijiren233/livelib v0.3.0 + github.com/zijiren233/livelib v0.3.1 github.com/zijiren233/stream v0.5.1 github.com/zijiren233/yaml-comment v0.2.1 go.etcd.io/etcd/client/v3 v3.5.11 diff --git a/go.sum b/go.sum index 7005941..59b3b04 100644 --- a/go.sum +++ b/go.sum @@ -362,6 +362,8 @@ github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb h1:0DyOxf/ github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb/go.mod h1:6TCzjDiQ8+5gWZiwsC3pnA5M0vUy2jV2Y7ciHJh729g= github.com/zijiren233/livelib v0.3.0 h1:RLfx6vSUr7wfK6F0VY0aFOBDcT/ljvjQUTOlHEcUlUQ= github.com/zijiren233/livelib v0.3.0/go.mod h1:2wrAAqNIdMZjQrdbO7ERQfqK4VS5fzgUj2xXwrJ8/uo= +github.com/zijiren233/livelib v0.3.1 h1:vNGQFeVyk1qrXTO/lqyRs0oC6cLzMD6yo2Jdym3XNpI= +github.com/zijiren233/livelib v0.3.1/go.mod h1:2wrAAqNIdMZjQrdbO7ERQfqK4VS5fzgUj2xXwrJ8/uo= github.com/zijiren233/stream v0.5.1 h1:9SUwM/fpET6frtBRT5WZBHnan0Hyzkezk/P8N78cgZQ= github.com/zijiren233/stream v0.5.1/go.mod h1:iIrOm3qgIepQFmptD/HDY+YzamSSzQOtPjpVcK7FCOw= github.com/zijiren233/yaml-comment v0.2.1 h1:/ymMfauuR6zPme+c59FvGNmvxmjOS+BRZSU9YEM82g4= diff --git a/script/build.sh b/script/build.sh index 4f61ec7..f798e0f 100755 --- a/script/build.sh +++ b/script/build.sh @@ -27,6 +27,7 @@ function Help() { echo "-v set build version (default: dev)" echo "-S set source dir (default: ../)" echo "-m set build mode (default: pie)" + echo "-M disable build micro" echo "-l set ldflags (default: -s -w --extldflags \"-static -fpic\")" echo "-p set platform (default: host platform, support: all, linux, darwin, windows)" echo "-P set disable trim path (default: disable)" @@ -77,7 +78,7 @@ function Init() { } function ParseArgs() { - while getopts "hCsS:v:w:m:l:p:Pd:T:tm" arg; do + while getopts "hCsS:v:w:m:l:p:Pd:T:tmM" arg; do case $arg in h) Help @@ -116,6 +117,9 @@ function ParseArgs() { m) GH_PROXY="https://mirror.ghproxy.com/" ;; + M) + DISABLE_MICRO="true" + ;; # ---- # dep s) @@ -898,7 +902,6 @@ function InitLinuxAmd64CGODeps() { function Build() { platform="$1" target_name="$2" - disable_micro="$3" GOOS=${platform%/*} GOARCH=${platform#*/} @@ -946,10 +949,17 @@ function Build() { GOOS=$GOOS \ GOARCH=$GOARCH" - if [ "$disable_micro" ]; then + if [ "$DISABLE_MICRO" ]; then echo "building $GOOS/$GOARCH" InitCGODeps "$GOOS" "$GOARCH" - eval "$BUILD_ENV CC=\"$CC\" CXX=\"$CXX\" go build $BUILD_FLAGS -o \"$TARGET_FILE$EXT\" \"$SOURCH_DIR\"" + eval "$BUILD_ENV CC=\"$CC\" CXX=\"$CXX\" \ + GO386=sse2 \ + GOARM=6 \ + GOAMD64=v1 \ + GOMIPS=hardfloat GOMIPS64=hardfloat \ + GOPPC64=power8 \ + GOWASM= \ + go build $BUILD_FLAGS -o \"$TARGET_FILE$EXT\" \"$SOURCH_DIR\"" if [ $? -ne 0 ]; then echo "build $GOOS/$GOARCH failed" exit 1 @@ -1182,7 +1192,7 @@ function Build() { function AutoBuild() { if [ ! "$1" ]; then echo "build host platform: $GOHOSTOS/$GOHOSTARCH" - Build "$GOHOSTOS/$GOHOSTARCH" "$BIN_NAME" "disable_micro" + Build "$GOHOSTOS/$GOHOSTARCH" "$BIN_NAME" else for platform in $1; do if [ "$platform" == "all" ]; then diff --git a/server/handlers/init.go b/server/handlers/init.go index 58ff50a..4818f2b 100644 --- a/server/handlers/init.go +++ b/server/handlers/init.go @@ -186,11 +186,18 @@ func initMovie(movie *gin.RouterGroup, needAuthMovie *gin.RouterGroup) { movie.GET("/proxy/:roomId/:movieId", ProxyMovie) { - live := needAuthMovie.Group("/live") + live := movie.Group("/live") + needAuthLive := needAuthMovie.Group("/live") - live.POST("/publishKey", NewPublishKey) + needAuthLive.POST("/publishKey", NewPublishKey) - live.GET("/*movieId", JoinLive) + // needAuthLive.GET("/join/:movieId", JoinLive) + + needAuthLive.GET("/flv/:movieId", JoinFlvLive) + + needAuthLive.GET("/hls/list/:movieId", JoinHlsLive) + + live.GET("/hls/data/:roomId/:movieId/:dataId", ServeHlsLive) } } diff --git a/server/handlers/movie.go b/server/handlers/movie.go index 6d80195..1dfdd79 100644 --- a/server/handlers/movie.go +++ b/server/handlers/movie.go @@ -12,7 +12,7 @@ import ( "math/rand" "net/http" "net/url" - "path" + "path/filepath" "strconv" "strings" @@ -85,11 +85,14 @@ func genCurrent(ctx context.Context, user *op.User, room *op.Room, current *op.C return parse2VendorMovie(ctx, user, room, ¤t.Movie) } if current.Movie.Base.RtmpSource || current.Movie.Base.Live && current.Movie.Base.Proxy { - t := current.Movie.Base.Type - if t != "flv" && t != "m3u8" { - t = "m3u8" + switch current.Movie.Base.Type { + case "m3u8": + current.Movie.Base.Url = fmt.Sprintf("/api/movie/live/hls/list/%s.m3u8", current.Movie.ID) + case "flv": + current.Movie.Base.Url = fmt.Sprintf("/api/movie/live/flv/%s.flv", current.Movie.ID) + default: + return errors.New("not support live movie type") } - current.Movie.Base.Url = fmt.Sprintf("/api/movie/live/%s.%s", current.Movie.ID, t) current.Movie.Base.Headers = nil } else if current.Movie.Base.Proxy { current.Movie.Base.Url = fmt.Sprintf("/api/movie/proxy/%s/%s", current.Movie.RoomID, current.Movie.ID) @@ -558,14 +561,10 @@ func (e FormatErrNotSupportFileType) Error() string { } func JoinLive(ctx *gin.Context) { + ctx.Header("Cache-Control", "no-store") room := ctx.MustGet("room").(*op.RoomEntry).Value() - // user := ctx.MustGet("user").(*op.UserEntry) - movieId := strings.Trim(ctx.Param("movieId"), "/") - fileExt := path.Ext(movieId) - splitedMovieId := strings.Split(movieId, "/") - channelName := strings.TrimSuffix(splitedMovieId[0], fileExt) - m, err := room.GetMovieByID(channelName) + m, err := room.GetMovieByID(movieId) if err != nil { ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) return @@ -582,38 +581,144 @@ func JoinLive(ctx *gin.Context) { ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) return } - if channel == nil { - ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorStringResp("channel is nil")) - return - } - switch fileExt { - case ".flv": - ctx.Header("Cache-Control", "no-store") + joinType := ctx.DefaultQuery("type", "auto") + if joinType == "auto" { + joinType = m.Movie.Base.Type + } + switch joinType { + case "flv": w := httpflv.NewHttpFLVWriter(ctx.Writer) defer w.Close() - channel.AddPlayer(w) - w.SendPacket() - case ".m3u8": - ctx.Header("Cache-Control", "no-store") + err = channel.AddPlayer(w) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) + return + } + _ = w.SendPacket() + case "m3u8": b, err := channel.GenM3U8File(func(tsName string) (tsPath string) { ext := "ts" if settings.TsDisguisedAsPng.Get() { ext = "png" } - return fmt.Sprintf("/api/movie/live/%s/%s.%s", channelName, tsName, ext) + return fmt.Sprintf("/api/movie/live/hls/data/%s/%s/%s.%s", room.ID, movieId, tsName, ext) }) if err != nil { ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) return } ctx.Data(http.StatusOK, hls.M3U8ContentType, b) + default: + ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp(fmt.Sprintf("not support join type: %s", joinType))) + return + } +} + +func JoinFlvLive(ctx *gin.Context) { + ctx.Header("Cache-Control", "no-store") + room := ctx.MustGet("room").(*op.RoomEntry).Value() + movieId := strings.TrimSuffix(strings.Trim(ctx.Param("movieId"), "/"), ".flv") + m, err := room.GetMovieByID(movieId) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) + return + } + if m.Movie.Base.RtmpSource && !conf.Conf.Server.Rtmp.Enable { + ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("rtmp is not enabled")) + return + } else if m.Movie.Base.Live && !settings.LiveProxy.Get() { + ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("live proxy is not enabled")) + return + } + channel, err := m.Channel() + if err != nil { + ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) + return + } + + w := httpflv.NewHttpFLVWriter(ctx.Writer) + defer w.Close() + err = channel.AddPlayer(w) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) + return + } + _ = w.SendPacket() +} + +func JoinHlsLive(ctx *gin.Context) { + ctx.Header("Cache-Control", "no-store") + room := ctx.MustGet("room").(*op.RoomEntry).Value() + movieId := strings.TrimSuffix(strings.Trim(ctx.Param("movieId"), "/"), ".m3u8") + m, err := room.GetMovieByID(movieId) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) + return + } + if m.Movie.Base.RtmpSource && !conf.Conf.Server.Rtmp.Enable { + ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("rtmp is not enabled")) + return + } else if m.Movie.Base.Live && !settings.LiveProxy.Get() { + ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("live proxy is not enabled")) + return + } + channel, err := m.Channel() + if err != nil { + ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) + return + } + + b, err := channel.GenM3U8File(func(tsName string) (tsPath string) { + ext := "ts" + if settings.TsDisguisedAsPng.Get() { + ext = "png" + } + return fmt.Sprintf("/api/movie/live/hls/data/%s/%s/%s.%s", room.ID, movieId, tsName, ext) + }) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) + return + } + ctx.Data(http.StatusOK, hls.M3U8ContentType, b) +} + +func ServeHlsLive(ctx *gin.Context) { + ctx.Header("Cache-Control", "no-store") + roomId := ctx.Param("roomId") + roomE, err := op.LoadOrInitRoomByID(roomId) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) + return + } + room := roomE.Value() + movieId := ctx.Param("movieId") + m, err := room.GetMovieByID(movieId) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) + return + } + if m.Movie.Base.RtmpSource && !conf.Conf.Server.Rtmp.Enable { + ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("rtmp is not enabled")) + return + } else if m.Movie.Base.Live && !settings.LiveProxy.Get() { + ctx.AbortWithStatusJSON(http.StatusForbidden, model.NewApiErrorStringResp("live proxy is not enabled")) + return + } + channel, err := m.Channel() + if err != nil { + ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) + return + } + + dataId := ctx.Param("dataId") + switch fileExt := filepath.Ext(dataId); fileExt { case ".ts": if settings.TsDisguisedAsPng.Get() { ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(FormatErrNotSupportFileType(fileExt))) return } - b, err := channel.GetTsFile(splitedMovieId[1]) + b, err := channel.GetTsFile(strings.TrimSuffix(dataId, fileExt)) if err != nil { ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) return @@ -625,7 +730,7 @@ func JoinLive(ctx *gin.Context) { ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(FormatErrNotSupportFileType(fileExt))) return } - b, err := channel.GetTsFile(splitedMovieId[1]) + b, err := channel.GetTsFile(strings.TrimSuffix(dataId, fileExt)) if err != nil { ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) return diff --git a/server/middlewares/auth.go b/server/middlewares/auth.go index b55df05..994c981 100644 --- a/server/middlewares/auth.go +++ b/server/middlewares/auth.go @@ -180,7 +180,12 @@ func NewAuthRoomToken(user *op.User, room *op.Room) (string, error) { } func AuthRoomMiddleware(ctx *gin.Context) { - userE, roomE, err := AuthRoom(ctx.GetHeader("Authorization")) + token, err := GetAuthorizationTokenFromContext(ctx) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusUnauthorized, model.NewApiErrorResp(err)) + return + } + userE, roomE, err := AuthRoom(token) if err != nil { ctx.AbortWithStatusJSON(http.StatusUnauthorized, model.NewApiErrorResp(err)) return @@ -212,7 +217,12 @@ func AuthRoomMiddleware(ctx *gin.Context) { } func AuthUserMiddleware(ctx *gin.Context) { - user, err := AuthUser(ctx.GetHeader("Authorization")) + token, err := GetAuthorizationTokenFromContext(ctx) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusUnauthorized, model.NewApiErrorResp(err)) + return + } + user, err := AuthUser(token) if err != nil { ctx.AbortWithStatusJSON(http.StatusUnauthorized, model.NewApiErrorResp(err)) return @@ -231,7 +241,12 @@ func AuthUserMiddleware(ctx *gin.Context) { } func AuthAdminMiddleware(ctx *gin.Context) { - user, err := AuthUser(ctx.GetHeader("Authorization")) + token, err := GetAuthorizationTokenFromContext(ctx) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusUnauthorized, model.NewApiErrorResp(err)) + return + } + user, err := AuthUser(token) if err != nil { ctx.AbortWithStatusJSON(http.StatusUnauthorized, model.NewApiErrorResp(err)) return @@ -246,7 +261,12 @@ func AuthAdminMiddleware(ctx *gin.Context) { } func AuthRootMiddleware(ctx *gin.Context) { - user, err := AuthUser(ctx.GetHeader("Authorization")) + token, err := GetAuthorizationTokenFromContext(ctx) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusUnauthorized, model.NewApiErrorResp(err)) + return + } + user, err := AuthUser(token) if err != nil { ctx.AbortWithStatusJSON(http.StatusUnauthorized, model.NewApiErrorResp(err)) return @@ -259,3 +279,15 @@ func AuthRootMiddleware(ctx *gin.Context) { ctx.Set("user", user) ctx.Next() } + +func GetAuthorizationTokenFromContext(ctx *gin.Context) (string, error) { + Authorization := ctx.GetHeader("Authorization") + if Authorization != "" { + return Authorization, nil + } + Authorization = ctx.Query("token") + if Authorization != "" { + return Authorization, nil + } + return "", errors.New("token is empty") +}