eric2788 / tempermonkey-scripts

所有我自製的油猴腳本庫
MIT License
7 stars 2 forks source link

B站随录问题报告 #16

Closed kindmeet closed 11 months ago

kindmeet commented 11 months ago

点击完,显示“寻找线路中”

eric2788 commented 11 months ago

线路API可能改了,又或者需要登入了,我有空看看

eric2788 commented 11 months ago

能打开F12 console看看debug信息?

kindmeet commented 11 months ago

------------------ 原始邮件 ------------------ 发件人: "Eric @.>; 发送时间: 2023年10月30日(星期一) 晚上11:49 收件人: @.>; 抄送: @.>; @.>; 主题: Re: [eric2788/tempermonkey-scripts] B站随录问题报告 (Issue #16)

能打开F12 console看看debug信息?

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you authored the thread.Message ID: @.***>

kindmeet commented 11 months ago

我发送了一张图片 您能看到么

------------------ 原始邮件 ------------------ 发件人: "Eric @.>; 发送时间: 2023年10月30日(星期一) 晚上11:48 收件人: @.>; 抄送: @.>; @.>; 主题: Re: [eric2788/tempermonkey-scripts] B站随录问题报告 (Issue #16)

线路API可能改了,又或者需要登入了,我有空看看

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you authored the thread.Message ID: @.***>

eric2788 commented 11 months ago

umm 没有图片

kindmeet commented 11 months ago

又发了一次 能看到吗

