Skip to content

Commit

Permalink
feat: 加入对m3u8视频流的支持
Browse files Browse the repository at this point in the history
  • Loading branch information
orangelckc committed Jan 27, 2023
1 parent 08bde30 commit 4f77dfe
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 53 deletions.
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bili-bot"
version = "1.2.2"
version = "1.2.3"
description = "哔哩哔哩-直播间管家机器人"
authors = ["半糖人类"]
license = "MIT"
Expand Down
34 changes: 20 additions & 14 deletions src/api/live.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,6 @@ const getLiveStatusApi = async (room_ids: string) =>
}
);

// 获取直播线路
const getLiveStreamApi = async () =>
await getQueryData(
`${LIVE_URL_PREFIX}/xlive/app-blink/v1/live/getWebUpStreamAddr`,
{
query: { platform: "pc" },
headers: { cookie: await getStore(LOGIN_INFO.cookie) }
}
);

// 获取身份码
const getLiveCodeApi = async () =>
await getQueryData(
Expand Down Expand Up @@ -124,8 +114,8 @@ const sendMessageApi = async (message: SendMessage) => {
});
};

// 获取直播视频流
const getLiveStreamUrlApi = async (qn: string = "0", roomid: string) =>
// 获取直播视频流 flv格式
const getLiveFlvUrlApi = async (qn: string = "0", roomid: string) =>
await getQueryData(`${LIVE_URL_PREFIX}/room/v1/Room/playUrl`, {
query: {
cid: roomid,
Expand All @@ -134,6 +124,22 @@ const getLiveStreamUrlApi = async (qn: string = "0", roomid: string) =>
}
});

// 获取直播视频流 m3u8格式
const getLiveM3U8UrlApi = async (qn: string = "0", roomid: string) =>
await getQueryData(`${LIVE_URL_PREFIX}/xlive/web-room/v2/index/getRoomPlayInfo`, {
query: {
device: "pc",
platform: "web",
scale: "3",
build: qn,
protocol: "0,1",
format: "0,1,2",
codec: "0,1",
room_id: roomid
}
})


