浏览器播放 WebRTC 视频流

news2025/5/23 0:39:44

源码(vue)

<template>
  <video ref="videoElement" class="video" autoplay muted playsinline></video>
</template>

<script setup lang="ts">
  import { onBeforeUnmount, onMounted, ref } from 'vue'

  import { JSWebrtc } from '@/utils/jswebrtc.min.js'

  const videoElement = ref<HTMLVideoElement | null>(null)
  let player: JSWebrtc.Player | null = null

  onMounted(() => {
    if (!videoElement.value) return

    player = new JSWebrtc.Player('webrtc://192.168.20.222/live/34020000001320000002', {
      video: videoElement.value,
      autoplay: true,
      onPlay: (obj: any) => {
        console.log('start play', obj)
      },
      onError: (error: Error) => {
        console.error('Playback error:', error)
      }
    })
  })

  onBeforeUnmount(() => {
    player?.destroy()
    player = null
  })
</script>

jswebrtc.min.js

export var JSWebrtc = {
  Player: null,
  VideoElement: null,
  CreateVideoElements: function () {
    let elements = document.querySelectorAll('.jswebrtc')
    for (let i = 0; i < elements.length; i++) {
      new JSWebrtc.VideoElement(elements[i])
    }
  },
  FillQuery: function (query_string, obj) {
    obj.user_query = {}
    if (query_string.length == 0) return
    if (query_string.indexOf('?') >= 0) query_string = query_string.split('?')[1]
    let queries = query_string.split('&')
    for (let i = 0; i < queries.length; i++) {
      let query = queries[i].split('=')
      obj[query[0]] = query[1]
      obj.user_query[query[0]] = query[1]
    }
    if (obj.domain) obj.vhost = obj.domain
  },
  ParseUrl: function (rtmp_url) {
    let a = document.createElement('a')
    a.href = rtmp_url.replace('rtmp://', 'http://').replace('webrtc://', 'http://').replace('rtc://', 'http://')
    let vhost = a.hostname
    let app = a.pathname.substr(1, a.pathname.lastIndexOf('/') - 1)
    let stream = a.pathname.substr(a.pathname.lastIndexOf('/') + 1)
    app = app.replace('...vhost...', '?vhost=')
    if (app.indexOf('?') >= 0) {
      let params = app.substr(app.indexOf('?'))
      app = app.substr(0, app.indexOf('?'))
      if (params.indexOf('vhost=') > 0) {
        vhost = params.substr(params.indexOf('vhost=') + 'vhost='.length)
        if (vhost.indexOf('&') > 0) {
          vhost = vhost.substr(0, vhost.indexOf('&'))
        }
      }
    }
    if (a.hostname == vhost) {
      let re = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/
      if (re.test(a.hostname)) vhost = '__defaultVhost__'
    }
    let schema = 'rtmp'
    if (rtmp_url.indexOf('://') > 0) schema = rtmp_url.substr(0, rtmp_url.indexOf('://'))
    let port = a.port
    if (!port) {
      if (schema === 'http') {
        port = 80
      } else if (schema === 'https') {
        port = 443
      } else if (schema === 'rtmp') {
        port = 1935
      } else if (schema === 'webrtc' || schema === 'rtc') {
        port = 1985
      }
    }
    let ret = {
      url: rtmp_url,
      schema: schema,
      server: a.hostname,
      port: port,
      vhost: vhost,
      app: app,
      stream: stream
    }
    JSWebrtc.FillQuery(a.search, ret)
    return ret
  },
  HttpPost: function (url, data) {
    return new Promise(function (resolve, reject) {
      let xhr = new XMLHttpRequest()
      xhr.onreadystatechange = function () {
        if (xhr.readyState === 4 && xhr.status >= 200 && xhr.status < 300) {
          let respone = JSON.parse(xhr.responseText)
          xhr.onreadystatechange = new Function()
          xhr = null
          resolve(respone)
        }
      }
      xhr.open('POST', url, true)
      xhr.timeout = 5e3
      xhr.responseType = 'text'
      xhr.setRequestHeader('Content-Type', 'application/json')
      xhr.send(data)
    })
  }
}
if (document.readyState === 'complete') {
  JSWebrtc.CreateVideoElements()
} else {
  document.addEventListener('DOMContentLoaded', JSWebrtc.CreateVideoElements)
}
JSWebrtc.VideoElement = (function () {
  'use strict'
  let VideoElement = function (element) {
    let url = element.dataset.url
    if (!url) {
      throw 'VideoElement has no `data-url` attribute'
    }
    let addStyles = function (element, styles) {
      for (let name in styles) {
        element.style[name] = styles[name]
      }
    }
    this.container = element
    addStyles(this.container, {
      display: 'inline-block',
      position: 'relative',
      minWidth: '80px',
      minHeight: '80px'
    })
    this.video = document.createElement('video')
    this.video.width = 960
    this.video.height = 540
    addStyles(this.video, { display: 'block', width: '100%' })
    this.container.appendChild(this.video)
    this.playButton = document.createElement('div')
    this.playButton.innerHTML = VideoElement.PLAY_BUTTON
    addStyles(this.playButton, {
      zIndex: 2,
      position: 'absolute',
      top: '0',
      bottom: '0',
      left: '0',
      right: '0',
      maxWidth: '75px',
      maxHeight: '75px',
      margin: 'auto',
      opacity: '0.7',
      cursor: 'pointer'
    })
    this.container.appendChild(this.playButton)
    let options = { video: this.video }
    for (let option in element.dataset) {
      try {
        options[option] = JSON.parse(element.dataset[option])
      } catch (err) {
        options[option] = element.dataset[option]
      }
    }
    this.player = new JSWebrtc.Player(url, options)
    element.playerInstance = this.player
    if (options.poster && !options.autoplay) {
      options.decodeFirstFrame = false
      this.poster = new Image()
      this.poster.src = options.poster
      this.poster.addEventListener('load', this.posterLoaded)
      addStyles(this.poster, {
        display: 'block',
        zIndex: 1,
        position: 'absolute',
        top: 0,
        left: 0,
        bottom: 0,
        right: 0
      })
      this.container.appendChild(this.poster)
    }
    if (!this.player.options.streaming) {
      this.container.addEventListener('click', this.onClick.bind(this))
    }
    if (options.autoplay) {
      this.playButton.style.display = 'none'
    }
    if (this.player.audioOut && !this.player.audioOut.unlocked) {
      let unlockAudioElement = this.container
      if (options.autoplay) {
        this.unmuteButton = document.createElement('div')
        this.unmuteButton.innerHTML = VideoElement.UNMUTE_BUTTON
        addStyles(this.unmuteButton, {
          zIndex: 2,
          position: 'absolute',
          bottom: '10px',
          right: '20px',
          width: '75px',
          height: '75px',
          margin: 'auto',
          opacity: '0.7',
          cursor: 'pointer'
        })
        this.container.appendChild(this.unmuteButton)
        unlockAudioElement = this.unmuteButton
      }
      this.unlockAudioBound = this.onUnlockAudio.bind(this, unlockAudioElement)
      unlockAudioElement.addEventListener('touchstart', this.unlockAudioBound, false)
      unlockAudioElement.addEventListener('click', this.unlockAudioBound, true)
    }
  }
  VideoElement.prototype.onUnlockAudio = function (element, ev) {
    if (this.unmuteButton) {
      ev.preventDefault()
      ev.stopPropagation()
    }
    this.player.audioOut.unlock(
      function () {
        if (this.unmuteButton) {
          this.unmuteButton.style.display = 'none'
        }
        element.removeEventListener('touchstart', this.unlockAudioBound)
        element.removeEventListener('click', this.unlockAudioBound)
      }.bind(this)
    )
  }
  VideoElement.prototype.onClick = function (ev) {
    if (this.player.isPlaying) {
      this.player.pause()
      this.playButton.style.display = 'block'
    } else {
      this.player.play()
      this.playButton.style.display = 'none'
      if (this.poster) {
        this.poster.style.display = 'none'
      }
    }
  }
  VideoElement.PLAY_BUTTON =
    '<svg style="max-width: 75px; max-height: 75px;" ' +
    'viewBox="0 0 200 200" alt="Play video">' +
    '<circle cx="100" cy="100" r="90" fill="none" ' +
    'stroke-width="15" stroke="#fff"/>' +
    '<polygon points="70, 55 70, 145 145, 100" fill="#fff"/>' +
    '</svg>'
  VideoElement.UNMUTE_BUTTON =
    '<svg style="max-width: 75px; max-height: 75px;" viewBox="0 0 75 75">' +
    '<polygon class="audio-speaker" stroke="none" fill="#fff" ' +
    'points="39,13 22,28 6,28 6,47 21,47 39,62 39,13"/>' +
    '<g stroke="#fff" stroke-width="5">' +
    '<path d="M 49,50 69,26"/>' +
    '<path d="M 69,50 49,26"/>' +
    '</g>' +
    '</svg>'
  return VideoElement
})()
JSWebrtc.Player = (function () {
  'use strict'
  let Player = function (url, options) {
    this.options = options || {}
    if (!url.match(/^webrtc?:\/\//)) {
      throw 'JSWebrtc just work with webrtc'
    }
    if (!this.options.video) {
      throw 'VideoElement is null'
    }
    this.urlParams = JSWebrtc.ParseUrl(url)
    this.pc = null
    this.autoplay = !!options.autoplay || false
    this.paused = true
    if (this.autoplay) this.options.video.muted = true
    this.startLoading()
  }
  Player.prototype.startLoading = function () {
    let _self = this
    if (_self.pc) {
      _self.pc.close()
    }
    _self.pc = new RTCPeerConnection(null)
    _self.pc.ontrack = function (event) {
      _self.options.video['srcObject'] = event.streams[0]
    }
    _self.pc.addTransceiver('audio', { direction: 'recvonly' })
    _self.pc.addTransceiver('video', { direction: 'recvonly' })
    _self.pc
      .createOffer()
      .then(function (offer) {
        return _self.pc.setLocalDescription(offer).then(function () {
          return offer
        })
      })
      .then(function (offer) {
        return new Promise(function (resolve, reject) {
          let port = _self.urlParams.port || 1985
          let api = _self.urlParams.user_query.play || '/rtc/v1/play/'
          if (api.lastIndexOf('/') != api.length - 1) {
            api += '/'
          }
          let url = 'http://' + _self.urlParams.server + ':' + port + api
          for (let key in _self.urlParams.user_query) {
            if (key != 'api' && key != 'play') {
              url += '&' + key + '=' + _self.urlParams.user_query[key]
            }
          }
          let data = {
            api: url,
            streamurl: _self.urlParams.url,
            clientip: null,
            sdp: offer.sdp,
            tid: Number(parseInt(new Date().getTime() * Math.random() * 100))
              .toString(16)
              .slice(0, 7)
          }
          //   console.log('offer:1111111111111 ' + JSON.stringify(data))
          JSWebrtc.HttpPost(url, JSON.stringify(data)).then(
            function (res) {
              // console.log('answer: ' + JSON.stringify(res))
              resolve(res.sdp)
            },
            function (rej) {
              reject(rej)
            }
          )
        })
      })
      .then(function (answer) {
        return _self.pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: answer }))
      })
      .catch(function (reason) {
        throw reason
      })
    if (this.autoplay) {
      this.play()
    }
  }
  Player.prototype.play = function (ev) {
    if (this.animationId) {
      return
    }
    this.animationId = requestAnimationFrame(this.update.bind(this))
    this.paused = false
  }
  Player.prototype.pause = function (ev) {
    if (this.paused) {
      return
    }
    cancelAnimationFrame(this.animationId)
    this.animationId = null
    this.isPlaying = false
    this.paused = true
    this.options.video.pause()
    if (this.options.onPause) {
      this.options.onPause(this)
    }
  }
  Player.prototype.stop = function (ev) {
    this.pause()
  }
  Player.prototype.destroy = function () {
    this.pause()
    this.pc && this.pc.close() && this.pc.destroy()
    this.audioOut && this.audioOut.destroy()
  }
  Player.prototype.update = function () {
    this.animationId = requestAnimationFrame(this.update.bind(this))
    if (this.options.video.readyState < 4) {
      return
    }
    if (!this.isPlaying) {
      this.isPlaying = true
      this.options.video.play()
      if (this.options.onPlay) {
        this.options.onPlay(this)
      }
    }
  }
  return Player
})()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2383497.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分类算法 Kmeans、KNN、Meanshift 实战