------------------ 原始邮件 ------------------ 发件人: "eric2788/tempermonkey-scripts" @.>; 发送时间: 2023年10月30日(星期一) 晚上11:49 @.>; @.**@.>; 主题: Re: [eric2788/tempermonkey-scripts] B站随录问题报告 (Issue #16)

能打开F12 console看看debug信息?

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you authored the thread.Message ID: @.***>

eric2788 commented 11 months ago

不行 你试试把图片拖拽到github回复框?

kindmeet commented 11 months ago

------------------ 原始邮件 ------------------ 发件人: "eric2788/tempermonkey-scripts" @.>; 发送时间: 2023年10月31日(星期二) 凌晨0:12 @.>; @.**@.>; 主题: Re: [eric2788/tempermonkey-scripts] B站随录问题报告 (Issue #16)

不行 你试试把图片拖拽到github回复框?

— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you authored the thread.Message ID: @.***>

从QQ邮箱发来的超大附件

寻找线路中.png (409.5K, 无限期)进入下载页面:https://mail.qq.com/cgi-bin/ftnExs_download?k=7a38646419f89d9a323b2e5d1462564a414c56565307050714085105564f52570e0e495d02015548015e5501535a005c085b5151326c64b489edb6abeda0d3b3e916140a556259&t=exs_ftn_download&code=98dd2bde

kindmeet commented 11 months ago

GitHub拖进来好像不行,我用邮箱发了一个超大附件。

eric2788 commented 11 months ago

复制图片再贴上呢 应该会生成一个markdown图片

kindmeet commented 11 months ago

寻找线路中

eric2788 commented 11 months ago

顺带给我直播间地址 我明天看看

kindmeet commented 11 months ago

https://live.bilibili.com/23307841?live_from=71002&visit_id=4rwfrw0htze0

kindmeet commented 11 months ago

https://space.bilibili.com/289254911/ 这是个人空间地址

kindmeet commented 11 months ago

辛苦您了。

eric2788 commented 11 months ago

看來要直播的時候才能測試了,我昨晚嘗試了別的正在直播的直播間,貌似沒有問題

kindmeet commented 11 months ago

我刚才用了一下,我发现有的可以有的不行,有点奇怪。

kindmeet commented 11 months ago

https://live.bilibili.com/5619438?live_from=71002&visit_id=7sky8q904ao0 这人的就不行,这个分区内我点了前几个 有的可以 有的不行。

eric2788 commented 11 months ago

https://live.bilibili.com/5619438?live_from=71002&visit_id=7sky8q904ao0 这人的就不行,这个分区内我点了前几个 有的可以 有的不行。

画质如何?

kindmeet commented 11 months ago

https://live.bilibili.com/5619438?live_from=71002&visit_id=7sky8q904ao0 这人的就不行,这个分区内我点了前几个 有的可以 有的不行。

画质如何?

“高清”和“原画”我都试了一下,都显示“寻找线路中”

eric2788 commented 11 months ago

只有高清和原画对吧,这大概是问题,因为我脚本默认是寻找蓝光以上的 你看看是不是不行的直播间都没有蓝光以上的画质选

kindmeet commented 11 months ago

只有高清和原画对吧,这大概是问题,因为我脚本默认是寻找蓝光以上的 你看看是不是不行的直播间都没有蓝光以上的画质选

好像是的,我测了十个,我这边是只有“原画”的 可以,有“原画”和“高清”两个选项的直播间 不可以。

kindmeet commented 11 months ago

你看看是不是不行的直播间都没有蓝光画质选

这个后面会增加高清的吗,大佬。

eric2788 commented 11 months ago

你看看是不是不行的直播间都没有蓝光画质选

这个后面会增加高清的吗,大佬。

给我一个正常的直播间号码,我试试

kindmeet commented 11 months ago

https://live.bilibili.com/5619438?live_from=71002&visit_id=7sky8q904ao0

你看看是不是不行的直播间都没有蓝光画质选

这个后面会增加高清的吗,大佬。

给我一个正常的直播间号码,我试试

https://live.bilibili.com/5619438?live_from=71002&visit_id=7sky8q904ao0

eric2788 commented 11 months ago

https://live.bilibili.com/5619438?live_from=71002&visit_id=7sky8q904ao0

你看看是不是不行的直播间都没有蓝光画质选

这个后面会增加高清的吗,大佬。

给我一个正常的直播间号码,我试试

https://live.bilibili.com/5619438?live_from=71002&visit_id=7sky8q904ao0

5619438 不是不行的那个么

kindmeet commented 11 months ago

噢sorry 是要正常的 https://live.bilibili.com/21332276?session_id=2602d6577145f83b7b15eac70a65407e_E66E862F-1245-4A90-A87D-6F071B1A523D&launch_id=1000216&live_from=71001

eric2788 commented 11 months ago
// ==UserScript==
// @name         B站直播随看随录
// @namespace    http://tampermonkey.net/
// @version      0.9
// @description  无需打开弹幕姬,必要时直接录制的快速切片工具
// @author       Eric Lam
// @license      MIT
// @include      /https?:\/\/live\.bilibili\.com\/(blanc\/)?\d+\??.*/
// @require      https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js
// @grant        none
// ==/UserScript==

class StreamUrlGetter {

    constructor() {
        if (this.constructor == StreamUrlGetter){
            throw new Error('cannot initialize abstract class')
        }
    }

    async getUrl(roomid){
    }

}

let enableIndexedDB = false;
let limit1gb = false;

(async function() {
    'use strict';
    const uidRegex = /\/\/space\.bilibili\.com\/(?<id>\d+)\//g
    const roomLink =  $('.room-owner-username').attr('href')
    const uid = uidRegex.exec(roomLink)?.groups?.id

    const roomReg = /^\/(blanc\/)?(?<id>\d+)/
    let roomId = parseInt(roomReg.exec(location.pathname)?.groups?.id)

    let res = await fetcher('https://api.live.bilibili.com/room/v1/Room/room_init?id='+roomId)
    roomId = res.data.room_id

    console.log('正在测试获取B站直播流')

    if (res.data.live_status != 1){
        console.log('此房间目前没有直播')
        return
    }

    // ========= indexdb 操作 =========================
    const key = `stream_record.${roomId}`

    if (window.indexedDB){
       try {
           await connect(key)
           enableIndexedDB = true
       }catch(err){
          console.error(err)
          alert(`連接資料庫時出現錯誤: ${err.message}, 没办法使用 IndexedDB。(尝试刷新?)`)
          closeDatabase()
       }
    }else{
        alert('你的瀏覽器不支援IndexedDB。')
    }

    if (!enableIndexedDB) {
        limit1gb = confirm('由于 IndexedDB 无法被使用,是否应该限制每次最多录制 1gb 视频以防止浏览器崩溃?')
    }

    // ======== 更改方式实作 , 如无法寻找可以更改别的 class =====
    const urlGetter = new RoomPlayInfo()
    // ===================================================

    const rows = $('.rows-ctnr')
    rows.append(`<button id="record">开始录制</button>`)

    //刷新一次可用线路
    //await findSuitableURL(stream_urls)

    $('#record').on('click', async () => {
        try {
            if (stop_record){
                const startDate = new Date().toString().substring(0, 24).replaceAll(' ', '-').replaceAll(':', '-')
                startRecord(urlGetter, roomId).then(data => download_flv(data, `${roomId}-${startDate}.flv`)).catch(err => { throw new Error(err) })
            }else{
               stopRecord()
            }
        }catch(err){
          alert(`啟用录制时出现错误: ${err?.message ?? err}`)
          console.error(err)
        }
    })

})().catch(console.warn);

async function findSuitableURL(stream_urls){
   for (const stream_url of stream_urls){
        try {
           await testUrlValid(stream_url)
           console.log(`找到可用线路: ${stream_url}`)
           return stream_url
        }catch(err){
          console.warn(`测试线路 ${stream_url} 时出现错误: ${err}, 寻找下一个节点`)
        }
    }
   return undefined
}

async function fetcher(url) {
    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), 5000); // 五秒timeout
    const res = await fetch(url, { signal: controller.signal })
    clearTimeout(id)
    if (!res.ok){
        throw new Error(res.statusText)
    }

    const data = await res.json()
    console.debug(data)
    if (data.code != 0){
        throw new Error(`B站API请求错误: ${data.message}`)
    }
    return data
}

