Feat: bilibli dash proxy

pull/31/head
zijiren233 1 year ago
parent d7f52304a8
commit 825950f7c5

@ -24,6 +24,7 @@ require (
github.com/soheilhy/cmux v0.1.5
github.com/spf13/cobra v1.7.0
github.com/ulule/limiter/v3 v3.11.2
github.com/zencoder/go-dash/v3 v3.0.3
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.3-0.20231103145812-58de2ae7f423

@ -19,7 +19,6 @@ github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
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=
@ -200,14 +199,12 @@ github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZ
github.com/ulule/limiter/v3 v3.11.2 h1:P4yOrxoEMJbOTfRJR2OzjL90oflzYPPmWg+dvwN2tHA=
github.com/ulule/limiter/v3 v3.11.2/go.mod h1:QG5GnFOCV+k7lrL5Y8kgEeeflPH3+Cviqlqa8SVSQxI=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zencoder/go-dash/v3 v3.0.3 h1:xqwGJ2fJCSArwONGx6sY26Z1lxQ7zTURoxdRjCpuodM=
github.com/zencoder/go-dash/v3 v3.0.3/go.mod h1:30R5bKy1aUYY45yesjtZ9l8trNc2TwNqbS17WVQmCzk=
github.com/zijiren233/gencontainer v0.0.0-20230930135658-e410015e13cc h1:qEYdClJZG4GHT7pG+scIkN36u5/n1uj5bAPt8UeLkO4=
github.com/zijiren233/gencontainer v0.0.0-20230930135658-e410015e13cc/go.mod h1:V5oL7PrZxgisuLCblFWd89Jg99O8vM1n58llcxZ2hDY=
github.com/zijiren233/go-colorable v0.0.0-20230930131441-997304c961cb h1:0DyOxf/TbbGodHhOVHNoPk+7v/YBJACs22gKpKlatWw=
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=

@ -64,6 +64,21 @@ func AssignFirstOrCreateVendorByUserIDAndVendor(userID string, vendor model.Stre
return &vendorInfo, err
}
func FirstOrInitVendorByUserIDAndVendor(userID string, vendor model.StreamingVendor, conf ...CreateVendorConfig) (*model.StreamingVendorInfo, error) {
var vendorInfo model.StreamingVendorInfo
v := &model.StreamingVendorInfo{
UserID: userID,
Vendor: vendor,
}
for _, c := range conf {
c(v)
}
err := db.Where("user_id = ? AND vendor = ?", userID, vendor).Attrs(
v,
).FirstOrInit(&vendorInfo).Error
return &vendorInfo, err
}
func DeleteVendorByUserIDAndVendor(userID string, vendor model.StreamingVendor) error {
return db.Where("user_id = ? AND vendor = ?", userID, vendor).Delete(&model.StreamingVendorInfo{}).Error
}

@ -4,10 +4,12 @@ import (
"errors"
"fmt"
"net/url"
"sync/atomic"
"time"
"github.com/synctv-org/synctv/internal/conf"
"github.com/synctv-org/synctv/utils"
refreshcache "github.com/synctv-org/synctv/utils/refreshCache"
"gorm.io/gorm"
)
@ -149,8 +151,26 @@ type VendorInfo struct {
}
type BilibiliVendorInfo struct {
Bvid string `json:"bvid,omitempty"`
Cid uint `json:"cid,omitempty"`
Epid uint `json:"epid,omitempty"`
Quality uint `json:"quality,omitempty"`
Bvid string `json:"bvid,omitempty"`
Cid uint `json:"cid,omitempty"`
Epid uint `json:"epid,omitempty"`
Quality uint `json:"quality,omitempty"`
Cache atomic.Pointer[refreshcache.RefreshCache[*BilibiliVendorCache]] `gorm:"-:all" json:"-"`
}
type BilibiliVendorCache struct {
MPDFile string
URLs []string
}
func (b *BilibiliVendorInfo) InitOrLoadCache(initCache func() *refreshcache.RefreshCache[*BilibiliVendorCache]) *refreshcache.RefreshCache[*BilibiliVendorCache] {
if c := b.Cache.Load(); c != nil {
return c
}
c := initCache()
if b.Cache.CompareAndSwap(nil, c) {
return c
} else {
return b.Cache.Load()
}
}

@ -17,13 +17,13 @@ import (
rtmps "github.com/zijiren233/livelib/server"
)
type movie struct {
type Movie struct {
*model.Movie
lock sync.RWMutex
channel *rtmps.Channel
}
func (m *movie) Channel() (*rtmps.Channel, error) {
func (m *Movie) Channel() (*rtmps.Channel, error) {
m.lock.Lock()
defer m.lock.Unlock()
return m.channel, m.init()
@ -33,7 +33,7 @@ func genTsName() string {
return utils.SortUUID()
}
func (m *movie) init() (err error) {
func (m *Movie) init() (err error) {
if err = m.Movie.Validate(); err != nil {
return
}
@ -104,20 +104,20 @@ func (m *movie) init() (err error) {
return nil
}
func (m *movie) Terminate() {
func (m *Movie) Terminate() {
m.lock.Lock()
defer m.lock.Unlock()
m.terminate()
}
func (m *movie) terminate() {
func (m *Movie) terminate() {
if m.channel != nil {
m.channel.Close()
m.channel = nil
}
}
func (m *movie) Update(movie model.BaseMovie) error {
func (m *Movie) Update(movie model.BaseMovie) error {
m.lock.Lock()
defer m.lock.Unlock()
m.terminate()

@ -15,14 +15,14 @@ import (
type movies struct {
roomID string
lock sync.RWMutex
list dllist.Dllist[*movie]
list dllist.Dllist[*Movie]
once sync.Once
}
func (m *movies) init() {
m.once.Do(func() {
for _, m2 := range db.GetAllMoviesByRoomID(m.roomID) {
m.list.PushBack(&movie{
m.list.PushBack(&Movie{
Movie: m2,
})
}
@ -41,7 +41,7 @@ func (m *movies) Add(mo *model.Movie) error {
defer m.lock.Unlock()
m.init()
mo.Position = uint(time.Now().UnixMilli())
movie := &movie{
movie := &Movie{
Movie: mo,
}
@ -134,13 +134,13 @@ func (m *movies) DeleteMovieByID(id string) error {
return errors.New("movie not found")
}
func (m *movies) GetMovieByID(id string) (*movie, error) {
func (m *movies) GetMovieByID(id string) (*Movie, error) {
m.lock.RLock()
defer m.lock.RUnlock()
return m.getMovieByID(id)
}
func (m *movies) getMovieByID(id string) (*movie, error) {
func (m *movies) getMovieByID(id string) (*Movie, error) {
m.init()
for e := m.list.Front(); e != nil; e = e.Next() {
if e.Value.ID == id {
@ -150,7 +150,7 @@ func (m *movies) getMovieByID(id string) (*movie, error) {
return nil, errors.New("movie not found")
}
func (m *movies) getMovieElementByID(id string) (*dllist.Element[*movie], error) {
func (m *movies) getMovieElementByID(id string) (*dllist.Element[*Movie], error) {
m.init()
for e := m.list.Front(); e != nil; e = e.Next() {
if e.Value.ID == id {
@ -186,13 +186,13 @@ func (m *movies) SwapMoviePositions(id1, id2 string) error {
return nil
}
func (m *movies) GetMoviesWithPage(page, pageSize int) []*movie {
func (m *movies) GetMoviesWithPage(page, pageSize int) []*Movie {
m.lock.RLock()
defer m.lock.RUnlock()
m.init()
start, end := utils.GetPageItemsRange(m.list.Len(), page, pageSize)
ms := make([]*movie, 0, end-start)
ms := make([]*Movie, 0, end-start)
i := 0
for e := m.list.Front(); e != nil; e = e.Next() {
if i >= start && i < end {

@ -139,7 +139,7 @@ func (r *Room) ClearMovies() error {
return r.movies.Clear()
}
func (r *Room) GetMovieByID(id string) (*movie, error) {
func (r *Room) GetMovieByID(id string) (*Movie, error) {
return r.movies.GetMovieByID(id)
}
@ -161,7 +161,7 @@ func (r *Room) SwapMoviePositions(id1, id2 string) error {
return r.movies.SwapMoviePositions(id1, id2)
}
func (r *Room) GetMoviesWithPage(page, pageSize int) []*movie {
func (r *Room) GetMoviesWithPage(page, pageSize int) []*Movie {
return r.movies.GetMoviesWithPage(page, pageSize)
}

@ -14,6 +14,7 @@ import (
"time"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/synctv-org/synctv/internal/conf"
"github.com/synctv-org/synctv/internal/db"
dbModel "github.com/synctv-org/synctv/internal/model"
@ -23,7 +24,9 @@ import (
"github.com/synctv-org/synctv/proxy"
"github.com/synctv-org/synctv/server/model"
"github.com/synctv-org/synctv/utils"
refreshcache "github.com/synctv-org/synctv/utils/refreshCache"
"github.com/synctv-org/synctv/vendors/bilibili"
"github.com/zencoder/go-dash/v3/mpd"
"github.com/zijiren233/livelib/protocol/hls"
"github.com/zijiren233/livelib/protocol/httpflv"
)
@ -61,11 +64,17 @@ func MovieList(ctx *gin.Context) {
m := room.GetMoviesWithPage(page, max)
current, err := genCurrent(room.Current(), user.ID)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
mresp := make([]model.MoviesResp, len(m))
for i, v := range m {
mresp[i] = model.MoviesResp{
Id: v.ID,
Base: m[i].Base,
Base: v.Base,
Creator: op.GetUserName(v.CreatorID),
}
// hide headers when proxy
@ -74,12 +83,6 @@ func MovieList(ctx *gin.Context) {
}
}
current, err := genCurrent(room.Current(), user.ID)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusInternalServerError, model.NewApiErrorResp(err))
return
}
current.UpdateSeek()
ctx.JSON(http.StatusOK, model.NewApiDataResp(gin.H{
@ -91,7 +94,7 @@ func MovieList(ctx *gin.Context) {
func genCurrent(current *op.Current, userID string) (*op.Current, error) {
if current.Movie.Base.VendorInfo.Vendor != "" {
return current, parse2VendorMovie(userID, &current.Movie, !current.Movie.Base.Proxy)
return current, parse2VendorMovie(userID, &current.Movie)
}
return current, nil
}
@ -141,11 +144,12 @@ func Movies(ctx *gin.Context) {
m := room.GetMoviesWithPage(int(page), int(max))
mresp := make([]model.MoviesResp, len(m))
mresp := make([]*model.MoviesResp, len(m))
for i, v := range m {
mresp[i] = model.MoviesResp{
logrus.Info(m[i].Base.Headers)
mresp[i] = &model.MoviesResp{
Id: v.ID,
Base: m[i].Base,
Base: v.Base,
Creator: op.GetUserName(v.CreatorID),
}
// hide headers when proxy
@ -480,23 +484,27 @@ func ProxyMovie(ctx *gin.Context) {
}
if m.Base.VendorInfo.Vendor != "" {
err = parse2VendorMovie(m.Movie.CreatorID, m.Movie, true)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
proxyVendorMovie(ctx, m.Movie)
return
}
if l, err := utils.ParseURLIsLocalIP(m.Base.Url); err != nil || l {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("parse url error or url is local ip"))
err = proxyURL(ctx, m.Base.Url, m.Base.Headers)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
}
hrs := proxy.NewBufferedHttpReadSeeker(256*1024, m.Base.Url,
func proxyURL(ctx *gin.Context, url string, headers map[string]string) error {
if l, err := utils.ParseURLIsLocalIP(url); err != nil || l {
return err
}
hrs := proxy.NewBufferedHttpReadSeeker(512*1024, url,
proxy.WithContext(ctx),
proxy.WithHeaders(m.Base.Headers),
proxy.WithHeaders(headers),
)
http.ServeContent(ctx.Writer, ctx.Request, m.Base.Url, time.Now(), hrs)
http.ServeContent(ctx.Writer, ctx.Request, url, time.Now(), hrs)
return nil
}
type FormatErrNotSupportFileType string
@ -578,7 +586,85 @@ func JoinLive(ctx *gin.Context) {
}
}
func parse2VendorMovie(userID string, movie *dbModel.Movie, getUrl bool) (err error) {
func initBilibiliCache(cookies []*http.Cookie, bvid string, cid, epid uint, roomID, movieID string) *refreshcache.RefreshCache[*dbModel.BilibiliVendorCache] {
return refreshcache.NewRefreshCache[*dbModel.BilibiliVendorCache](func() (*dbModel.BilibiliVendorCache, error) {
cli, err := bilibili.NewClient(cookies)
if err != nil {
return nil, err
}
var m *mpd.MPD
if bvid != "" && cid != 0 {
m, err = cli.GetDashVideoURL(0, bvid, cid)
} else if epid != 0 {
m, err = cli.GetDashPGCURL(epid, 0)
} else {
return nil, errors.New("bvid and epid are empty")
}
if err != nil {
return nil, err
}
m.BaseURL = append(m.BaseURL, fmt.Sprintf("/api/movie/proxy/%s/", roomID))
id := 0
movies := []string{}
for _, as := range m.GetCurrentPeriod().AdaptationSets {
for _, r := range as.Representations {
for i := range r.BaseURL {
movies = append(movies, r.BaseURL[i])
r.BaseURL[i] = fmt.Sprintf("%s?id=%d", movieID, id)
id++
}
}
}
s, err := m.WriteToString()
if err != nil {
return nil, err
}
return &dbModel.BilibiliVendorCache{
URLs: movies,
MPDFile: s,
}, nil
}, time.Minute*119)
}
func proxyVendorMovie(ctx *gin.Context, movie *dbModel.Movie) {
switch movie.Base.VendorInfo.Vendor {
case dbModel.StreamingVendorBilibili:
info := movie.Base.VendorInfo.Bilibili
bvc, err := movie.Base.VendorInfo.Bilibili.InitOrLoadCache(func() *refreshcache.RefreshCache[*dbModel.BilibiliVendorCache] {
vendor, err := db.FirstOrInitVendorByUserIDAndVendor(movie.CreatorID, dbModel.StreamingVendorBilibili)
if err != nil {
return nil
}
return initBilibiliCache(vendor.Cookies, info.Bvid, info.Cid, info.Epid, movie.RoomID, movie.ID)
}).Get()
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
if id := ctx.Query("id"); id == "" {
ctx.Data(http.StatusOK, "application/dash+xml", []byte(bvc.MPDFile))
return
} else {
streamId, err := strconv.Atoi(id)
if err != nil {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorResp(err))
return
}
if streamId >= len(bvc.URLs) {
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("stream id out of range"))
return
}
proxyURL(ctx, bvc.URLs[streamId], movie.Base.Headers)
return
}
default:
ctx.AbortWithStatusJSON(http.StatusBadRequest, model.NewApiErrorStringResp("vendor not support"))
return
}
}
func parse2VendorMovie(userID string, movie *dbModel.Movie) (err error) {
if movie.Base.VendorInfo.Shared {
userID = movie.CreatorID
}
@ -596,7 +682,7 @@ func parse2VendorMovie(userID string, movie *dbModel.Movie, getUrl bool) (err er
return err
}
if getUrl {
if !movie.Base.Proxy {
var mu *bilibili.VideoURL
if info.Bvid != "" {
mu, err = cli.GetVideoURL(0, info.Bvid, info.Cid, bilibili.WithQuality(info.Quality))
@ -609,6 +695,8 @@ func parse2VendorMovie(userID string, movie *dbModel.Movie, getUrl bool) (err er
return err
}
movie.Base.Url = mu.URL
} else {
movie.Base.Type = "mpd"
}
return nil

@ -41,7 +41,7 @@ func (p *PushMovieReq) Validate() error {
return ErrTypeTooLong
}
return nil
return (*model.BaseMovie)(p).Validate()
}
type PushMoviesReq []*PushMovieReq

@ -921,3 +921,181 @@ type Nav struct {
IsJury bool `json:"is_jury"`
} `json:"data"`
}
type dashResp struct {
Code int `json:"code"`
Message string `json:"message"`
TTL int `json:"ttl"`
Data struct {
From string `json:"from"`
Result string `json:"result"`
Message string `json:"message"`
Quality int `json:"quality"`
Format string `json:"format"`
Timelength int `json:"timelength"`
AcceptFormat string `json:"accept_format"`
AcceptDescription []string `json:"accept_description"`
AcceptQuality []int `json:"accept_quality"`
VideoCodecid int `json:"video_codecid"`
SeekParam string `json:"seek_param"`
SeekType string `json:"seek_type"`
Dash struct {
Duration float64 `json:"duration"`
MinBufferTime float64 `json:"min_buffer_time"`
Video []struct {
ID int `json:"id"`
BaseURL string `json:"base_url"`
BackupURL []string `json:"backup_url"`
Bandwidth int64 `json:"bandwidth"`
MimeType string `json:"mime_type"`
Codecs string `json:"codecs"`
Width int64 `json:"width"`
Height int64 `json:"height"`
FrameRate string `json:"frame_rate"`
Sar string `json:"sar"`
StartWithSap int64 `json:"start_with_sap"`
SegmentBase struct {
Initialization string `json:"initialization"`
IndexRange string `json:"index_range"`
} `json:"segment_base"`
Codecid int `json:"codecid"`
} `json:"video"`
Audio []struct {
ID int `json:"id"`
BaseURL string `json:"base_url"`
BackupURL []string `json:"backup_url"`
Bandwidth int64 `json:"bandwidth"`
MimeType string `json:"mime_type"`
Codecs string `json:"codecs"`
Width int `json:"width"`
Height int `json:"height"`
FrameRate string `json:"frame_rate"`
Sar string `json:"sar"`
StartWithSap int64 `json:"start_with_sap"`
SegmentBase struct {
Initialization string `json:"initialization"`
IndexRange string `json:"index_range"`
} `json:"segment_base"`
Codecid int `json:"codecid"`
} `json:"audio"`
Dolby struct {
Type int `json:"type"`
Audio interface{} `json:"audio"`
} `json:"dolby"`
Flac interface{} `json:"flac"`
} `json:"dash"`
SupportFormats []struct {
Quality int `json:"quality"`
Format string `json:"format"`
NewDescription string `json:"new_description"`
DisplayDesc string `json:"display_desc"`
Superscript string `json:"superscript"`
Codecs []string `json:"codecs"`
} `json:"support_formats"`
HighFormat interface{} `json:"high_format"`
LastPlayTime int `json:"last_play_time"`
LastPlayCid int `json:"last_play_cid"`
} `json:"data"`
}
type dashPGCResp struct {
Code int `json:"code"`
Message string `json:"message"`
Result struct {
AcceptFormat string `json:"accept_format"`
Code int `json:"code"`
SeekParam string `json:"seek_param"`
IsPreview int `json:"is_preview"`
Fnval int `json:"fnval"`
VideoProject bool `json:"video_project"`
Fnver int `json:"fnver"`
Type string `json:"type"`
Bp int `json:"bp"`
Result string `json:"result"`
SeekType string `json:"seek_type"`
From string `json:"from"`
VideoCodecid int `json:"video_codecid"`
RecordInfo struct {
RecordIcon string `json:"record_icon"`
Record string `json:"record"`
} `json:"record_info"`
IsDrm bool `json:"is_drm"`
NoRexcode int `json:"no_rexcode"`
Format string `json:"format"`
SupportFormats []struct {
DisplayDesc string `json:"display_desc"`
Superscript string `json:"superscript"`
NeedLogin bool `json:"need_login,omitempty"`
Codecs []string `json:"codecs"`
Format string `json:"format"`
Description string `json:"description"`
NeedVip bool `json:"need_vip,omitempty"`
Quality int `json:"quality"`
NewDescription string `json:"new_description"`
} `json:"support_formats"`
Message string `json:"message"`
AcceptQuality []int `json:"accept_quality"`
Quality int `json:"quality"`
Timelength int `json:"timelength"`
HasPaid bool `json:"has_paid"`
Dash struct {
Duration float64 `json:"duration"`
MinBufferTime float64 `json:"min_buffer_time"`
Video []struct {
StartWithSap int64 `json:"start_with_sap"`
Bandwidth int64 `json:"bandwidth"`
Sar string `json:"sar"`
Codecs string `json:"codecs"`
BaseURL string `json:"base_url"`
BackupURL []string `json:"backup_url"`
SegmentBase struct {
Initialization string `json:"initialization"`
IndexRange string `json:"index_range"`
} `json:"segment_base"`
FrameRate string `json:"frame_rate"`
Codecid int `json:"codecid"`
Size int `json:"size"`
MimeType string `json:"mime_type"`
Width int64 `json:"width"`
StartWithSAP int `json:"startWithSAP"`
ID int `json:"id"`
Height int64 `json:"height"`
Md5 string `json:"md5"`
} `json:"video"`
Audio []struct {
StartWithSap int64 `json:"start_with_sap"`
Bandwidth int64 `json:"bandwidth"`
Sar string `json:"sar"`
Codecs string `json:"codecs"`
BaseURL string `json:"base_url"`
BackupURL []string `json:"backup_url"`
SegmentBase struct {
Initialization string `json:"initialization"`
IndexRange string `json:"index_range"`
} `json:"segment_base"`
FrameRate string `json:"frame_rate"`
Codecid int `json:"codecid"`
Size int `json:"size"`
MimeType string `json:"mime_type"`
Width int `json:"width"`
StartWithSAP int `json:"startWithSAP"`
ID int `json:"id"`
Height int `json:"height"`
Md5 string `json:"md5"`
} `json:"audio"`
Dolby struct {
Audio []interface{} `json:"audio"`
Type int `json:"type"`
} `json:"dolby"`
} `json:"dash"`
ClipInfoList []struct {
MaterialNo int `json:"materialNo"`
Start int `json:"start"`
End int `json:"end"`
ToastText string `json:"toastText"`
ClipType string `json:"clipType"`
} `json:"clip_info_list"`
AcceptDescription []string `json:"accept_description"`
Status int `json:"status"`
} `json:"result"`
}

@ -1,6 +1,7 @@
package bilibili_test
import (
"fmt"
"testing"
"github.com/synctv-org/synctv/vendors/bilibili"
@ -62,3 +63,61 @@ func TestMatch(t *testing.T) {
})
}
}
func TestGetDashVideoURL(t *testing.T) {
c, err := bilibili.NewClient(nil)
if err != nil {
t.Fatal(err)
}
m, err := c.GetDashVideoURL(0, "BV1y7411Q7Eq", 171776208)
if err != nil {
t.Fatal(err)
}
for _, as := range m.GetCurrentPeriod().AdaptationSets {
for _, r := range as.Representations {
t.Log(r.BaseURL)
}
}
}
func TestGetDashVideoURLMPDFile(t *testing.T) {
c, err := bilibili.NewClient(nil)
if err != nil {
t.Fatal(err)
}
m, err := c.GetDashVideoURL(0, "BV1y7411Q7Eq", 171776208)
if err != nil {
t.Fatal(err)
}
s, err := m.WriteToString()
if err != nil {
t.Fatal(err)
}
t.Log(s)
}
func TestEditAndGetDashVideoURLMPDFile(t *testing.T) {
c, err := bilibili.NewClient(nil)
if err != nil {
t.Fatal(err)
}
m, err := c.GetDashVideoURL(0, "BV1y7411Q7Eq", 171776208)
if err != nil {
t.Fatal(err)
}
m.BaseURL = append(m.BaseURL, "/")
id := 0
for _, as := range m.GetCurrentPeriod().AdaptationSets {
for _, r := range as.Representations {
for i := range r.BaseURL {
r.BaseURL[i] = fmt.Sprintf("/api/movie/proxy/roomid/movieid?id=%d", id)
id++
}
}
}
s, err := m.WriteToString()
if err != nil {
t.Fatal(err)
}
t.Log(s)
}

@ -4,8 +4,11 @@ import (
"errors"
"fmt"
"net/http"
"strconv"
"time"
json "github.com/json-iterator/go"
"github.com/zencoder/go-dash/v3/mpd"
)
type VideoPageInfo struct {
@ -180,6 +183,149 @@ func (c *Client) GetVideoURL(aid uint, bvid string, cid uint, conf ...GetVideoUR
}, nil
}
type GetDashVideoURLConf struct {
HDR bool
Need4K bool
NeedDOLBY bool
NeedDOLBYAudio bool
Need8K bool
NeedAV1 bool
}
type GetDashVideoURLConfig func(*GetDashVideoURLConf)
func WithHDR(hdr bool) GetDashVideoURLConfig {
return func(c *GetDashVideoURLConf) {
c.HDR = hdr
}
}
func WithNeed4K(need4k bool) GetDashVideoURLConfig {
return func(c *GetDashVideoURLConf) {
c.Need4K = need4k
}
}
func WithNeedDOLBY(needDOLBY bool) GetDashVideoURLConfig {
return func(c *GetDashVideoURLConf) {
c.NeedDOLBY = needDOLBY
}
}
func WithNeedDOLBYAudio(needDOLBYAudio bool) GetDashVideoURLConfig {
return func(c *GetDashVideoURLConf) {
c.NeedDOLBYAudio = needDOLBYAudio
}
}
func WithNeed8K(need8k bool) GetDashVideoURLConfig {
return func(c *GetDashVideoURLConf) {
c.Need8K = need8k
}
}
// https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/videostream_url.md
func (c *Client) GetDashVideoURL(aid uint, bvid string, cid uint, conf ...GetDashVideoURLConfig) (*mpd.MPD, error) {
config := &GetDashVideoURLConf{}
for _, v := range conf {
v(config)
}
var (
fnval uint = 16
extQuery string
)
if config.Need4K {
fnval = 128
extQuery = "&fourk=1"
} else if config.Need8K {
fnval = 1024
}
if config.HDR {
fnval |= 64
}
if config.NeedDOLBY {
fnval |= 512
}
if config.NeedDOLBYAudio {
fnval |= 256
}
if config.NeedAV1 {
fnval |= 2048
}
var url string
if aid != 0 {
url = fmt.Sprintf("https://api.bilibili.com/x/player/wbi/playurl?aid=%d&cid=%d&fnver=0&platform=pc&fnval=%d%s", aid, cid, fnval, extQuery)
} else if bvid != "" {
url = fmt.Sprintf("https://api.bilibili.com/x/player/wbi/playurl?bvid=%s&cid=%d&fnver=0&platform=pc&fnval=%d%s", bvid, cid, fnval, extQuery)
} else {
return nil, fmt.Errorf("aid and bvid are both empty")
}
req, err := c.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
info := dashResp{}
err = json.NewDecoder(resp.Body).Decode(&info)
if err != nil {
return nil, err
}
if info.Code != 0 {
return nil, errors.New(info.Message)
}
m := mpd.NewMPD(mpd.DASH_PROFILE_ONDEMAND, fmt.Sprintf("PT%.2fS", info.Data.Dash.Duration), fmt.Sprintf("PT%.2fS", info.Data.Dash.MinBufferTime))
var as *mpd.AdaptationSet
for _, v := range info.Data.Dash.Video {
as, err = m.AddNewAdaptationSetVideo(v.MimeType, "progressive", true, v.StartWithSap)
if err != nil {
return nil, err
}
video, err := as.AddNewRepresentationVideo(v.Bandwidth, v.Codecs, fmt.Sprint(time.Now().UnixMicro()), v.FrameRate, v.Width, v.Height)
if err != nil {
return nil, err
}
video.Sar = &v.Sar
err = video.AddNewBaseURL(v.BaseURL)
if err != nil {
return nil, err
}
_, err = video.AddNewSegmentBase(v.SegmentBase.IndexRange, v.SegmentBase.Initialization)
if err != nil {
return nil, err
}
}
as = nil
for _, a := range info.Data.Dash.Audio {
as, err = m.AddNewAdaptationSetAudio(a.MimeType, true, a.StartWithSap, "und")
if err != nil {
return nil, err
}
audio, err := as.AddNewRepresentationAudio(44100, a.Bandwidth, a.Codecs, strconv.Itoa(a.ID))
if err != nil {
return nil, err
}
audio.Sar = &a.Sar
err = audio.AddNewBaseURL(a.BaseURL)
if err != nil {
return nil, err
}
_, err = audio.AddNewSegmentBase(a.SegmentBase.IndexRange, a.SegmentBase.Initialization)
if err != nil {
return nil, err
}
}
return m, nil
}
type Subtitle struct {
Name string `json:"name"`
URL string `json:"url"`
@ -309,3 +455,104 @@ func (c *Client) GetPGCURL(ep_id, cid uint, conf ...GetVideoURLConfig) (*VideoUR
URL: info.Result.Durl[0].URL,
}, nil
}
func (c *Client) GetDashPGCURL(ep_id, cid uint, conf ...GetDashVideoURLConfig) (*mpd.MPD, error) {
config := &GetDashVideoURLConf{}
for _, v := range conf {
v(config)
}
var (
fnval uint = 16
extQuery string
)
if config.Need4K {
fnval = 128
extQuery = "&fourk=1"
} else if config.Need8K {
fnval = 1024
}
if config.HDR {
fnval |= 64
}
if config.NeedDOLBY {
fnval |= 512
}
if config.NeedDOLBYAudio {
fnval |= 256
}
if config.NeedAV1 {
fnval |= 2048
}
var url string
if ep_id != 0 {
url = fmt.Sprintf("https://api.bilibili.com/pgc/player/web/playurl?ep_id=%d&fnval=%d%s", ep_id, fnval, extQuery)
} else if cid != 0 {
url = fmt.Sprintf("https://api.bilibili.com/pgc/player/web/playurl?cid=%d&fnval=%d%s", ep_id, fnval, extQuery)
} else {
return nil, fmt.Errorf("edId and season_id are both empty")
}
req, err := c.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
info := dashPGCResp{}
err = json.NewDecoder(resp.Body).Decode(&info)
if err != nil {
return nil, err
}
if info.Code != 0 {
return nil, errors.New(info.Message)
}
m := mpd.NewMPD(mpd.DASH_PROFILE_ONDEMAND, fmt.Sprintf("PT%.2fS", info.Result.Dash.Duration), fmt.Sprintf("PT%.2fS", info.Result.Dash.MinBufferTime))
var as *mpd.AdaptationSet
for _, v := range info.Result.Dash.Video {
as, err = m.AddNewAdaptationSetVideo(v.MimeType, "progressive", true, v.StartWithSap)
if err != nil {
return nil, err
}
video, err := as.AddNewRepresentationVideo(v.Bandwidth, v.Codecs, fmt.Sprint(time.Now().UnixMicro()), v.FrameRate, v.Width, v.Height)
if err != nil {
return nil, err
}
video.Sar = &v.Sar
err = video.AddNewBaseURL(v.BaseURL)
if err != nil {
return nil, err
}
_, err = video.AddNewSegmentBase(v.SegmentBase.IndexRange, v.SegmentBase.Initialization)
if err != nil {
return nil, err
}
}
as = nil
for _, a := range info.Result.Dash.Audio {
as, err = m.AddNewAdaptationSetAudio(a.MimeType, true, a.StartWithSap, "und")
if err != nil {
return nil, err
}
audio, err := as.AddNewRepresentationAudio(44100, a.Bandwidth, a.Codecs, strconv.Itoa(a.ID))
if err != nil {
return nil, err
}
audio.Sar = &a.Sar
err = audio.AddNewBaseURL(a.BaseURL)
if err != nil {
return nil, err
}
_, err = audio.AddNewSegmentBase(a.SegmentBase.IndexRange, a.SegmentBase.Initialization)
if err != nil {
return nil, err
}
}
return m, nil
}

Loading…
Cancel
Save