任务 1、采用 Kmeans 算法实现 2D 数据自动聚类&#xff0c;预测 V180,V260 数据类别&#xff1b; 2、计算预测准确率&#xff0c;完成结果矫正 3、采用 KNN、Meanshift 算法&#xff0c;重复步骤 1-2 代码工具&#xff1a;jupyter notebook 视频资料 无监督学习&#xff…

网络安全之身份验证绕过漏洞

漏洞简介 CrushFTP 是一款由 CrushFTP LLC 开发的强大文件传输服务器软件&#xff0c;支持FTP、SFTP、HTTP、WebDAV等多种协议&#xff0c;为企业和个人用户提供安全文件传输服务。近期&#xff0c;一个被编号为CVE-2025-2825的严重安全漏洞被发现&#xff0c;该漏洞影响版本1…

MySQL 主从复制搭建全流程:基于 Docker 与 Harbor 仓库

一、引言 在数据库管理中&#xff0c;MySQL 主从复制是一种非常重要的技术&#xff0c;它可以实现数据的备份、读写分离&#xff0c;减轻主数据库的压力。本文将详细介绍如何使用 Docker 和 Harbor 仓库来搭建 MySQL 主从复制环境&#xff0c;适合刚接触数据库和 Docker 的新手…

Django框架的前端部分使用Ajax请求一