let stop_record = true
let timer_interval = -1

async function testUrlValid(url){
  const res = await fetch(url, { credentials: 'same-origin' })
  if (!res.ok){
     throw new Error(res.statusText)
  }
}

function toTimer(secs){
    let min = 0;
    let hr = 0;
    while(secs >= 60){
        secs -= 60
        min++
    }
    while (min >= 60){
        min -= 60
        hr++
    }
    const mu = min > 9 ? `${min}`: `0${min}`
    const ms = secs > 9 ? `${secs}` : `0${secs}`
    return `${hr}:${mu}:${ms}`
}

function isFlvHeader(buf) {
    if (!buf || buf.length < 4) {
        return false;
    }
    return buf[0] === 0x46 && buf[1] === 0x4c && buf[2] === 0x56 && buf[3] === 0x01;
}

let symbol = '🔴'
function startTimer(){
  let seconds = 0
  timer_interval = setInterval(() => {
     seconds += 1
     symbol = seconds % 2 == 0 ? '🔴' : '⚪'
  }, 1000)
}

function stopTimer() {
   clearInterval(timer_interval)
   $('#record')[0].innerText = '开始录制'
}

function round(float){
  return Math.round(float * 10) / 10
}

function formatSize(size) {
  const mb = round(size/1024/1024)
  if (mb > 1000){
     return `${round(mb / 1000).toFixed(1)}GB`
  }else{
     return `${mb.toFixed(1)}MB`
  }
}

const banned_urls = new Set();

async function startRecord(urlGetter, roomId) {
    await clearRecords() // 清空之前的记录

    $('#record').attr('disabled', '')
    $('#record')[0].innerText = '寻找线路中'

    const urls = await urlGetter.getUrl(roomId)

    if (urls.length == 0){
        throw new Error('没有可用线路,稍后再尝试?')
    }

    let res = undefined
    for (const url of urls) {
       try {
          console.log('正在测试目前线路...')
          if (banned_urls.has(url)) {
            console.warn('该线路在黑名单内,已略过')
            continue
          }
          const controller = new AbortController();
          const id = setTimeout(() => controller.abort(), 1000);
          res = await fetch(url, { credentials: 'same-origin', signal: controller.signal })
          clearTimeout(id)
          if (res.ok && !res.bodyUsed) break
       }catch(err){
           console.warn(`使用线路 ${url} 时出现错误: ${err}, 使用下一个节点`)
       }
    }
    if (!res) {
      throw new Error('没有可用线路,稍后再尝试?')
    }
    console.log('线路请求成功, 正在开始录制')
    startTimer()
    const reader = res.body.getReader();
    stop_record = false
    const chunks = [] // 不支援 indexeddb 时采用
    let size = 0
    console.log('录制已经开始...')
    $('#record').removeAttr('disabled')
    while (!stop_record){
      const {done, value } = await reader.read()
      // 下播
      if (done){
         if (size == 0) {
            banned_urls.add(res.url)
            throw new Error('此线路不可用,请再尝试一次。')
         }
         stop_record = true
         break
      }
      size += value.length
      $('#record')[0].innerText = `${symbol}录制中(${formatSize(size)})` // hover 显示目前录制视频大小
      const blob = new Blob([value], { type: 'application/octet-stream'})
      if (enableIndexedDB){
         await pushRecord(blob)
      }else{
         chunks.push(blob)
         if (limit1gb && round(size/1024/1024) > 1000){ // 采用非 indexeddb 且启用了限制 1gb 大小录制
            stop_record = true
            break
         }
      }
    }
    stopTimer()
    console.log('录制已中止。')
    if (enableIndexedDB){
       return await pollRecords()
    }else{
       return chunks
    }
}

