vue3 + Ts + flv视频流

背景:本项目使用的是 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>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值