diff --git a/src/live/bilibili/bilibili.go b/src/live/bilibili/bilibili.go index 22c50636..201bc35f 100644 --- a/src/live/bilibili/bilibili.go +++ b/src/live/bilibili/bilibili.go @@ -129,7 +129,7 @@ func (l *Live) GetInfo() (info *live.Info, err error) { return info, nil } -func (l *Live) GetStreamUrls() (us []*url.URL, err error) { +func (l *Live) GetStreamInfos() (infos []*live.StreamUrlInfo, err error) { if l.realID == "" { if err := l.parseRealId(); err != nil { return nil, err @@ -177,7 +177,7 @@ func (l *Live) GetStreamUrls() (us []*url.URL, err error) { if err != nil { return nil, err } - urls := make([]string, 0, 4) + urlStrings := make([]string, 0, 4) addr := "" if l.Options.Quality == 0 && gjson.GetBytes(body, "data.playurl_info.playurl.stream.1.format.1.codec.#").Int() > 1 { @@ -190,18 +190,23 @@ func (l *Live) GetStreamUrls() (us []*url.URL, err error) { gjson.GetBytes(body, addr+".url_info").ForEach(func(_, value gjson.Result) bool { hosts := gjson.Get(value.String(), "host").String() queries := gjson.Get(value.String(), "extra").String() - urls = append(urls, hosts+baseURL+queries) + urlStrings = append(urlStrings, hosts+baseURL+queries) return true }) - return utils.GenUrls(urls...) + urls, err := utils.GenUrls(urlStrings...) + if err != nil { + return nil, err + } + infos = utils.GenUrlInfos(urls, l.getHeadersForDownloader()) + return } func (l *Live) GetPlatformCNName() string { return cnName } -func (l *Live) GetHeadersForDownloader() map[string]string { +func (l *Live) getHeadersForDownloader() map[string]string { agent := biliWebAgent referer := l.GetRawUrl() if l.Options.AudioOnly { diff --git a/src/live/huya/huya.go b/src/live/huya/huya.go index 4d479796..1415faa4 100644 --- a/src/live/huya/huya.go +++ b/src/live/huya/huya.go @@ -1,8 +1,6 @@ package huya import ( - "crypto/md5" - "encoding/hex" "fmt" "net/http" "net/url" @@ -12,13 +10,11 @@ import ( "github.com/hr3lxphr6j/bililive-go/src/live/internal" "github.com/hr3lxphr6j/bililive-go/src/pkg/utils" "github.com/hr3lxphr6j/requests" - "github.com/tidwall/gjson" ) const ( domain = "www.huya.com" cnName = "虎牙" - uaApp = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.49(0x18003137) NetType/WIFI Language/zh_CN WeChat/8.0.49.33 CFNetwork/1474 Darwin/23.0.0" ) func init() { @@ -35,6 +31,22 @@ func (b *builder) Build(url *url.URL, opt ...live.Option) (live.Live, error) { type Live struct { internal.BaseLive + getInfoMethodIndex int + getStreamInfosMethodIndex int + LastCdnIndex int +} + +type GetInfoMethod func(l *Live, body string) (*live.Info, error) +type GetStreamInfosMethod func(l *Live) ([]*live.StreamUrlInfo, error) + +var GetInfoMethodList = []GetInfoMethod{ + GetInfo_ForXingXiu, + GetInfo_ForLol, +} + +var GetStreamInfosMethodList = []GetStreamInfosMethod{ + GetStreamInfos_ForXingXiu, + GetStreamInfos_ForLol, } func (l *Live) GetHtmlBody() (htmlBody string, err error) { @@ -50,38 +62,6 @@ func (l *Live) GetHtmlBody() (htmlBody string, err error) { return } -func (l *Live) getDate(htmlBody string) (result *gjson.Result, err error) { - strFilter := utils.NewStringFilterChain(utils.ParseUnicode, utils.UnescapeHTMLEntity) - rjson := strFilter.Do(utils.Match1(`stream: (\{"data".*?),"iWebDefaultBitRate"`, htmlBody)) + "}" - gj := gjson.Parse(rjson) - - roomId := gj.Get("data.0.gameLiveInfo.profileRoom").String() - params := make(map[string]string) - params["m"] = "Live" - params["do"] = "profileRoom" - params["roomid"] = roomId - params["showSecret"] = "1" - - headers := make(map[string]interface{}) - headers["User-Agent"] = uaApp - headers["xweb_xhr"] = "1" - headers["referer"] = "https://servicewechat.com/wx74767bf0b684f7d3/301/page-frame.html" - headers["accept-language"] = "zh-CN,zh;q=0.9" - resp, err := requests.Get("https://mp.huya.com/cache.php", requests.Headers(headers), requests.Queries(params), requests.UserAgent(uaApp)) - if err != nil { - return nil, err - } - if resp.StatusCode != http.StatusOK { - return nil, live.ErrRoomNotExist - } - body, err := resp.Text() - if err != nil { - return nil, err - } - res := gjson.Parse(body) - return &res, nil -} - func (l *Live) GetInfo() (info *live.Info, err error) { body, err := l.GetHtmlBody() if err != nil { @@ -101,70 +81,41 @@ func (l *Live) GetInfo() (info *live.Info, err error) { }, nil } - res, err := l.getDate(body) - if err != nil { - return nil, err + getInfoMethodCount := len(GetInfoMethodList) + if getInfoMethodCount == 0 { + return nil, fmt.Errorf("no GetInfoMethod") } - if res := utils.Match1("该主播不存在!", res.String()); res != "" { - return nil, live.ErrRoomNotExist + if l.getInfoMethodIndex >= getInfoMethodCount { + l.getInfoMethodIndex = 0 } - var ( - hostName = res.Get("data.liveData.nick").String() - roomName = res.Get("data.liveData.introduction").String() - status = res.Get("data.realLiveStatus").String() - ) - - if hostName == "" || roomName == "" || status == "" { - return nil, live.ErrInternalError - } - - info = &live.Info{ - Live: l, - HostName: hostName, - RoomName: roomName, - Status: status == "ON", - } - return info, nil -} - -func GetMD5Hash(text string) string { - hash := md5.Sum([]byte(text)) - return hex.EncodeToString(hash[:]) + info, err = GetInfoMethodList[l.getInfoMethodIndex](l, body) + l.getInfoMethodIndex++ + return } -func (l *Live) GetStreamUrls() (us []*url.URL, err error) { - body, err := l.GetHtmlBody() - if err != nil { - return nil, err - } - - data, err := l.getDate(body) - if err != nil { - return nil, err +func (l *Live) GetStreamInfos() (infos []*live.StreamUrlInfo, err error) { + getStreamUrlsMethodCount := len(GetStreamInfosMethodList) + if getStreamUrlsMethodCount == 0 { + return nil, fmt.Errorf("no GetStreamUrlsMethod") } - sFlvUrl := data.Get("data.stream.baseSteamInfoList.0.sFlvUrl").String() - sStreamName := data.Get("data.stream.baseSteamInfoList.0.sStreamName").String() - sFlvUrlSuffix := data.Get("data.stream.baseSteamInfoList.0.sFlvUrlSuffix").String() - sFlvAntiCode := data.Get("data.stream.baseSteamInfoList.0.sFlvAntiCode").String() - streamUrl := fmt.Sprintf("%s/%s.%s?%s", sFlvUrl, sStreamName, sFlvUrlSuffix, sFlvAntiCode) - res, err := utils.GenUrls(streamUrl) - if err != nil { - return nil, err + if l.getStreamInfosMethodIndex >= getStreamUrlsMethodCount { + l.getStreamInfosMethodIndex = 0 } - return res, nil + infos, err = GetStreamInfosMethodList[l.getStreamInfosMethodIndex](l) + l.getStreamInfosMethodIndex++ + return } func (l *Live) GetPlatformCNName() string { return cnName } -func (l *Live) GetHeadersForDownloader() map[string]string { +func getGeneralHeadersForDownloader() map[string]string { return map[string]string{ - "User-Agent": uaApp, "Accept": `text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8`, "Accept-Encoding": `gzip, deflate`, "Accept-Language": `zh-CN,zh;q=0.8,en-US;q=0.5,en;q=0.3`, diff --git a/src/live/huya/implForLol.go b/src/live/huya/implForLol.go new file mode 100644 index 00000000..a0164cce --- /dev/null +++ b/src/live/huya/implForLol.go @@ -0,0 +1,188 @@ +package huya + +import ( + "bytes" + "crypto/md5" + "encoding/base64" + "encoding/hex" + "fmt" + "math/rand" + "net/http" + "net/url" + "strconv" + "strings" + "text/template" + "time" + + "github.com/hr3lxphr6j/bililive-go/src/live" + "github.com/hr3lxphr6j/bililive-go/src/pkg/utils" + "github.com/hr3lxphr6j/requests" + "github.com/tidwall/gjson" +) + +const uaForLol = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36" + +var downloaderHeadersForLol = func() map[string]string { + headers := getGeneralHeadersForDownloader() + headers["User-Agent"] = uaForLol + return headers +}() + +func GetInfo_ForLol(l *Live, body string) (info *live.Info, err error) { + var ( + strFilter = utils.NewStringFilterChain(utils.ParseUnicode, utils.UnescapeHTMLEntity) + hostName = strFilter.Do(utils.Match1(`"nick":"([^"]*)"`, body)) + roomName = strFilter.Do(utils.Match1(`"introduction":"([^"]*)"`, body)) + status = strFilter.Do(utils.Match1(`"isOn":([^,]*),`, body)) + ) + + if hostName == "" || roomName == "" || status == "" { + return nil, live.ErrInternalError + } + + info = &live.Info{ + Live: l, + HostName: hostName, + RoomName: roomName, + Status: status == "true", + } + return info, nil +} + +func GetStreamInfos_ForLol(l *Live) (infos []*live.StreamUrlInfo, err error) { + resp, err := requests.Get(l.Url.String(), requests.UserAgent(uaForLol)) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("status code: %d", resp.StatusCode) + } + body, err := resp.Text() + if err != nil { + return nil, err + } + + tmpStrings := strings.Split(body, `stream: `) + if len(tmpStrings) < 2 { + return nil, fmt.Errorf("stream json info not found") + } + tmpStreamJsonRawString := strings.Split(tmpStrings[1], `};`) + if len(tmpStreamJsonRawString) < 1 { + return nil, fmt.Errorf("stream json info end not found. stream text: %s", tmpStrings[1]) + } + streamJsonRawString := tmpStreamJsonRawString[0] + if !gjson.Valid(streamJsonRawString) { + return nil, fmt.Errorf("streamJsonRawString not valid") + } + streamJson := gjson.Parse(streamJsonRawString) + vMultiStreamInfoJson := streamJson.Get("vMultiStreamInfo").Array() + if len(vMultiStreamInfoJson) == 0 { + return nil, fmt.Errorf("vMultiStreamInfo not found") + } + + streamInfoJsons := streamJson.Get("data.0.gameStreamInfoList").Array() + if len(streamInfoJsons) == 0 { + return nil, fmt.Errorf("gameStreamInfoList not found") + } + index := l.LastCdnIndex + if index >= len(streamInfoJsons) { + index = 0 + } + l.LastCdnIndex = index + 1 + gameStreamInfoJson := streamInfoJsons[index] + urls, err := getStreamUrlsFromGameStreamInfoJson(gameStreamInfoJson) + if err != nil { + return nil, err + } + return utils.GenUrlInfos(urls, downloaderHeadersForLol), nil +} + +func getStreamUrlsFromGameStreamInfoJson(gameStreamInfoJson gjson.Result) (us []*url.URL, err error) { + // get streamName + sStreamName := gameStreamInfoJson.Get("sStreamName").String() + // get sFlvAntiCode + sFlvAntiCode := gameStreamInfoJson.Get("sFlvAntiCode").String() + // get sFlvUrl + sFlvUrl := gameStreamInfoJson.Get("sFlvUrl").String() + // get random uid + uid := rand.Int63n(99999999999) + 1200000000000 + + query, err := parseAntiCode(sFlvAntiCode, uid, sStreamName) + if err != nil { + return nil, err + } + tmpUrlString := fmt.Sprintf("%s/%s.flv?%s", sFlvUrl, sStreamName, query) + u, err := url.Parse(tmpUrlString) + if err != nil { + return nil, err + } + return []*url.URL{u}, nil +} + +type urlQueryParams struct { + WsSecret string + WsTime string + Seqid string + Ctype string + Ver string + Fs string + U string + T string + Sv string + Sdk_sid string + Codec string +} + +func parseAntiCode(anticode string, uid int64, streamName string) (string, error) { + qr, err := url.ParseQuery(anticode) + if err != nil { + return "", err + } + resultTemplate := template.Must(template.New("urlQuery").Parse( + "wsSecret={{.WsSecret}}" + + "&wsTime={{.WsTime}}" + + "&seqid={{.Seqid}}" + + "&ctype={{.Ctype}}" + + "&ver={{.Ver}}" + + "&fs={{.Fs}}" + + "&u={{.U}}" + + "&t={{.T}}" + + "&sv={{.Sv}}" + + "&sdk_sid={{.Sdk_sid}}" + + "&codec={{.Codec}}", + )) + timeNow := time.Now().Unix() * 1000 + resultParams := urlQueryParams{ + WsSecret: "", + WsTime: qr.Get("wsTime"), + Seqid: strconv.FormatInt(timeNow+uid, 10), + Ctype: qr.Get("ctype"), + Ver: "1", + Fs: qr.Get("fs"), + U: strconv.FormatInt(uid, 10), + T: "100", + Sv: "2405220949", + Sdk_sid: strconv.FormatInt(uid, 10), + Codec: "264", + } + ss := getMD5Hash(fmt.Sprintf("%s|%s|%s", resultParams.Seqid, resultParams.Ctype, resultParams.T)) + + decodeString, _ := base64.StdEncoding.DecodeString(qr.Get("fm")) + fm := string(decodeString) + fm = strings.ReplaceAll(fm, "$0", resultParams.U) + fm = strings.ReplaceAll(fm, "$1", streamName) + fm = strings.ReplaceAll(fm, "$2", ss) + fm = strings.ReplaceAll(fm, "$3", resultParams.WsTime) + + resultParams.WsSecret = getMD5Hash(fm) + var buf bytes.Buffer + if err := resultTemplate.Execute(&buf, resultParams); err != nil { + return "", err + } + return buf.String(), nil +} + +func getMD5Hash(text string) string { + hash := md5.Sum([]byte(text)) + return hex.EncodeToString(hash[:]) +} diff --git a/src/live/huya/implForXingXiu.go b/src/live/huya/implForXingXiu.go new file mode 100644 index 00000000..77a9ccdf --- /dev/null +++ b/src/live/huya/implForXingXiu.go @@ -0,0 +1,104 @@ +package huya + +import ( + "fmt" + "net/http" + + "github.com/hr3lxphr6j/bililive-go/src/live" + "github.com/hr3lxphr6j/bililive-go/src/pkg/utils" + "github.com/hr3lxphr6j/requests" + "github.com/tidwall/gjson" +) + +const uaForXingXiu = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.49(0x18003137) NetType/WIFI Language/zh_CN WeChat/8.0.49.33 CFNetwork/1474 Darwin/23.0.0" + +var downloaderHeadersForXingXiu = func() map[string]string { + headers := getGeneralHeadersForDownloader() + headers["User-Agent"] = uaForXingXiu + return headers +}() + +func GetInfo_ForXingXiu(l *Live, body string) (info *live.Info, err error) { + res, err := getJsonFromBody(body) + if err != nil { + return nil, err + } + + if res := utils.Match1("该主播不存在!", res.String()); res != "" { + return nil, live.ErrRoomNotExist + } + + var ( + hostName = res.Get("data.liveData.nick").String() + roomName = res.Get("data.liveData.introduction").String() + status = res.Get("data.realLiveStatus").String() + ) + + if hostName == "" || roomName == "" || status == "" { + return nil, live.ErrInternalError + } + + info = &live.Info{ + Live: l, + HostName: hostName, + RoomName: roomName, + Status: status == "ON", + } + return info, nil +} + +func GetStreamInfos_ForXingXiu(l *Live) (infos []*live.StreamUrlInfo, err error) { + body, err := l.GetHtmlBody() + if err != nil { + return nil, err + } + + data, err := getJsonFromBody(body) + if err != nil { + return nil, err + } + sFlvUrl := data.Get("data.stream.baseSteamInfoList.0.sFlvUrl").String() + sStreamName := data.Get("data.stream.baseSteamInfoList.0.sStreamName").String() + sFlvUrlSuffix := data.Get("data.stream.baseSteamInfoList.0.sFlvUrlSuffix").String() + sFlvAntiCode := data.Get("data.stream.baseSteamInfoList.0.sFlvAntiCode").String() + streamUrl := fmt.Sprintf("%s/%s.%s?%s", sFlvUrl, sStreamName, sFlvUrlSuffix, sFlvAntiCode) + + res, err := utils.GenUrls(streamUrl) + if err != nil { + return nil, err + } + infos = utils.GenUrlInfos(res, downloaderHeadersForXingXiu) + return infos, nil +} + +func getJsonFromBody(htmlBody string) (result *gjson.Result, err error) { + strFilter := utils.NewStringFilterChain(utils.ParseUnicode, utils.UnescapeHTMLEntity) + rjson := strFilter.Do(utils.Match1(`stream: (\{"data".*?),"iWebDefaultBitRate"`, htmlBody)) + "}" + gj := gjson.Parse(rjson) + + roomId := gj.Get("data.0.gameLiveInfo.profileRoom").String() + params := make(map[string]string) + params["m"] = "Live" + params["do"] = "profileRoom" + params["roomid"] = roomId + params["showSecret"] = "1" + + headers := make(map[string]interface{}) + headers["User-Agent"] = uaForXingXiu + headers["xweb_xhr"] = "1" + headers["referer"] = "https://servicewechat.com/wx74767bf0b684f7d3/301/page-frame.html" + headers["accept-language"] = "zh-CN,zh;q=0.9" + resp, err := requests.Get("https://mp.huya.com/cache.php", requests.Headers(headers), requests.Queries(params), requests.UserAgent(uaForXingXiu)) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, live.ErrRoomNotExist + } + body, err := resp.Text() + if err != nil { + return nil, err + } + res := gjson.Parse(body) + return &res, nil +} diff --git a/src/live/internal/base_live.go b/src/live/internal/base_live.go index 16762ed6..53f9f18f 100644 --- a/src/live/internal/base_live.go +++ b/src/live/internal/base_live.go @@ -52,6 +52,7 @@ func (a *BaseLive) SetLastStartTime(time time.Time) { a.LastStartTime = time } -func (a *BaseLive) GetHeadersForDownloader() map[string]string { - return make(map[string]string) +// TODO: remove this method +func (a *BaseLive) GetStreamInfos() ([]*live.StreamUrlInfo, error) { + return nil, fmt.Errorf("GetStreamInfos() is not implemented") } diff --git a/src/live/lives.go b/src/live/lives.go index 441b07a8..baa1045d 100755 --- a/src/live/lives.go +++ b/src/live/lives.go @@ -100,11 +100,12 @@ func WithAudioOnly(audioOnly bool) Option { type ID string type StreamUrlInfo struct { - Url *url.URL - Name string - Description string - Resolution int - Vbitrate int + Url *url.URL + Name string + Description string + Resolution int + Vbitrate int + HeadersForDownloader map[string]string } type Live interface { @@ -112,11 +113,15 @@ type Live interface { GetLiveId() ID GetRawUrl() string GetInfo() (*Info, error) - GetStreamUrls() ([]*url.URL, error) + GetStreamInfos() ([]*StreamUrlInfo, error) GetPlatformCNName() string GetLastStartTime() time.Time SetLastStartTime(time.Time) - GetHeadersForDownloader() map[string]string +} + +type HasGetStreamUrls interface { + // Deprecated: GetStreamUrls is deprecated, using GetStreamInfos instead + GetStreamUrls() ([]*url.URL, error) } type WrappedLive struct { diff --git a/src/live/mock/mock.go b/src/live/mock/mock.go index ec3bfee5..37a781ff 100644 --- a/src/live/mock/mock.go +++ b/src/live/mock/mock.go @@ -5,7 +5,6 @@ package mock import ( - url "net/url" reflect "reflect" time "time" @@ -36,20 +35,6 @@ func (m *MockLive) EXPECT() *MockLiveMockRecorder { return m.recorder } -// GetHeadersForDownloader mocks base method. -func (m *MockLive) GetHeadersForDownloader() map[string]string { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetHeadersForDownloader") - ret0, _ := ret[0].(map[string]string) - return ret0 -} - -// GetHeadersForDownloader indicates an expected call of GetHeadersForDownloader. -func (mr *MockLiveMockRecorder) GetHeadersForDownloader() *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeadersForDownloader", reflect.TypeOf((*MockLive)(nil).GetHeadersForDownloader)) -} - // GetInfo mocks base method. func (m *MockLive) GetInfo() (*live.Info, error) { m.ctrl.T.Helper() @@ -121,19 +106,19 @@ func (mr *MockLiveMockRecorder) GetRawUrl() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetRawUrl", reflect.TypeOf((*MockLive)(nil).GetRawUrl)) } -// GetStreamUrls mocks base method. -func (m *MockLive) GetStreamUrls() ([]*url.URL, error) { +// GetStreamInfos mocks base method. +func (m *MockLive) GetStreamInfos() ([]*live.StreamUrlInfo, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetStreamUrls") - ret0, _ := ret[0].([]*url.URL) + ret := m.ctrl.Call(m, "GetStreamInfos") + ret0, _ := ret[0].([]*live.StreamUrlInfo) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetStreamUrls indicates an expected call of GetStreamUrls. -func (mr *MockLiveMockRecorder) GetStreamUrls() *gomock.Call { +// GetStreamInfos indicates an expected call of GetStreamInfos. +func (mr *MockLiveMockRecorder) GetStreamInfos() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStreamUrls", reflect.TypeOf((*MockLive)(nil).GetStreamUrls)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetStreamInfos", reflect.TypeOf((*MockLive)(nil).GetStreamInfos)) } // SetLastStartTime mocks base method. diff --git a/src/pkg/parser/ffmpeg/ffmpeg.go b/src/pkg/parser/ffmpeg/ffmpeg.go index a142d8ea..1fadd6dc 100644 --- a/src/pkg/parser/ffmpeg/ffmpeg.go +++ b/src/pkg/parser/ffmpeg/ffmpeg.go @@ -6,7 +6,6 @@ import ( "context" "fmt" "io" - "net/url" "os" "os/exec" "strconv" @@ -126,12 +125,13 @@ func (p *Parser) Status() (map[string]string, error) { return <-p.statusResp, nil } -func (p *Parser) ParseLiveStream(ctx context.Context, url *url.URL, live live.Live, file string) (err error) { +func (p *Parser) ParseLiveStream(ctx context.Context, streamUrlInfo *live.StreamUrlInfo, live live.Live, file string) (err error) { + url := streamUrlInfo.Url ffmpegPath, err := utils.GetFFmpegPath(ctx) if err != nil { return err } - headers := live.GetHeadersForDownloader() + headers := streamUrlInfo.HeadersForDownloader ffUserAgent, exists := headers["User-Agent"] if !exists { ffUserAgent = userAgent @@ -183,7 +183,9 @@ func (p *Parser) ParseLiveStream(ctx context.Context, url *url.URL, live live.Li p.cmd.Stderr = os.Stderr } if err = p.cmd.Start(); err != nil { - p.cmd.Process.Kill() + if p.cmd.Process != nil { + p.cmd.Process.Kill() + } return } }() diff --git a/src/pkg/parser/mock/mock.go b/src/pkg/parser/mock/mock.go index b9912a8f..2be19810 100644 --- a/src/pkg/parser/mock/mock.go +++ b/src/pkg/parser/mock/mock.go @@ -6,7 +6,6 @@ package mock import ( context "context" - url "net/url" reflect "reflect" gomock "github.com/golang/mock/gomock" @@ -37,7 +36,7 @@ func (m *MockParser) EXPECT() *MockParserMockRecorder { } // ParseLiveStream mocks base method. -func (m *MockParser) ParseLiveStream(arg0 context.Context, arg1 *url.URL, arg2 live.Live, arg3 string) error { +func (m *MockParser) ParseLiveStream(arg0 context.Context, arg1 *live.StreamUrlInfo, arg2 live.Live, arg3 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "ParseLiveStream", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(error) diff --git a/src/pkg/parser/native/flv/flv.go b/src/pkg/parser/native/flv/flv.go index d01e980c..8157dce2 100644 --- a/src/pkg/parser/native/flv/flv.go +++ b/src/pkg/parser/native/flv/flv.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "net/http" - "net/url" "os" "runtime/debug" "sync" @@ -73,7 +72,8 @@ type Parser struct { closeOnce *sync.Once } -func (p *Parser) ParseLiveStream(ctx context.Context, url *url.URL, live live.Live, file string) error { +func (p *Parser) ParseLiveStream(ctx context.Context, streamUrlInfo *live.StreamUrlInfo, live live.Live, file string) error { + url := streamUrlInfo.Url // init input req, err := http.NewRequest("GET", url.String(), nil) if err != nil { @@ -81,7 +81,7 @@ func (p *Parser) ParseLiveStream(ctx context.Context, url *url.URL, live live.Li } req.Header.Add("User-Agent", "Chrome/59.0.3071.115") // add headers for downloader from live - for k, v := range live.GetHeadersForDownloader() { + for k, v := range streamUrlInfo.HeadersForDownloader { req.Header.Set(k, v) } resp, err := p.hc.Do(req) diff --git a/src/pkg/parser/parser.go b/src/pkg/parser/parser.go index ca9da43f..ffb4ab3d 100644 --- a/src/pkg/parser/parser.go +++ b/src/pkg/parser/parser.go @@ -4,7 +4,6 @@ package parser import ( "context" "errors" - "net/url" "github.com/hr3lxphr6j/bililive-go/src/live" ) @@ -14,7 +13,7 @@ type Builder interface { } type Parser interface { - ParseLiveStream(ctx context.Context, url *url.URL, live live.Live, file string) error + ParseLiveStream(ctx context.Context, streamUrlInfo *live.StreamUrlInfo, live live.Live, file string) error Stop() error } diff --git a/src/pkg/utils/utils.go b/src/pkg/utils/utils.go index 49b79675..d4a9a7e3 100644 --- a/src/pkg/utils/utils.go +++ b/src/pkg/utils/utils.go @@ -14,6 +14,7 @@ import ( "strings" "github.com/hr3lxphr6j/bililive-go/src/instance" + "github.com/hr3lxphr6j/bililive-go/src/live" ) func GetFFmpegPath(ctx context.Context) (string, error) { @@ -95,6 +96,21 @@ func GenUrls(strs ...string) ([]*url.URL, error) { return urls, nil } +func GenUrlInfos(urls []*url.URL, headersForDownloader map[string]string) []*live.StreamUrlInfo { + infos := make([]*live.StreamUrlInfo, 0, len(urls)) + for _, u := range urls { + infos = append(infos, &live.StreamUrlInfo{ + Url: u, + Name: "", + Description: "", + Resolution: 0, + Vbitrate: 0, + HeadersForDownloader: headersForDownloader, + }) + } + return infos +} + func PrintStack(ctx context.Context) { inst := instance.GetInstance(ctx) logger := inst.Logger diff --git a/src/recorders/recorder.go b/src/recorders/recorder.go index 0753d0ba..14923c3b 100644 --- a/src/recorders/recorder.go +++ b/src/recorders/recorder.go @@ -103,9 +103,34 @@ func NewRecorder(ctx context.Context, live live.Live) (Recorder, error) { }, nil } +func getStreamInfosForDeprecatedImpl(l live.HasGetStreamUrls) ([]*live.StreamUrlInfo, error) { + urls, err := l.GetStreamUrls() + if err != nil { + return nil, err + } + infos := make([]*live.StreamUrlInfo, 0, len(urls)) + for _, u := range urls { + infos = append(infos, &live.StreamUrlInfo{ + Url: u, + Name: "", + Description: "", + Resolution: 0, + Vbitrate: 0, + HeadersForDownloader: make(map[string]string), + }) + } + return infos, nil +} + func (r *recorder) tryRecord(ctx context.Context) { - urls, err := r.Live.GetStreamUrls() - if err != nil || len(urls) == 0 { + var streamInfos []*live.StreamUrlInfo + var err error + if l, ok := r.Live.(live.HasGetStreamUrls); ok { + streamInfos, err = getStreamInfosForDeprecatedImpl(l) + } else { + streamInfos, err = r.Live.GetStreamInfos() + } + if err != nil || len(streamInfos) == 0 { r.getLogger().WithError(err).Warn("failed to get stream url, will retry after 5s...") time.Sleep(5 * time.Second) return @@ -128,7 +153,8 @@ func (r *recorder) tryRecord(ctx context.Context) { } fileName := filepath.Join(r.OutPutPath, buf.String()) outputPath, _ := filepath.Split(fileName) - url := urls[0] + streamInfo := streamInfos[0] + url := streamInfo.Url if strings.Contains(url.Path, "m3u8") { fileName = fileName[:len(fileName)-4] + ".ts" @@ -156,7 +182,7 @@ func (r *recorder) tryRecord(ctx context.Context) { r.setAndCloseParser(p) r.startTime = time.Now() r.getLogger().Debugln("Start ParseLiveStream(" + url.String() + ", " + fileName + ")") - r.getLogger().Println(r.parser.ParseLiveStream(ctx, url, r.Live, fileName)) + r.getLogger().Println(r.parser.ParseLiveStream(ctx, streamInfo, r.Live, fileName)) r.getLogger().Debugln("End ParseLiveStream(" + url.String() + ", " + fileName + ")") removeEmptyFile(fileName) ffmpegPath, err := utils.GetFFmpegPath(ctx)