async function stopRecord(){
   stop_record = true
}

function download_flv(chunks, file = 'test.flv'){
  if (!chunks || chunks.length == 0){
     console.warn('没有可以下载的资料')
     alert('没有可以下载的资料')
     return
  }
  const blob = new Blob(chunks, { type: 'video/x-flv' }, file)
  const url = window.URL.createObjectURL(blob)
  const a = document.createElement('a');
  a.style.display = "none";
  a.setAttribute("href", url);
  a.setAttribute("download", file);
  document.body.appendChild(a);
  a.click();
  window.URL.revokeObjectURL(url);
  a.remove();
}

class RoomPlayUrl extends StreamUrlGetter {

    async getUrl(roomid){
        const stream_urls = []
        const res = await fetcher(`http://api.live.bilibili.com/room/v1/Room/playUrl?cid=${roomid}&qn=${qn}`)

        const durls = res.data.durl
        if (durls.length == 0){
            console.warn('没有可用的直播视频流')
            return stream_urls
        }

        for (const durl of durls){
            stream_urls.push(durl.url)
        }

        return stream_urls
    }
}

class RoomPlayInfo extends StreamUrlGetter {

    async getUrl(roomid){
        const stream_urls = []
        const url = `https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?room_id=${roomid}&protocol=0,1&format=0,2&codec=0,1&qn=10000&platform=web&ptype=16`
       const res = await fetcher(url)

       if (res.data.is_hidden){
           console.warn('此直播間被隱藏')
           return stream_urls
       }

        if (res.data.is_locked){
            console.warn('此直播間已被封鎖')
            return stream_urls
        }

        if (res.data.encrypted && !res.data.pwd_verified){
            console.warn('此直播間已被上鎖')
            return stream_urls
        }

        const streams = res?.data?.playurl_info?.playurl?.stream ?? []
        if (streams.length == 0){
            console.warn('没有可用的直播视频流')
            return stream_urls
        }

        for (const st of streams){
            for (const format of st.format){
                if (format.format_name !== 'flv'){
                    console.warn(`线路 ${index} 格式 ${f_index} 并不是 flv, 已经略过`)
                    continue
                }

                for (const codec of format.codec.sort((a,b) => b.current_qn - a.current_qn)){
                     const base_url = codec.base_url
                     for (const url_info of codec.url_info){
                         const real_url = url_info.host + base_url + url_info.extra
                         stream_urls.push(real_url)
                     }
                }

                return stream_urls
            }

        }
    }

}

// ========== indexdb ==========

function log(msg){
    console.log(`[IndexedDB] ${msg}`)
}

let db = undefined
const storeName = 'stream_record'

async function connect(key){
    return new Promise((res, rej) => {
        const open = window.indexedDB.open(key, 1)
        log('connecting to indexedDB')
        open.onerror = function(event){
            log('connection error: '+event.target.error.message)
            rej(event.target.error)
        }
        open.onsuccess = function(event){
            db = open.result
            log('connection success')
            createObjectStoreIfNotExist(db, rej)
            res(event)
        }
        open.onupgradeneeded = function(event) {
            db = event.target.result;
            log('connection success on upgrade needed')
            createObjectStoreIfNotExist(db, rej)
            res(event.target.error)
        }
    })

}

function closeDatabase(){
    db?.close()
}

async function drop(key){
    return new Promise((res, rej) => {
        const req = window.indexedDB.deleteDatabase(key);
        req.onsuccess = function () {
            log("Deleted database successfully");
            res()
        };
        req.onerror = function () {
            log("Couldn't delete database");
            rej(req.error)
        };
        req.onblocked = function () {
            log("Couldn't delete database due to the operation being blocked");
            rej(req.error)
        };
    })
}

