diff --git a/go.mod b/go.mod index 7c3478d..999cd55 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/ulule/limiter/v3 v3.11.2 github.com/zijiren233/gencontainer v0.0.0-20230930135658-e410015e13cc github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb - github.com/zijiren233/livelib v0.2.2-0.20231021080243-c5097432686c + github.com/zijiren233/livelib v0.2.3-0.20231103145812-58de2ae7f423 github.com/zijiren233/stream v0.5.1 github.com/zijiren233/yaml-comment v0.2.1 golang.org/x/crypto v0.14.0 @@ -46,7 +46,7 @@ require ( github.com/BurntSushi/toml v1.3.2 // indirect github.com/bytedance/sonic v1.10.2 // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect - github.com/chenzhuoyu/iasm v0.9.0 // indirect + github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/fatih/color v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect diff --git a/go.sum b/go.sum index 3905bb5..1090022 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= github.com/chenzhuoyu/iasm v0.9.0 h1:9fhXjVzq5hUy2gkhhgHl95zG2cEAhw9OSGs8toWWAwo= github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= +github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= +github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -204,6 +206,10 @@ 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.2.2-0.20231021080243-c5097432686c h1:sQtkWQi+QWdmx4Jx2MA/Ib9pYPmnVw5Qd/xOB8K5zs0= github.com/zijiren233/livelib v0.2.2-0.20231021080243-c5097432686c/go.mod h1:2wrAAqNIdMZjQrdbO7ERQfqK4VS5fzgUj2xXwrJ8/uo= +github.com/zijiren233/livelib v0.2.2 h1:2VeJSg9tmmQ7KfeeVQwVZhmzj/FkXRErdwdvZVxUyN0= +github.com/zijiren233/livelib v0.2.2/go.mod h1:2wrAAqNIdMZjQrdbO7ERQfqK4VS5fzgUj2xXwrJ8/uo= +github.com/zijiren233/livelib v0.2.3-0.20231103145812-58de2ae7f423 h1:6febr/evRs52lo2lHSpcc6e7+yVPI04ba9eEl26tl+Y= +github.com/zijiren233/livelib v0.2.3-0.20231103145812-58de2ae7f423/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/internal/conf/rtmp.go b/internal/conf/rtmp.go index 75ae125..a21e279 100644 --- a/internal/conf/rtmp.go +++ b/internal/conf/rtmp.go @@ -6,6 +6,7 @@ type RtmpConfig struct { CustomPublishHost string `yaml:"custom_publish_host" lc:"default use http header host" env:"RTMP_CUSTOM_PUBLISH_HOST"` RtmpPlayer bool `yaml:"rtmp_player" hc:"can watch live streams through the RTMP protocol (without authentication, insecure)." env:"RTMP_PLAYER"` + TsDisguisedAsPng bool `yaml:"ts_disguised_as_png" hc:"disguise the .ts file as a .png file" env:"RTMP_TS_DISGUISED_AS_PNG"` } func DefaultRtmpConfig() RtmpConfig { @@ -14,5 +15,6 @@ func DefaultRtmpConfig() RtmpConfig { Port: 0, CustomPublishHost: "", RtmpPlayer: false, + TsDisguisedAsPng: true, } } diff --git a/internal/op/movie.go b/internal/op/movie.go index 1bb24dc..3a17e83 100644 --- a/internal/op/movie.go +++ b/internal/op/movie.go @@ -8,8 +8,10 @@ import ( "github.com/go-resty/resty/v2" "github.com/synctv-org/synctv/internal/model" + "github.com/synctv-org/synctv/utils" "github.com/zijiren233/livelib/av" "github.com/zijiren233/livelib/container/flv" + "github.com/zijiren233/livelib/protocol/hls" rtmpProto "github.com/zijiren233/livelib/protocol/rtmp" "github.com/zijiren233/livelib/protocol/rtmp/core" rtmps "github.com/zijiren233/livelib/server" @@ -27,6 +29,10 @@ func (m *movie) Channel() (*rtmps.Channel, error) { return m.channel, m.init() } +func genTsName() string { + return utils.SortUUID() +} + func (m *movie) init() (err error) { if err = m.Movie.Validate(); err != nil { return @@ -35,7 +41,7 @@ func (m *movie) init() (err error) { case m.Base.Live && m.Base.RtmpSource: if m.channel == nil { m.channel = rtmps.NewChannel() - m.channel.InitHlsPlayer() + m.channel.InitHlsPlayer(hls.WithGenTsNameFunc(genTsName)) } case m.Base.Live && m.Base.Proxy: u, err := url.Parse(m.Base.Url) @@ -46,7 +52,7 @@ func (m *movie) init() (err error) { case "rtmp": if m.channel == nil { m.channel = rtmps.NewChannel() - m.channel.InitHlsPlayer() + m.channel.InitHlsPlayer(hls.WithGenTsNameFunc(genTsName)) go func() { for { if m.channel.Closed() { @@ -68,7 +74,7 @@ func (m *movie) init() (err error) { case "http", "https": if m.channel == nil { m.channel = rtmps.NewChannel() - m.channel.InitHlsPlayer() + m.channel.InitHlsPlayer(hls.WithGenTsNameFunc(genTsName)) go func() { for { if m.channel.Closed() { diff --git a/server/handlers/movie.go b/server/handlers/movie.go index ad48338..857d019 100644 --- a/server/handlers/movie.go +++ b/server/handlers/movie.go @@ -3,6 +3,10 @@ package handlers import ( "errors" "fmt" + "image" + "image/color" + "image/png" + "math/rand" "net/http" "path" "strconv" @@ -528,13 +532,23 @@ func JoinLive(ctx *gin.Context) { w.SendPacket() case ".m3u8": ctx.Header("Cache-Control", "no-store") - b, err := channel.GenM3U8PlayList(fmt.Sprintf("/api/movie/live/%s", channelName)) + b, err := channel.GenM3U8File(func(tsName string) (tsPath string) { + ext := "ts" + if conf.Conf.Rtmp.TsDisguisedAsPng { + ext = "png" + } + return fmt.Sprintf("/api/movie/live/%s.%s", channelName, ext) + }) if err != nil { ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) return } - ctx.Data(http.StatusOK, hls.M3U8ContentType, b.Bytes()) + ctx.Data(http.StatusOK, hls.M3U8ContentType, b) case ".ts": + if conf.Conf.Rtmp.TsDisguisedAsPng { + ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(FormatErrNotSupportFileType(fileExt))) + return + } b, err := channel.GetTsFile(movieIdSplitd[1]) if err != nil { ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) @@ -542,6 +556,22 @@ func JoinLive(ctx *gin.Context) { } ctx.Header("Cache-Control", "public, max-age=90") ctx.Data(http.StatusOK, hls.TSContentType, b) + case ".png": + if !conf.Conf.Rtmp.TsDisguisedAsPng { + ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(FormatErrNotSupportFileType(fileExt))) + return + } + b, err := channel.GetTsFile(movieIdSplitd[1]) + if err != nil { + ctx.AbortWithStatusJSON(http.StatusNotFound, model.NewApiErrorResp(err)) + return + } + ctx.Header("Cache-Control", "public, max-age=90") + ctx.Header("Content-Type", "image/png") + img := image.NewGray(image.Rect(0, 0, 1, 1)) + img.Set(1, 1, color.Gray{uint8(rand.Intn(255))}) + png.Encode(ctx.Writer, img) + ctx.Writer.Write(b) default: ctx.Header("Cache-Control", "no-store") ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(FormatErrNotSupportFileType(fileExt)))