背景:本项目使用的是 jeecg-boot3.X 和 vue3 + ts + flv视频流。 我是真不喜欢ts。真TM变态 呵tui~
正文: 要去SRS 视频流的控制台获取数据,存入本地后台。并且可以在线阅览。(仅前端代码)
一:生成jeecgboot菜单和配置,并生成列表页面。(这里因为不是主要的,就不做赘述了)
参考网址 jeecg-boot Vue3参考视频
二:因为本地后台地址和需要请求获取的地址信息不一致,所以要先配置。
记得开发环境,和生产环境都要配置
到这一步就算配置好了
三:拉取数据的api
/***
* 流媒体form表单获取数据
*/
export const videoForm = (data = { count: 1000 }) => {
return axios.request({
baseURL: window._CONFIG['srsBase'],
url: '/api/v1/streams/',
method: 'get',
params: data
})
}
四:提交表单修改,
主要去SRS控制台拉取数据 根据选取的 流名称 获取数据。放入本地 list 列表
<template>
<a-spin :spinning="confirmLoading">
<a-form ref="formRef" class="antd-modal-form" :labelCol="labelCol" :wrapperCol="wrapperCol">
<a-row>
<a-col :span="24">
<a-form-item label="流名称" v-bind="validateInfos.name">
<j-dict-select-tag v-model:value="formData.name" @change="changeSelect" dictCode="" :options="liumeitiName"
placeholder="请选择流名称" :disabled="disabled" />
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="Vhost" v-bind="validateInfos.vhost">
<a-input v-model:value="formData.vhost" placeholder="请输入Vhost" disabled></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="状态" v-bind="validateInfos.status">
<a-input v-model:value="formData.status" placeholder="请输入状态" disabled></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="在线人数" v-bind="validateInfos.clients">
<a-input v-model:value="formData.clients" placeholder="请输入在线人数" disabled></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="入口带宽" v-bind="validateInfos.recv">
<a-input v-model:value="formData.recv" placeholder="请输入入口带宽" disabled></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="出口带宽" v-bind="validateInfos.send">
<a-input v-model:value="formData.send" placeholder="请输入出口带宽" disabled></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="视频信息" v-bind="validateInfos.videoinfo">
<a-input v-model:value="formData.videoinfo" placeholder="请输入视频信息" disabled></a-input>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="音频信息" v-bind="validateInfos.audioinfo">
<a-input v-model:value="formData.audioinfo" placeholder="请输入音频信息" disabled></a-input>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-spin>
</template>
<script lang="ts" setup>
import { ref, reactive, defineExpose, nextTick, defineProps, computed, onMounted } from 'vue';
import { defHttp } from '/@/utils/http/axios';
import { useMessage } from '/@/hooks/web/useMessage';
import JDictSelectTag from '/@/components/Form/src/jeecg/components/JDictSelectTag.vue';
import { getValueType } from '/@/utils';
import { saveOrUpdate, videoForm } from '../Video.api';
import { Form } from 'ant-design-vue';
const props = defineProps({
formDisabled: { type: Boolean, default: false },
formData: { type: Object, default: () => { } },
formBpm: { type: Boolean, default: true }
});
const formRef = ref();
const useForm = Form.useForm;
const emit = defineEmits(['register', 'ok']);
const formData = reactive<Record<string, any>>({
name: '',
vhost: '',
status: '',
clients: '',
recv: '',
send: '',
videoinfo: '',
audioinfo: '',
});
const { createMessage } = useMessage();
const labelCol = ref<any>({ xs: { span: 24 }, sm: { span: 5 } });
const wrapperCol = ref<any>({ xs: { span: 24 }, sm: { span: 16 } });
const confirmLoading = ref<boolean>(false);
let liumeitiName = ref([
// { label: '视频1', value: '视频1' },
// { label: '视频2', value: '视频2' },
]);
//表单验证
const validatorRules = {
name: [{ required: true, message: '请输入流名称!' },],
};
let { resetFields, validate, validateInfos } = useForm(formData, validatorRules, { immediate: true });
// 表单禁用
const disabled = computed(() => {
if (props.formBpm === true) {
if (props.formData.disabled === false) {
return false;
} else {
return true;
}
}
return props.formDisabled;
});
/**
* 新增
*/
function add() {
edit({});
}
/**
* 编辑
*/
function edit(record) {
nextTick(() => {
resetFields();
//赋值
Object.assign(formData, record);
});
}
/**
* 提交数据
*/
async function submitForm() {
// 触发表单验证
await validate();
confirmLoading.value = true;
const isUpdate = ref<boolean>(false);
//时间格式化
let model = formData;
if (model.id) {
isUpdate.value = true;
}
//循环数据
for (let data in model) {
//如果该数据是数组并且是字符串类型
if (model[data] instanceof Array) {
let valueType = getValueType(formRef.value.getProps, data);
//如果是字符串类型的需要变成以逗号分割的字符串
if (valueType === 'string') {
model[data] = model[data].join(',');
}
}
}
await saveOrUpdate(model, isUpdate.value)
.then((res) => {
if (res.success) {
createMessage.success(res.message);
emit('ok');
} else {
createMessage.warning(res.message);
}
})
.finally(() => {
confirmLoading.value = false;
});
}
/**
* onMounted 语法糖生命周期
*/
onMounted(() => {
// const videoValue = videoForm()
videoForm().then(res => {
console.log('res', res.data.streams);
liumeitiName = res.data.streams.map((item) => {
return {
label: item.name,
value: JSON.stringify(item)
}
})
})
})
const changeSelect = (value) => {
// console.log('value====', JSON.parse(value));
Object.keys(formData).forEach(key => {
// console.log('element=====', key);
formData[key] = JSON.parse(value)[key]
// formData.ip = JSON.parse(value).id
formData.status = JSON.parse(value).publish.active ? '有流' : '无流' //状态
formData.clients = JSON.parse(value).clients //在线人数
formData.recv = JSON.parse(value).kbps.recv_30s + '.00 Kbps' //入口带宽
formData.send = JSON.parse(value).kbps.send_30s + '.00 Kbps' //出口带宽
formData.videoinfo = JSON.parse(value).video.codec +'/' + JSON.parse(value).video.profile + '/' + JSON.parse(value).video.level + '/' + JSON.parse(value).video.width + 'x' + JSON.parse(value).video.height //视频信息
formData.audioinfo = JSON.parse(value).audio.codec +'/' + JSON.parse(value).audio.sample_rate + 'Stereo' + JSON.parse(value).audio.profile //音频信息
});
}
defineExpose({
add,
edit,
submitForm,
changeSelect
});
</script>
<style lang="less" scoped>
.antd-modal-form {
min-height: 500px !important;
overflow-y: auto;
padding: 24px 24px 24px 24px;
}
</style>
五:因为表单提价主要是生成好的,所以不需要过多修改。就提修改数据样式自己去调整。
现在主要是在提交成功后,本地list视频在线预览
效果图:
<!--操作栏-->
<template #action="{ record }">
<TableAction :actions="getTableAction(record)" /> <!--这里是之前的下拉详情和删除-->
</template>
<!--字段回显插槽-->
<template #htmlSlot="{ text }">
<div v-html="text"></div>
</template>
<!--省市区字段回显插槽-->
<template #pcaSlot="{ text }">
{{ getAreaTextByCode(text) }}
</template>
<template #fileSlot="{ text }">
<span v-if="!text" style="font-size: 12px;font-style: italic;">无文件</span>
<a-button v-else :ghost="true" type="primary" preIcon="ant-design:download-outlined" size="small"
@click="downloadFile(text)">下载</a-button>
</template>
</BasicTable>
<!-- 表单区域 -->
<VideoModal ref="registerModal" @success="handleSuccess"></VideoModal>
<!-- 视频浏览模态框 -->
<VideoLookModal ref="lookVideoModal" />
</div>
</template>
<script lang="ts" name="video_Demo-video" setup>
import { ref, reactive } from 'vue';
import { BasicTable, useTable, TableAction } from '/@/components/Table';
import { useListPage } from '/@/hooks/system/useListPage';
import { columns } from './Video.data';
import { list, deleteOne, batchDelete, getImportUrl, getExportUrl } from './Video.api';
import { downloadFile } from '/@/utils/common/renderUtils';
import VideoModal from './components/VideoModal.vue'
import VideoLookModal from './components/VideoLookModal.vue'
/**
* 操作栏 这里主要是之前的详情和删除,把详情的函数一换,删除依旧延用之前的删除
*/
function getTableAction(record) {
return [
{
label: '预览',
onClick: lookVideo.bind(null, record),
},
{
label: '踢流',
popConfirm: {
title: '是否确认踢流',
confirm: handleDelete.bind(null, record),
}
}
];
}
/**
* 预览视频
*/
function lookVideo(record){
lookVideoModal.value.disableSubmit = true
// disableSubmit 子页面的值,用于是否显示modal框
lookVideoModal.value.openVideo(record)
//openVideo 这个调用的是弹出的模态框里的函数方法(也就是子页面方法)
}
父页面的全部代码:
<template>
<a-modal :title="title" :width="width" :visible="visible" @ok="handleOk" :okButtonProps="{ class: { 'jee-hidden': disableSubmit } }" @cancel="handleCancel" cancelText="关闭">
<VideoForm ref="registerForm" @ok="submitCallback" :formDisabled="disableSubmit" :formBpm="false"></VideoForm>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, nextTick, defineExpose } from 'vue';
import VideoForm from './VideoForm.vue'
const title = ref<string>('');
const width = ref<number>(800);
const visible = ref<boolean>(false);
const disableSubmit = ref<boolean>(false);
const registerForm = ref();
const emit = defineEmits(['register', 'success']);
/**
* 新增
*/
function add() {
title.value = '新增';
visible.value = true;
nextTick(() => {
registerForm.value.add();
});
}
/**
* 编辑
* @param record
*/
function edit(record) {
title.value = disableSubmit.value ? '详情' : '编辑';
visible.value = true;
nextTick(() => {
registerForm.value.edit(record);
});
}
/**
* 确定按钮点击事件
*/
function handleOk() {
registerForm.value.submitForm();
}
/**
* form保存回调事件
*/
function submitCallback() {
handleCancel();
emit('success');
}
/**
* 取消按钮回调事件
*/
function handleCancel() {
visible.value = false;
}
defineExpose({
add,
edit,
disableSubmit,
});
</script>
<style>
/**隐藏样式-modal确定按钮 */
.jee-hidden {
display: none !important;
}
</style>
modal页面,也就是弹出框,也就是 子页面
全部代码:
<template>
<a-modal :title="title" :width="width" :visible="visible" v-if="visible" (这里一定要加上v-if不然会只调用一次接口,也就是说,你打开的所有视频预览,都只会播放你第一个打开的视频) @ok="handleOk" :footer="false"
:okButtonProps="{ class: { 'jee-hidden': disableSubmit } }" @cancel="handleCancel" cancelText="关闭">
<VideoFlv :palyVideoName="palyVideoName" />
</a-modal>
</template>
<script lang="ts" setup>
import { ref, defineExpose } from 'vue';
import VideoFlv from './Videoflv.vue'
const title = ref<string>('');
const width = ref<number>(800);
const visible = ref<boolean>(false);
const disableSubmit = ref<boolean>(false);
const registerForm = ref();
let palyVideoName = ref();
/**
* 视频预览
*/
function openVideo(record) {
title.value = '视频预览';
visible.value = true;
// 这里主要是获取传过来的值,因为不能直接从record中拿到。可以参考我另一篇博客
let name = Reflect.get(record, 'name')
palyVideoName = name
}
/**
* 确定按钮点击事件
*/
function handleOk() {
registerForm.value.submitForm();
}
/**
* 取消按钮回调事件
*/
function handleCancel() {
visible.value = false;
}
defineExpose({
openVideo,
disableSubmit,
});
</script>
<style>
/**隐藏样式-modal确定按钮 */
.jee-hidden {
display: none !important;
}
</style>
孙子组件
全部代码:
<template>
<video id="videoElement" class="player-container" autoplay="true" controls="true" muted="true"></video>
</template>
<script setup>
import { onMounted } from "vue"
const props = defineProps({
// 流名称 这里拿到从父级传过来的值,用于判断
palyVideoName: {
type: String,
default: ""
}
})
console.log('props===',props);
function playVideo() {
if (flvjs.isSupported()) {
var videoElement = document.getElementById('videoElement');
var flvPlayer = flvjs.createPlayer({
type: 'flv',
// window._CONFIG['srsLive'] 这里是之前配置的
// props.palyVideoName 这里是name
url: window._CONFIG['srsLive'] + props.palyVideoName + '.flv'
});
flvPlayer.attachMediaElement(videoElement);
flvPlayer.load();
flvPlayer.play();
}
}
onMounted(() => {
playVideo()
})
</script>
<style lang="less" scoped>
.player-container {
width: 100%;
// height: 480px;
// background-color: #292b29;
border: none;
}
</style>