function createObjectStoreIfNotExist(db, rej){
    if(!db) return
    try{
        if (!db.objectStoreNames.contains(storeName)) {
            log(`objectStore ${storeName} does not exist, creating new one.`)
            db.createObjectStore(storeName, { autoIncrement: true })
            log('successfully created.')
        }
    }catch(err){
        log('error while creating object store: '+err.message)
        rej(err)
    }
    db.onerror = function(event) {
        log("Database error: " + event.target.error.message);
    }
    db.onclose = () => {
        console.log('Database connection closed');
    }
}

async function pushRecord(object){
   return new Promise((res, rej)=>{
        if (!db){
            log('db not defined, so skipped')
            rej(new Error('db is not defined'))
        }
        try{
            const tran = db.transaction([storeName], 'readwrite')
            handleTrans(rej, tran)
            const s = tran.objectStore(storeName).add(object)
            s.onsuccess = (e) => {
                //log('pushing successful')
                res(e)
            }
            s.onerror = () => {
                log('error while adding byte: '+s.error.message)
                rej(s.error)
            }
        }catch(err){
            rej(err)
        }
   })
 }

 function handleTrans(rej, tran){
    tran.oncomplete = function(){
        //log('transaction completed')
    }
    tran.onerror = function(){
        log('transaction error: '+tran.error.message)
        rej(tran.error)
    }
 }

async function pollRecords(){
    const buffer = await listRecords()
    await clearRecords()
    return buffer
}

async function listRecords(){
   return new Promise((res, rej) => {
    if (!db){
        log('db not defined, so skipped')
        rej(new Error('db is not defined'))
      }
      try{
        const tran = db.transaction([storeName], 'readwrite')
        handleTrans(rej, tran)
        const cursors = tran.objectStore(storeName).openCursor()
        const records = []
        cursors.onsuccess = function(event){
           let cursor = event.target.result;
           if (cursor) {
              records.push(cursor.value)
              cursor.continue();
           }
           else {
             log("total bytes: "+records.length);
             res(records)
           }
        }
        cursors.onerror = function(){
            log('error while fetching data: '+cursors.error.message)
            rej(cursors.error)
        }
      }catch(err){
          rej(err)
      }
   })
 }

async function clearRecords(){
   return new Promise((res, rej) => {
        if (!db){
            log('db not defined, so skipped')
            rej(new Error('db is not defined'))
        }
       try{
            const tran = db.transaction([storeName], 'readwrite')
            handleTrans(rej, tran)
            const req = tran.objectStore(storeName).clear()
            req.onsuccess = (e) => {
            log('clear success')
            res(e)
            }
            req.onerror = () =>{
                log('error while clearing data: '+req.error.message)
                rej(req.error)
            }
       }catch(err){
           rej(err)
       }
   })
}

试试用这个看看能否所有直播间都能获取

kindmeet commented 11 months ago

还是不行,并且 正常的 也不行了

eric2788 commented 11 months ago

还是不行,并且 正常的 也不行了

看看F12?

kindmeet commented 11 months ago

正常的f12 这是 原来正常的。

kindmeet commented 11 months ago

原来不正常的f12 这是 原来 就不行的。

eric2788 commented 11 months ago
// ==UserScript==
// @name         B站直播随看随录
// @namespace    http://tampermonkey.net/
// @version      0.9
// @description  无需打开弹幕姬,必要时直接录制的快速切片工具
// @author       Eric Lam
// @license      MIT
// @include      /https?:\/\/live\.bilibili\.com\/(blanc\/)?\d+\??.*/
// @require      https://cdn.jsdelivr.net/npm/jquery@3.5.1/dist/jquery.min.js
// @grant        none
// ==/UserScript==

class StreamUrlGetter {

    constructor() {
        if (this.constructor == StreamUrlGetter){
            throw new Error('cannot initialize abstract class')
        }
    }

    async getUrl(roomid){
    }

}

let enableIndexedDB = false;
let limit1gb = false;