// 获取关注的主播列表
const getMyFollowLiveInfo = async (page: string = "1") =>
await getQueryData(
Expand All @@ -152,11 +158,11 @@ const getMyFollowLiveInfo = async (page: string = "1") =>
export {
getLiveCategoryApi,
getLiveStatusApi,
getLiveStreamApi,
getLiveCodeApi,
getGiftApi,
getEmojiApi,
sendMessageApi,
getLiveStreamUrlApi,
getLiveFlvUrlApi,
getLiveM3U8UrlApi,
getMyFollowLiveInfo
};
6 changes: 6 additions & 0 deletions src/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,9 @@ export interface SendMessage {
dm_type?: string;
isInitiative?: boolean;
}

export type Stream = {
type: string;
url: string;
ext: string;
};
4 changes: 1 addition & 3 deletions src/utils/cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,11 @@ export const recordPath = async (folder: string) =>
export const testFfmpge = () =>
new Command("ffmpeg-version").execute();

export const newRecorder = async (streamUrl: string, folder: string, description: string) => {
export const newRecorder = async (streamUrl: string, folder: string, description: string, ext: string) => {
// 片段时长
const partDuration = "1800";
// 片段名
const segment = `part%03d`
// 文件后缀
const ext = ".mp4"
// 当前时间
const timestamp = `${dayjs().format("YYYY-MM-DD HH")}点场`;
// 直播录制存储路径
Expand Down
52 changes: 41 additions & 11 deletions src/views/Robot/room.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { connected, startWebsocket, stopWebsocket, active, msgList, manage } from "./message/robot";
import { ROOM_URL_PREFIX } from "@/constants";
import { computed, ref, watch } from "vue";
import { getLiveStreamUrlApi, getLiveStatusApi } from "@/api";
import { getLiveM3U8UrlApi, getLiveStatusApi } from "@/api";
import { newRecorder, testFfmpge, recordPath } from '@/utils/cmd'
import { type Child, type Command } from "@tauri-apps/api/shell";
Expand All @@ -11,7 +11,9 @@ import Video from "./video.vue";
import { message } from "@tauri-apps/api/dialog";
import { open } from "@tauri-apps/api/shell";
const url = ref('');
import { Stream } from '@/types'
const streams = ref<Stream[]>([]);
const isRecording = ref(false)
const getUrl = async () => {
Expand All @@ -22,16 +24,44 @@ const getUrl = async () => {
message(`${uname}直播间未开播!`)
return false
}
const res = await getLiveStreamUrlApi("10000", manage.roomid);
const res = await getLiveM3U8UrlApi('10000', manage.roomid);
if (!res) return;
const urls = res.durl.map((item: any) => item.url);
const urls = [] as Stream[]
res.playurl_info.playurl.stream.forEach((stream: any) => {
if (stream.protocol_name === 'http_hls') {
// m3u8格式
stream.format.forEach((format: any) => {
if (format.format_name === 'ts') {
const { base_url, url_info } = format.codec[0];
urls.push({
type: 'm3u8',
url: `${url_info[0].host}${base_url}${url_info[0].extra}`,
ext: '.mp4'
})
}
});
} else if (stream.protocol_name === 'http_stream') {
// flv格式
stream.format.forEach((format: any) => {
if (format.format_name === 'flv') {
const { base_url, url_info } = format.codec[0];
urls.push({
type: 'flv',
url: `${url_info[0].host}${base_url}${url_info[0].extra}`,
ext: '.mp4'
})
}
});
}
});
return { urls, uname, title };
}
const openPreview = async () => {
const res = await getUrl();
if (!res || !res?.urls.length) return
url.value = res?.urls[0];
streams.value = [...res?.urls];
};
const scrollRef = ref();
Expand Down Expand Up @@ -68,19 +98,19 @@ const startRecord = async (order = 0) => {
}
const res = await getUrl();
if (!res || !res?.urls.length) return
const streamUrl = res?.urls[order]
const streamUrl = res?.urls[order];
const folder = `${res?.uname}-[${manage.roomid}]`
// 创建录制器
recorder = await newRecorder(streamUrl, folder, res?.title)
recorder = await newRecorder(streamUrl?.url, folder, res?.title, streamUrl?.ext)
recorder.on("close", async ({ code }) => {
if (code) {
console.log(`${manage.roomid}链接-${order + 1} 失败`)
if (order === 1) {
console.log(`录制${manage.roomid}-${streamUrl?.type} 视频流失败`)
if (order === res.urls.length - 1) {
await message('录制视频流失败')
isRecording.value = false
return
}
startRecord(1)
startRecord(order + 1)
} else {
await message('录制完成')
open(await recordPath(folder))
Expand Down Expand Up @@ -164,7 +194,7 @@ const stopRecord = async () => {
<q-card class="my-2">
<q-card-section class="flex items-center h-[480px] gap-2">
<div class="max-w-3/5 flex-grow ">
<Video :url="url" v-show="url.length" />
<Video :streams="streams" v-show="streams.length" />
</div>
<div class="flex-grow max-w-2/5">
<q-scroll-area ref="scrollRef" class="h-[350px]" @wheel="scrollControl" :visible="false">
Expand Down
72 changes: 49 additions & 23 deletions src/views/Robot/video.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
<script setup lang="ts">
import flvjs from "flv.js";
import { reactive, ref, watch } from "vue";
import { computed, onUpdated, ref, watch, watchEffect } from "vue";
import { Stream } from '@/types'
import { message } from "@tauri-apps/api/dialog";
const props = defineProps<{
url: string;
streams: Stream[];
}>();
let flvPlayer = reactive<any>({});
let flvPlayer: any = {};
const curStream = ref<Stream>();
let curStreamIndex = 0;
const volume = ref(0.5);
const audioMode = ref(false);
Expand All @@ -30,8 +34,8 @@ const initPlayer = () => {
if (flvjs.isSupported()) {
flvPlayer = flvjs.createPlayer(
{
type: "flv",
url: props.url,
type: curStream.value!.type,
url: curStream.value!.url,
isLive: true,
hasVideo: !audioMode.value,
},
Expand All @@ -47,29 +51,40 @@ const initPlayer = () => {
flvPlayer.play();
flvPlayer.on(flvjs.Events.LOADING_COMPLETE, (data: any) => {
console.log("视频流停止", data);
message("视频流停止");
destroyPlayer();
});
flvPlayer.on(flvjs.Events.ERROR, (data: any) => {
console.log("加载失败", data);
destroyPlayer();
curStreamIndex++;
if (curStreamIndex < props.streams.length) {
curStream.value = props.streams[curStreamIndex]
} else {
message('视频流加载失败')
destroyPlayer();
}
});
flvPlayer.on(flvjs.Events.MEDIA_INFO, (data: any) => {
// 根据视频流的宽高比,设置视频的宽高
const { width, height } = data;
if (width > height) {
// 横屏
videoRef.value.style.width = '100%';
videoRef.value.style.height = 'auto';
const { width, height } = data
if (curStream.value!.type === 'flv') {
// 根据视频流的宽高比,设置视频的宽高
if (width > height) {
// 横屏
videoRef.value.style.width = '100%';
videoRef.value.style.height = 'auto';
} else {
// 竖屏
videoRef.value.style.width = '50%';
videoRef.value.style.height = '100%';
}
} else {
// 竖屏
videoRef.value.style.width = '50%';
videoRef.value.style.height = '100%';
videoRef.value.style.maxHeight = '393px';
videoRef.value.style.width = '100%';
}
});
} else {
message('系统不支持flv.js')
}
};
Expand All @@ -90,15 +105,26 @@ const destroyPlayer = () => {
}
};
watch(props, async () => {
Object.keys(flvPlayer).length && destroyPlayer();
initPlayer();
});
watch(curStream, async (val) => {
await destroyPlayer()
initPlayer()
})
watchEffect(async () => {
if (props.streams.length === 0) return;
await destroyPlayer();
curStreamIndex = 0;
curStream.value = props.streams[curStreamIndex];
// 优先m3u8
// curStream.value = props.streams.find(item => item.type === 'm3u8');
})
</script>

<template>
<div class="flex h-full items-center justify-center bg-gray/30" @wheel="changeVolume">
<div class="flex flex-col h-full items-center justify-center bg-gray/30" @wheel="changeVolume">
<video ref="videoRef" :volume="volume" controls />
<div class="flex justify-around items-center gap-4">
<div :class="audioMode ? 'i-carbon-video-filled' : 'i-carbon-headphones'"
Expand Down

0 comments on commit 4f77dfe

Please sign in to comment.