Ajax请求 目录 1.ajax请求使用 2.增加任务列表功能(只有查看和新增) 3.代码展示集合 这篇文章, 要开始讲关于ajax请求的内容了。这个和以前文章中写道的Vue框架里面的axios请求, 很相似。后端代码, 会有一些细节点, 跟前几节文章写的有些区别。 一、ajax请求使用 我们先…

cmd如何从C盘默认路径切换到D盘某指定目录

以从C盘cmd打开后的默认目录切换到目录"D:\Program Files\MySQL\MySQL Server 8.0\bin\mysqld"为例 打开cmd 首先点击开始键&#xff0c;搜索cms&#xff0c;右键以管理员身份运行打开管理员端的命令行提示符 1、首先要先切换到D盘 直接输入D:然后回车就可以&…

每日Prompt:实物与手绘涂鸦创意广告

提示词 一则简约且富有创意的广告&#xff0c;设置在纯白背景上。 一个真实的 [真实物体] 与手绘黑色墨水涂鸦相结合&#xff0c;线条松散而俏皮。涂鸦描绘了&#xff1a;[涂鸦概念及交互&#xff1a;以巧妙、富有想象力的方式与物体互动]。在顶部或中部加入粗体黑色 [广告文案…

学习笔记:黑马程序员JavaWeb开发教程(2025.4.8)