(async function() {
    'use strict';
    const uidRegex = /\/\/space\.bilibili\.com\/(?<id>\d+)\//g
    const roomLink =  $('.room-owner-username').attr('href')
    const uid = uidRegex.exec(roomLink)?.groups?.id

    const roomReg = /^\/(blanc\/)?(?<id>\d+)/
    let roomId = parseInt(roomReg.exec(location.pathname)?.groups?.id)

    let res = await fetcher('https://api.live.bilibili.com/room/v1/Room/room_init?id='+roomId)
    roomId = res.data.room_id

    console.log('正在测试获取B站直播流')

    if (res.data.live_status != 1){
        console.log('此房间目前没有直播')
        return
    }

    // ========= indexdb 操作 =========================
    const key = `stream_record.${roomId}`

    if (window.indexedDB){
       try {
           await connect(key)
           enableIndexedDB = true
       }catch(err){
          console.error(err)
          alert(`連接資料庫時出現錯誤: ${err.message}, 没办法使用 IndexedDB。(尝试刷新?)`)
          closeDatabase()
       }
    }else{
        alert('你的瀏覽器不支援IndexedDB。')
    }

    if (!enableIndexedDB) {
        limit1gb = confirm('由于 IndexedDB 无法被使用,是否应该限制每次最多录制 1gb 视频以防止浏览器崩溃?')
    }

    // ======== 更改方式实作 , 如无法寻找可以更改别的 class =====
    const urlGetter = new RoomPlayInfo()
    // ===================================================

    const rows = $('.rows-ctnr')
    rows.append(`<button id="record">开始录制</button>`)

    //刷新一次可用线路
    //await findSuitableURL(stream_urls)

    $('#record').on('click', async () => {
        try {
            if (stop_record){
                const startDate = new Date().toString().substring(0, 24).replaceAll(' ', '-').replaceAll(':', '-')
                startRecord(urlGetter, roomId).then(data => download_flv(data, `${roomId}-${startDate}.flv`)).catch(err => { throw new Error(err) })
            }else{
               stopRecord()
            }
        }catch(err){
          alert(`啟用录制时出现错误: ${err?.message ?? err}`)
          console.error(err)
        }
    })

})().catch(console.warn);

async function findSuitableURL(stream_urls){
   for (const stream_url of stream_urls){
        try {
           await testUrlValid(stream_url)
           console.log(`找到可用线路: ${stream_url}`)
           return stream_url
        }catch(err){
          console.warn(`测试线路 ${stream_url} 时出现错误: ${err}, 寻找下一个节点`)
        }
    }
   return undefined
}

async function fetcher(url) {
    const controller = new AbortController();
    const id = setTimeout(() => controller.abort(), 5000); // 五秒timeout
    const res = await fetch(url, { signal: controller.signal })
    clearTimeout(id)
    if (!res.ok){
        throw new Error(res.statusText)
    }

    const data = await res.json()
    console.debug(data)
    if (data.code != 0){
        throw new Error(`B站API请求错误: ${data.message}`)
    }
    return data
}

let stop_record = true
let timer_interval = -1

async function testUrlValid(url){
  const res = await fetch(url, { credentials: 'same-origin' })
  if (!res.ok){
     throw new Error(res.statusText)
  }
}

function toTimer(secs){
    let min = 0;
    let hr = 0;
    while(secs >= 60){
        secs -= 60
        min++
    }
    while (min >= 60){
        min -= 60
        hr++
    }
    const mu = min > 9 ? `${min}`: `0${min}`
    const ms = secs > 9 ? `${secs}` : `0${secs}`
    return `${hr}:${mu}:${ms}`
}

function isFlvHeader(buf) {
  if (!buf || buf.length < 4) {
      return false;
  }
  return buf[0] === 0x46 && buf[1] === 0x4c && buf[2] === 0x56 && buf[3] === 0x01;
}

let symbol = '🔴'
function startTimer(){
  let seconds = 0
  timer_interval = setInterval(() => {
     seconds += 1
     symbol = seconds % 2 == 0 ? '🔴' : '⚪'
  }, 1000)
}

function stopTimer() {
   clearInterval(timer_interval)
   $('#record')[0].innerText = '开始录制'
}

function round(float){
  return Math.round(float * 10) / 10
}

function formatSize(size) {
  const mb = round(size/1024/1024)
  if (mb > 1000){
     return `${round(mb / 1000).toFixed(1)}GB`
  }else{
     return `${mb.toFixed(1)}MB`
  }
}

const banned_urls = new Set();

async function startRecord(urlGetter, roomId) {
    await clearRecords() // 清空之前的记录

    $('#record').attr('disabled', '')
    $('#record')[0].innerText = '寻找线路中'

    const urls = await urlGetter.getUrl(roomId)

    if (urls.length == 0){
        throw new Error('没有可用线路,稍后再尝试?')
    }

    let res = undefined
    for (const url of urls) {
       try {
          console.log('正在测试目前线路...')
          if (banned_urls.has(url)) {
            console.warn('该线路在黑名单内,已略过')
            continue
          }
          const controller = new AbortController();
          const id = setTimeout(() => controller.abort(), 1000);
          res = await fetch(url, { credentials: 'same-origin', signal: controller.signal })
          clearTimeout(id)
          if (res.ok && !res.bodyUsed) break
       }catch(err){
           console.warn(`使用线路 ${url} 时出现错误: ${err}, 使用下一个节点`)
       }
    }
    if (!res) {
      throw new Error('没有可用线路,稍后再尝试?')
    }
    console.log('线路请求成功, 正在开始录制')
    startTimer()
    const reader = res.body.getReader();
    stop_record = false
    const chunks = [] // 不支援 indexeddb 时采用
    let size = 0
    console.log('录制已经开始...')
    $('#record').removeAttr('disabled')
    while (!stop_record){
      const {done, value } = await reader.read()
      // 下播
      if (done){
         if (size == 0) {
            banned_urls.add(res.url)
            throw new Error('此线路不可用,请再尝试一次。')
         }
         stop_record = true
         break
      }
      size += value.length
      $('#record')[0].innerText = `${symbol}录制中(${formatSize(size)})` // hover 显示目前录制视频大小
      const blob = new Blob([value], { type: 'application/octet-stream'})
      if (enableIndexedDB){
         await pushRecord(blob)
      }else{
         chunks.push(blob)
         if (limit1gb && round(size/1024/1024) > 1000){ // 采用非 indexeddb 且启用了限制 1gb 大小录制
            stop_record = true
            break
         }
      }
    }
    stopTimer()
    console.log('录制已中止。')
    if (enableIndexedDB){
       return await pollRecords()
    }else{
       return chunks
    }
}

async function stopRecord(){
   stop_record = true
}

function download_flv(chunks, file = 'test.flv'){
  if (!chunks || chunks.length == 0){
     console.warn('没有可以下载的资料')
     alert('没有可以下载的资料')
     return
  }
  const blob = new Blob(chunks, { type: 'video/x-flv' }, file)
  const url = window.URL.createObjectURL(blob)
  const a = document.createElement('a');
  a.style.display = "none";
  a.setAttribute("href", url);
  a.setAttribute("download", file);
  document.body.appendChild(a);
  a.click();
  window.URL.revokeObjectURL(url);
  a.remove();
}

class RoomPlayUrl extends StreamUrlGetter {

    async getUrl(roomid){
        const stream_urls = []
        const res = await fetcher(`http://api.live.bilibili.com/room/v1/Room/playUrl?cid=${roomid}&qn=${qn}`)

        const durls = res.data.durl
        if (durls.length == 0){
            console.warn('没有可用的直播视频流')
            return stream_urls
        }

        for (const durl of durls){
            stream_urls.push(durl.url)
        }

        return stream_urls
    }
}

class RoomPlayInfo extends StreamUrlGetter {

    async getUrl(roomid){
        const stream_urls = []
        const url = `https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?room_id=${roomid}&protocol=0,1&format=0,2&codec=0,1&qn=10000&platform=web&ptype=16`
       const res = await fetcher(url)

       if (res.data.is_hidden){
           console.warn('此直播間被隱藏')
           return stream_urls
       }

        if (res.data.is_locked){
            console.warn('此直播間已被封鎖')
            return stream_urls
        }

        if (res.data.encrypted && !res.data.pwd_verified){
            console.warn('此直播間已被上鎖')
            return stream_urls
        }

        const streams = res?.data?.playurl_info?.playurl?.stream ?? []
        if (streams.length == 0){
            console.warn('没有可用的直播视频流')
            return stream_urls
        }

        for (const st of streams){
            for (const format of st.format){
                if (format.format_name !== 'flv'){
                    console.warn(`线路 ${index} 格式 ${f_index} 并不是 flv, 已经略过`)
                    continue
                }

                for (const codec of format.codec.sort((a,b) => b.current_qn - a.current_qn)){
                     const base_url = codec.base_url
                     for (const url_info of codec.url_info){
                         const real_url = url_info.host + base_url + url_info.extra
                         stream_urls.push(real_url)
                     }
                }

                return stream_urls
            }

        }
    }

}