12.11 登录校验-Filter-详解&#xff08;过滤器链&#xff09; 过滤器链及其执行顺序&#xff0c;一个Filter一个过滤器链&#xff0c;类名排名越靠前&#xff08;按照ABC这样的顺序&#xff09;&#xff0c;就先执行谁 12.12 登录校验-Filter-登录校验过滤器 获取请求参数&…

Ubuntu部署私有Gitlab

这个东西安装其实挺简单的&#xff0c;但是因为我这边迁移了数据目录和使用自己安装的 nginx 代理还是踩了几个坑&#xff0c;所以大家可以注意下 先看下安装 # 先安装必要组件 sudo apt update sudo apt install -y curl openssh-server ca-certificates tzdata perl# 添加gi…

genicamtl_lmi_gocator_objectmodel3d

目录 一、在halcon中找不到genicamtl_lmi_gocator_objectmodel3d例程二、在halcon中运行genicamtl_lmi_gocator_objectmodel3d,该如何配置三、代码分段详解(一)传感器连接四、代码分段详解(二)采集图像并显示五、代码分段详解(三)坐标变换六、常见问题一、在halcon中找不…

[LevelDB]LevelDB版本管理的黑魔法-为什么能在不锁表的情况下管理数据?

文章摘要 LevelDB的日志管理系统是怎么通过双链表来进行数据管理为什么LevelDB能够在不锁表的情况下进行日志新增 适用人群: 对版本管理机制有开发诉求&#xff0c;并且希望参考LevelDB的版本开发机制。数据库相关从业者的专业人士。计算机狂热爱好者&#xff0c;对计算机的…

bisheng系列(二)- 本地部署(前后端)

一、导读 环境&#xff1a;Ubuntu 24.04、open Euler 23.03、Windows 11、WSL 2、Python 3.10 、bisheng 1.1.1 背景&#xff1a;需要bisheng二开商用&#xff0c;故而此处进行本地部署&#xff0c;便于后期调试开发 时间&#xff1a;20250519 说明&#xff1a;bisheng前后…

【网络编程】十二、两万字详解 IP协议

文章目录 Ⅰ. 基本概念1、网络层解决的问题2、保证数据可靠的从一台主机送到另一台主机的前提3、路径选择4、主机和路由器的区别 Ⅱ. IP协议格式IP如何将报头与有效载荷进行分离&#xff1f;IP如何决定将有效载荷交付给上层的哪一个协议&#xff1f;理解socket编程 Ⅲ. 分片与组…

Linux探秘:驾驭开源,解锁高效能——基础指令

♥♥♥~~~~~~欢迎光临知星小度博客空间~~~~~~♥♥♥ ♥♥♥零星地变得优秀~也能拼凑出星河~♥♥♥ ♥♥♥我们一起努力成为更好的自己~♥♥♥ ♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥ ♥♥♥如果有什么问题可以评论区留言或者私信我哦~♥♥♥ ✨✨✨✨✨✨ 个…

WebSocket解决方案的一些细节阐述

今天我们来看看WebSocket解决方案的一些细节问题&#xff1a; 实际上&#xff0c;集成WebSocket的方法都有相关的工程挑战&#xff0c;这可能会影响项目成本和交付期限。在最简单的层面上&#xff0c;构建 WebSocket 解决方案似乎是添加接收实时更新功能的前进方向。但是&…

Java 代码生成工具:如何快速构建项目骨架?

Java 代码生成工具&#xff1a;如何快速构建项目骨架&#xff1f; 在 Java 项目开发过程中&#xff0c;构建项目骨架是一项繁琐但又基础重要的工作。幸运的是&#xff0c;Java 领域有许多代码生成工具可以帮助我们快速完成这一任务&#xff0c;大大提高开发效率。 一、代码生…

Nginx核心服务

一&#xff0e;正向代理 正向代理&#xff08;Forward Proxy&#xff09;‌是一种位于客户端和原始服务器之间的代理服务器&#xff0c;其主要作用是将客户端的请求转发给目标服务器&#xff0c;并将响应返回给客户端 Nginx 的 正向代理 充当客户端的“中间人”&#xff0c;代…

第22天-Python ttkbootstrap 界面美化指南

环境安装 pip install ttkbootstrap 示例1:基础主题切换器 import ttkbootstrap as ttk from ttkbootstrap.constants import *def create_theme_switcher():root = ttk.Window(title="主题切换器", themename="cosmo")def change_theme():selected = t…

Kubernetes控制平面组件:Kubelet详解(七):容器网络接口 CNI

云原生学习路线导航页&#xff08;持续更新中&#xff09; kubernetes学习系列快捷链接 Kubernetes架构原则和对象设计&#xff08;一&#xff09;Kubernetes架构原则和对象设计&#xff08;二&#xff09;Kubernetes架构原则和对象设计&#xff08;三&#xff09;Kubernetes控…

web应用技术第6次课---Apifox的使用

Apifox - API 文档、调试、Mock、测试一体化协作平台。拥有接口文档管理、接口调试、Mock、自动化测试等功能&#xff0c;接口开发、测试、联调效率&#xff0c;提升 10 倍。最好用的接口文档管理工具&#xff0c;接口自动化测试工具。 第一个问题&#xff1a;为什么需要用Apif…

Redis队列与Pub/Sub方案全解析:原理、对比与实战性能测试

一、为什么选择Redis实现消息队列&#xff1f; Redis凭借其内存级操作&#xff08;微秒级响应&#xff09;、丰富的数据结构以及持久化能力&#xff0c;成为构建高性能消息队列的热门选择。相比传统消息队列&#xff08;如Kafka/RabbitMQ&#xff09;&#xff0c;Redis在以下场…