// ========== indexdb ==========

function log(msg){
    console.log(`[IndexedDB] ${msg}`)
}

let db = undefined
const storeName = 'stream_record'

async function connect(key){
    return new Promise((res, rej) => {
        const open = window.indexedDB.open(key, 1)
        log('connecting to indexedDB')
        open.onerror = function(event){
            log('connection error: '+event.target.error.message)
            rej(event.target.error)
        }
        open.onsuccess = function(event){
            db = open.result
            log('connection success')
            createObjectStoreIfNotExist(db, rej)
            res(event)
        }
        open.onupgradeneeded = function(event) {
            db = event.target.result;
            log('connection success on upgrade needed')
            createObjectStoreIfNotExist(db, rej)
            res(event.target.error)
        }
    })

}

function closeDatabase(){
    db?.close()
}

async function drop(key){
    return new Promise((res, rej) => {
        const req = window.indexedDB.deleteDatabase(key);
        req.onsuccess = function () {
            log("Deleted database successfully");
            res()
        };
        req.onerror = function () {
            log("Couldn't delete database");
            rej(req.error)
        };
        req.onblocked = function () {
            log("Couldn't delete database due to the operation being blocked");
            rej(req.error)
        };
    })
}

function createObjectStoreIfNotExist(db, rej){
    if(!db) return
    try{
        if (!db.objectStoreNames.contains(storeName)) {
            log(`objectStore ${storeName} does not exist, creating new one.`)
            db.createObjectStore(storeName, { autoIncrement: true })
            log('successfully created.')
        }
    }catch(err){
        log('error while creating object store: '+err.message)
        rej(err)
    }
    db.onerror = function(event) {
        log("Database error: " + event.target.error.message);
    }
    db.onclose = () => {
        console.log('Database connection closed');
    }
}

async function pushRecord(object){
   return new Promise((res, rej)=>{
        if (!db){
            log('db not defined, so skipped')
            rej(new Error('db is not defined'))
        }
        try{
            const tran = db.transaction([storeName], 'readwrite')
            handleTrans(rej, tran)
            const s = tran.objectStore(storeName).add(object)
            s.onsuccess = (e) => {
                //log('pushing successful')
                res(e)
            }
            s.onerror = () => {
                log('error while adding byte: '+s.error.message)
                rej(s.error)
            }
        }catch(err){
            rej(err)
        }
   })
 }

 function handleTrans(rej, tran){
    tran.oncomplete = function(){
        //log('transaction completed')
    }
    tran.onerror = function(){
        log('transaction error: '+tran.error.message)
        rej(tran.error)
    }
 }

async function pollRecords(){
    const buffer = await listRecords()
    await clearRecords()
    return buffer
}

async function listRecords(){
   return new Promise((res, rej) => {
    if (!db){
        log('db not defined, so skipped')
        rej(new Error('db is not defined'))
      }
      try{
        const tran = db.transaction([storeName], 'readwrite')
        handleTrans(rej, tran)
        const cursors = tran.objectStore(storeName).openCursor()
        const records = []
        cursors.onsuccess = function(event){
           let cursor = event.target.result;
           if (cursor) {
              records.push(cursor.value)
              cursor.continue();
           }
           else {
             log("total bytes: "+records.length);
             res(records)
           }
        }
        cursors.onerror = function(){
            log('error while fetching data: '+cursors.error.message)
            rej(cursors.error)
        }
      }catch(err){
          rej(err)
      }
   })
 }

async function clearRecords(){
   return new Promise((res, rej) => {
        if (!db){
            log('db not defined, so skipped')
            rej(new Error('db is not defined'))
        }
       try{
            const tran = db.transaction([storeName], 'readwrite')
            handleTrans(rej, tran)
            const req = tran.objectStore(storeName).clear()
            req.onsuccess = (e) => {
            log('clear success')
            res(e)
            }
            req.onerror = () =>{
                log('error while clearing data: '+req.error.message)
                rej(req.error)
            }
       }catch(err){
           rej(err)
       }
   })
}

试试用这个看看能否所有直播间都能获取

改好了,再复制一次试试

kindmeet commented 11 months ago

可以了 厉害的 大佬。完美解决。各找了两三个 测试 没问题。

kindmeet commented 11 months ago

大佬你可以更新一下了。