草庐IT

P2P视频聊天技术分析

扑天鹰 2023-06-20 原文

整个P2P视频过程需要知道双方的媒体类型、流和候选者,所以这里就会用到一下技术:

​ 信令服务器socket.io

​ 状态机

​ ICE服务器

​ WebRTC框架

​ 媒体协商

信令服务器Socket.io

信令服务器说白了作用就是发消息的中转站,A把msg发到信令服务器,然后信令服务器把msg发给B

Socket.IO 是一个库,可在客户端和服务器之间实现低延迟双向基于事件的通信。

它建立在 WebSocket 协议之上,并提供额外的保证,例如回退到 HTTP 长轮询或自动重新连接。

WebSocket 是一种通信协议,它在服务器和浏览器之间提供全双工和低延迟通道。更多信息可以在这里找到。

有几种可用的 Socket.IO 服务器实现:

  • JavaScript (可以在本网站上找到其文档)
  • Java: https://github.com/mrniko/netty-socketio
  • Java: https://github.com/trinopoty/socket.io-server-java
  • Python: https://github.com/miguelgrinberg/python-socketio

大多数主要语言的客户端实现:

  • JavaScript (可以在浏览器、Node.js 或 React Native 中运行)
  • Java: https://github.com/socketio/socket.io-client-java
  • C++: https://github.com/socketio/socket.io-client-cpp
  • Swift: https://github.com/socketio/socket.io-client-swift
  • Dart: https://github.com/rikulo/socket.io-client-dart
  • Python: https://github.com/miguelgrinberg/python-socketio
  • .Net: https://github.com/doghappy/socket.io-client-csharp
  • Golang: https://github.com/googollee/go-socket.io
  • Rust: https://github.com/1c3t3a/rust-socketio
  • Kotlin: https://github.com/icerockdev/moko-socket-io

这是一个使用普通 WebSocket 的基本示例:

服务器 (基于 ws)

import { WebSocketServer } from "ws";

const server = new WebSocketServer({ port: 3000 });

server.on("connection", (socket) => {
  // 向客户端发送消息
  socket.send(JSON.stringify({
    type: "hello from server",
    content: [ 1, "2" ]
  }));

  // 从客户端接收消息
  socket.on("message", (data) => {
    const packet = JSON.parse(data);

    switch (packet.type) {
      case "hello from server":
        // ...
        break;
    }
  });
});

客户端

const socket = new WebSocket("ws://localhost:3000");

socket.addEventListener("open", () => {
  // 向服务器发送消息
  socket.send(JSON.stringify({
    type: "hello from server",
    content: [ 3, "4" ]
  }));
});

// 从服务器接收消息
socket.addEventListener("message", ({ data }) => {
  const packet = JSON.parse(data);

  switch (packet.type) {
    case "hello from server":
      // ...
      break;
  }
});

这是与 Socket.IO 相同的示例:

服务器

import { Server } from "socket.io";

const io = new Server(3000);

io.on("connection", (socket) => {
  // 向客户端发送消息
  socket.emit("hello from server", 1, "2", { 3: Buffer.from([4]) });

  // 从客户端接收消息
  socket.on("hello from server", (...args) => {
    // ...
  });
});

客户端

import { io } from "socket.io-client";

const socket = io("ws://localhost:3000");

// 向服务器发送消息
socket.emit("hello from server", 5, "6", { 7: Uint8Array.from([8]) });

// 从服务器接收消息
socket.on("hello from server", (...args) => {
  // ...
});

这两个示例看起来非常相似,但实际上 Socket.IO 提供了附加功能,这些功能隐藏了在生产环境中运行基于 WebSockets 的应用程序的复杂性。 下面列出了这些功能。

但首先,让我们明确 Socket.IO 不是什么。

Socket.IO 不是什么:

Socket.IO 不是 WebSocket实现。

尽管 Socket.IO 确实在可能的情况下使用 WebSocket 进行传输,但它为每个数据包添加了额外的元数据。这就是为什么 WebSocket 客户端将无法成功连接到 Socket.IO 服务器,而 Socket.IO 客户端也将无法连接到普通 WebSocket 服务器。

// 警告:客户端将无法连接!
const socket = io("ws://echo.websocket.org");

如果您正在寻找一个普通的 WebSocket 服务器,请查看 wsµWebSockets.js.

还有关于在 Node.js 核心中包含 WebSocket 服务器的讨论。在客户端,您可能对robust-websocket感兴趣。

Socket.IO 并不打算在移动应用程序的后台服务中使用

Socket.IO 库保持与服务器的开放 TCP 连接,这可能会导致用户消耗大量电池。请为此用例使用请为此用例使用FCM等专用消息传递平台。

特点

以下是 Socket.IO 在普通 WebSockets 上提供的功能:

HTTP 长轮询回退

如果无法建立 WebSocket 连接,连接将回退到 HTTP 长轮询。

这个特性是人们在十多年前创建项目时使用 Socket.IO 的原因(!),因为浏览器对 WebSockets 的支持仍处于起步阶段。

即使现在大多数浏览器都支持 WebSockets(超过97%),它仍然是一个很棒的功能,因为我们仍然会收到来自用户的报告,这些用户无法建立 WebSocket 连接,因为他们使用了一些错误配置的代理。

自动重新连接

在某些特定情况下,服务器和客户端之间的 WebSocket 连接可能会中断,而双方都不知道链接的断开状态。

这就是为什么 Socket.IO 包含一个心跳机制,它会定期检查连接的状态。

当客户端最终断开连接时,它会以指数回退延迟自动重新连接,以免使服务器不堪重负。

数据包缓冲

当客户端断开连接时,数据包会自动缓冲,并在重新连接时发送。

更多信息在这里.

收到后的回调

Socket.IO 提供了一种方便的方式来发送事件和接收响应:

发件人

socket.emit("hello", "world", (response) => {
  console.log(response); // "got it"
});

接收者

socket.on("hello", (arg, callback) => {
  console.log(arg); // "world"
  callback("got it!");
});

您还可以添加超时:

socket.timeout(5000).emit("hello", "world", (err, response) => {
  if (err) {
    // 另一方未在给定延迟内确认事件
  } else {
    console.log(response); // "got it"
  }
});

广播

在服务器端,您可以向所有连接的客户端客户端的子集发送事件:

// 到所有连接的客户端
io.emit("hello");

// 致“news”房间中的所有连接客户端
io.to("news").emit("hello");

这在扩展到多个节点时也有效。

多路复用

命名空间允许您在单个共享连接上拆分应用程序的逻辑。例如,如果您想创建一个只有授权用户才能加入的“管理员”频道,这可能很有用。

io.on("connection", (socket) => {
  // 普通用户
});

io.of("/admin").on("connection", (socket) => {
  // 管理员用户
});

常见问题

现在还需要 Socket.IO 吗?

这是一个很好的问题,因为现在几乎所有地方 都支持 WebSocket 。

话虽如此,我们相信,如果您在应用程序中使用普通的 WebSocket,您最终将需要实现 Socket.IO 中已经包含(并经过实战测试)的大部分功能,例如重新连接确认广播.

Socket.IO 协议的数据表大小?

socket.emit("hello", "world") 将作为单个 WebSocket 帧发送,其中包含42["hello","world"]

  • 4 是 Engine.IO “消息”数据包类型
  • 2 是 Socket.IO “消息”数据包类型
  • ["hello","world"]JSON.stringify()参数数组的 -ed 版本

因此,每条消息都会增加几个字节,可以通过使用自定义解析器进一步减少。

&浏览器包本身的大小是10.4 kB(缩小和压缩)。

开始

声明:socket.io的版本不同,用法不同,这里用最新用法

第一步:使用express框架搭建服务器路由

const express = require('express'); //引入express模块
const app = express();
app.get('/', (req, res) => {
  res.sendFile(__dirname + '/login.html');
});
/http/
const http = require('http');//此处引用的是http,但是我们的目的是创建P2P视频,所以要开启https,所以需要拥有ssl证书,这个可以在各大云服务商免费申请(推荐华为云和阿里云)
const httpServer = http.createServer(app)
				.listen(8888, "0.0.0.0");
https
const https = require('https');
var fs = require("fs");
var opt = {
    key:fs.readFileSync("./cert/ssl_server.key"),
    cert:fs.readFileSync("./cert/ssl_server.pem")
}//引入ssl证书
const httpsServer = http.createServer(app,opt)
 				.listen(8008, "0.0.0.0");
//这样一个服务器就搭好了,直接node xx.js就可以启动

第二步:使用Socket.io服务端

服务端直接npm install socket.io就可以

老版本///
var io = socketIo.listen(httpsServer);
/新版本///
const io = new Server(httpsServer);

io.sockets.on("connection", (socket)=>{
    /操作内容///
})

第三步:使用Socket.io客户端

客户端可以使用CDN引入也可以下载相关的js库

<script src="/socket.io/socket.io.js"></script>
<script>
  var url = "120.78.xxx.xx:8008"
  var socket = io(url);
  });
</script>

这样一个客户端就已经连上一个服务端了,接下来就是相关操作

第四步:相关操作(略)

P2P加入房间流程

下图有三个客户端ABC,它们同时连接信令服务器,首先是A发起加入房间信号join,然后信令服务器就把A加入房间然后回答joined信号给A;然后B又来发起加入房间信号join,然后信令服务器就把B加入房间而且回答joined信号给B,同时给A其他人加入房间信号otherjoin。然后C就来发起加入房间的信号join,但是信令服务器会对房间人数进行控制,当房间人数等于2就给新请求加入的客户端回答full信号表示房间已满,没有把你加入。这时候A和B就在一个房间里面,接下来它们就可以在房间里面通讯。

状态机

利用状态机进行状态变换和判断

为什么要有状态机:

先考虑一个客户端在一个聊天室会有几种状态:

未加入房间前或者离开房间后(Init/Leave)

加入房间后(joined)

第二个聊天者加入后(joined_conn)

第二个聊天者离开 后(joined_unbind)

通过上面的状态可以发现,用户状态除非就是加入和离开,但是用户进进出出房间会出什么情况呢。这里就要思考用户进入聊天室和离开聊天室,会影响什么?一个聊天室至少会有一个人存在,那么这个人就是发起人,那么怎么知道当前用户是发起人呢。就是通过用户状态来确定。接下来看下面这张图,开始当前用户还未加入房间也就是处于离开状态,但加入房间后就变成joined状态,当第二个人加入就变成joined_conn状态,而相对于第二个人是不会出现joined_conn状态,所以就可以判断当前用户是不是第一个用户,也就是发起者(发起者的作用涉及媒体协商)。最后就是当第二个用户离开就会变为joined_unbind状态。

ICE框架

首先来了解一下两个客户端是怎么点对点通讯的。

第一种:自己知道host ip

第二种:用到一个STUN server,A和B都访问这个STUN server就可以拿到对方的公网ip,然后再利用信令服务器访问,任何客户端都加入了信令服务器,通过信令服务器交换信息,就可以达到NAT穿透的效果

第三种:用到一个中继服务器Relay server(TURN server)

这三种方式就是三个候选者,为什么要三种通讯方式呢,因为信令服务器想尽量不参与通讯,或者说信令服务器只想做一些简单的信息通讯。所以可知道我们音视频通讯利用的大概率就是Relay server和STUN server

现在host IP、Relay server和STUN server都集中到一个ICE服务器项目中,现在只要搭建这个服务器就可以了。

搭建stun/turn服务器步骤:

先安装依赖库:

ubuntu:
apt(-get) install build-essential
apt(-get) install openssl libssl-dev
centos:
yum install libevent-devel openssl-devel

下载4.5版本源码

wget https://github.com/coturn/coturn/archive/4.5.1.1.tar.ge
连不上github的查下资料改hosts

解压

tar -zxvf 4.5.1.1.tar.gz

进入到项目目录

cd coturn-4.5.1.1

源码安装3连

./configure
make
make install

复制配置文件

cp examoles/etc/turnserver.conf bin/turnserver.conf

修改配置文件

#服务器监听端口,3478为默认端口,可修改为自己想要的端口号
listening-port=3478
#想要监听的ip地址,云服务器则是云服务器的内网ip
listening-ip=xxx.xxx.xxx.xxx
#扩展ip地址,云服务器则是云服务器的外网ip
extenal-ip=xxx.xxx.xxx.xxx
#可以设置访问的用户及密码
user=demon:123

启动服务

turnserver -v -r 外网ip:监听端口 -a -o

验证:

https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/

host 本地连接

srflx nat映射后的结果

relay 中继服务器

WebRTC框架

RTCPeerConnection相关原理

pc = new RTCPeerConnection([config])

pc的相关能力:

媒体协商

流和轨的添加与停止

传输相关功能

统计相关功能

媒体协商

每个客户机都有自己支持的媒体格式信息,所以为了统一双方媒体支持格式,所以就想要媒体协商,这是pc的功能。

过程如下:

首先A是一个发起者(通过状态机知道),A首先Get一个offer,这个Get过程收集A自己的媒体信息和候选者(通过ICE就知道)两个信息,然后setLocalDescription这个信息,然后把这个媒体信息发给信令服务器,信令服务器发给房间内的另一个客户机B。然后B收到这个消息后就把这个消息setRemoteDescription,同时B也要收集自己的媒体信息和候选者信息两个信息setLocalDescription后,把answer发给信令服务器,信令服务器转发给A,A收到answer后也setRemoteDescription。这样双方都知道对方支持的媒体信息和候选者。

代码如下:

function mediaNegociate() {
    if (status === "joined_conn") {//joined_conn代表我是连接的发起人
        if (pc) {
            var options = {//要协商的内容,如音频、视频...
                offerToReceiveVideo:true,
                offerToReceiveAudio:true
            }

            pc.createOffer(options)
                .then(getOffer)
                .catch(handleErr);
        }
    }
}


socked.on("vgetdata", (room, data)=>{
        console.log("vgetdata:", data);
        if (!data) {
            return ;
        }
        if (data.type === "candidata") {//拿到对方传过来的候选者信息
           //
        } else if(data.type === "offer") {//媒体协商默认有type值为offer
            console.log("get offer");
            pc.setRemoteDescription(new RTCSessionDescription(data));//把对方的媒体格式设置进来

            //查询自己的媒体格式信息并且应答给对方
            pc.createAnswer()
                .then(getAnswer)
                .catch(handleErr);
        } else if(data.type === "answer") {//媒体协商默认回应有type值为answer
            console.log("get answer");
            pc.setRemoteDescription(new RTCSessionDescription(data));//我把offer发给对方,对方回的answer。offer和answer都是有媒体格式信息。所以offer和answer不会同时存在一个客户端,第一个进来的会发offer,第二个进来的会发answer。把对方的媒体格式设置进来
        }
    });

P2P代码

html代码:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>视频聊天</title>
    <link rel="icon" href="./2.jpg" type="image/x-icon">
</head>
<body>

    <div align="center">
        <table>
            <tr><td colspan="2">
                <h1 id="welcom">欢迎来到1v1的聊天室</h1>
                <input id = "room">
                <button id = "enterRoom">进入房间</button>
                <button id="leaveRoom" >离开房间</button>
            </td></tr>
            <tr>
                <td><lable>本地视频</lable></td>
                <td><label>远端视频</label></td>
            </tr>
            <tr>
                <td><video id="localVideo" autoplay playsinline></video></td>
                <td><video id="remoteVideo" autoplay playsinline></video></td>
            </tr>
        </table>
    </div>

    <script src="js/socket.io.js"></script>
    <script src="js/videoRoom.js"></script>

</body>
</html>

信令服务器代码:

"use strict";

var http = require("http");
var https = require("https");
var fs = require("fs");

//自己安装的模块
var express = require("express");
var serveIndex = require("serve-index");//文件目录
var sqlite3 = require("sqlite3");
var log4js = require("log4js");
var socketIo = require("socket.io");

var logger = log4js.getLogger();
logger.level = "info";

var app=express();
app.use(serveIndex("./zhangyangsong"));
app.use(express.static("./zhangyangsong"));

var opt = {
    key:fs.readFileSync("./cert/ssl_server.key"),
    cert:fs.readFileSync("./cert/ssl_server.pem")
}

// var httpServer=http.createServer(app)
//     .listen(8888, "0.0.0.0");
var httpsServer=https.createServer(opt, app)
    .listen(8008, "0.0.0.0");

var db = null;
var sql = "";
// var io = socketIo.listen(httpServer);
var io = socketIo.listen(httpsServer);
io.sockets.on("connection", (socket)=>{
    logger.info("connection:", socket.id);

    //处理1v1聊天室的消息
    socket.on("vjoin", (room, uname)=>{
        logger.info("vjoin", room, uname);
        socket.join(room);

        var myRoom = io.sockets.adapter.rooms[room];
        var users = Object.keys(myRoom.sockets).length;
        logger.info(room + "user=" + users);
        if (users > 2) {
            socket.leave(room);
            socket.emit("vfull", room);
        } else {
            socket.emit("vjoined", room);
            if (users > 1) {
                socket.to(room).emit("votherjoined", room, uname);
            }
        }
    });
    socket.on("vdata", (room, data)=>{
        logger.info("vdata", data);
        socket.to(room).emit("vgetdata", room, data);
    });

    socket.on("vleave", (room, uname)=>{
        if (room === "") {
            logger.info("room is empty string");
        } else if (room === undefined) {
            logger.info("room is undefine");
        } else if (room === null) {
            logger.info("room is null");
        }

        var myRoom = io.sockets.adapter.rooms[room];
        var users = Object.keys(myRoom.sockets).length;

        logger.info("vleave users=" + (users - 1));
        socket.leave(room);
        socket.emit("vleft", room);
        socket.to(room).emit("votherleft", room, uname);
    });
});

function handleErr(e) {
    logger.info(e);
}

具体操作js代码:

"use strict"
//整个P2P过程需要知道双方的媒体类型、流和候选者
var hWelcom = document.querySelector("h1#welcom");

var url = location.href;
var uname = url.split("?")[1].split("=")[1];

hWelcom.textContent = "欢迎来到1v1视频聊天室:" + uname;

var iptRoom = document.querySelector("input#room");
var btnEnterRoom = document.querySelector("button#enterRoom");
var btnLeaveRoom = document.querySelector("button#leaveRoom");

var videoLocal = document.querySelector("video#localVideo");
var videoRemote = document.querySelector("video#remoteVideo");

var localStream = null;
var remoteStream = null;

var socked = null;
var room = null;
var status = "init";
var pc = null;
var url = "120.78.130.50:8008"
function getMedia(stream) {
    localStream = stream;
    videoLocal.srcObject = stream;
}

function  start() {
    var constraints = {
        video:true,
        audio:true
    };

    //打开摄像头
    navigator.mediaDevices.getUserMedia(constraints)
        .then(getMedia)
        .catch(handleErr);
    conn();
}

function conn() {
    socked = io.connect(url);
    //监听来自服务器的信号
    socked.on("vfull", (room)=>{
        status = "leaved";
        alert("房间已满:" + room);
        console.log("vfull", status);
    });

    socked.on("vjoined", (room)=>{
        //创建视频连接类
        alert("成功加入房间:" + room);
        createPeerConnection();

        status = "joined";
        console.log("vjoined:", status);
    });

    socked.on("votherjoined", (room, uname)=>{
        //建立视频连接
        alert("有人进来了:" + uname);

        if (status === "joined_unbind") {
            createPeerConnection();
        }
        status = "joined_conn";

        //当第二个人进来就要发起媒体协商了:媒体协商就是双方互相知道和设置对方的媒体格式
        mediaNegociate();

        console.log("votherjoined:", status);
    });

    socked.on("vgetdata", (room, data)=>{
        console.log("vgetdata:", data);
        if (!data) {
            return ;
        }
        if (data.type === "candidata") {//拿到对方传过来的候选者信息
            console.log("get other candidata");

            //候选者信息
            var cddt = new RTCIceCandidate({
                sdpMLineIndex:data.label,
                candidate:data.candidate
            });
            pc.addIceCandidate(cddt);//把候选者对象加入pc

        } else if(data.type === "offer") {//媒体协商默认有type值为offer
            console.log("get offer");
            pc.setRemoteDescription(new RTCSessionDescription(data));//把对方的媒体格式设置进来

            //查询自己的媒体格式信息并且应答给对方
            pc.createAnswer()
                .then(getAnswer)
                .catch(handleErr);
        } else if(data.type === "answer") {//媒体协商默认回应有type值为answer
            console.log("get answer");
            pc.setRemoteDescription(new RTCSessionDescription(data));//我把offer发给对方,对方回的answer。offer和answer都是有媒体格式信息。所以offer和answer不会同时存在一个客户端,第一个进来的会发offer,第二个进来的会发answer。把对方的媒体格式设置进来
        }
    });

    socked.on("vleft", (room)=>{
        status = "leaved";
        console.log("vleft:", status);
    });

    socked.on("votherleft", (room, uname)=>{
        status = "joined_unbind";
        closePeerConnection();
        console.log("votherleft:", status);
    });
}

function getAnswer(decs) {
    pc.setLocalDescription(decs);//设置一下本地的媒体格式信息
    sendMessage(decs);
}

function mediaNegociate() {
    if (status === "joined_conn") {//joined_conn代表我是连接的发起人
        if (pc) {
            var options = {//要协商的内容,如音频、视频...
                offerToReceiveVideo:true,
                offerToReceiveAudio:true
            }

            pc.createOffer(options)
                .then(getOffer)
                .catch(handleErr);
        }
    }
}

function getOffer(desc) {//收到的媒体格式
    pc.setLocalDescription(desc);
    sendMessage(desc);//把我需要的媒体格式发给对方
}

function createPeerConnection() {
    if (!pc) {
        var pcConfig = {//ICE服务器
            "iceServers":[{
                "urls":"turn:120.78.130.xx:3478", //指定中继服务器turn
                "username":"zhangyangsong",
                "credential":"123"
            }]
        }

        pc = new RTCPeerConnection(pcConfig); //pc作用:媒体协商,流和轨的添加和停止,传输相关功能,统计相关功能

        pc.onicecandidate = (e)=>{ //得到了ICE服务器选择的候选者返回的事件
            if (e.candidate) {//先判断是不是候选者事件回来的
                console.log("CANDIDATE", e.candidate);
                sendMessage({//把候选者信息发给对方(会发给信令服务器然后转发给对方)
                    type:"candidata",
                    label:e.candidate.sdpMLineIndex,//候选者标签
                    id:e.candidate.sdpMid,//候选者id
                    candidate:e.candidate.candidate//候选者数据
                });
            }
        }

        //当媒体到达的时候,做什么
        pc.ontrack = (e)=>{//ontrack收到远程音视频轨e时
            //alert("连接成功")
            remoteStream = e.streams[0];
            videoRemote.srcObject = remoteStream;//把远程媒体流放到远程音频标签里面显示出来
        }
    }

    if (localStream) {
        localStream.getTracks().forEach((track)=>{
            pc.addTrack(track, localStream);//将本地的媒体流轨加到pc里面
        })
    }
}

start();

function sendMessage(data) {
    if (socked) {
        socked.emit("vdata", room, data);
    }
}

function handleErr(e) {
    console.log(e);
}

function enterRoom() {
    room = iptRoom.value.trim();
    if (room === "") {
        alert("请输入房间号");
        return;
    }
    socked.emit("vjoin", room, uname);
}

function  leaveRoom() {
    socked.emit("vleave", room, uname);
    closePeerConnection();
}

function closePeerConnection() {
    console.log("close RTCPeerConnection");
    if (pc) {
        pc.close();
        pc = null;
    }
}

btnEnterRoom.onclick = enterRoom;
btnLeaveRoom.onclick = leaveRoom;

到这里,整个WebRTCP2P聊天室就完成了,WebRTC可以开发的功能还有很多,但基本原理都是这几个内容。

注意:记得打开chrome://flags/这个网站搜索platform然后打开

有关P2P视频聊天技术分析的更多相关文章

  1. Unity 热更新技术 | (三) Lua语言基本介绍及下载安装 - 2

    ?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------

  2. MIMO-OFDM无线通信技术及MATLAB实现(1)无线信道:传播和衰落 - 2

     MIMO技术的优缺点优点通过下面三个增益来总体概括:阵列增益。阵列增益是指由于接收机通过对接收信号的相干合并而活得的平均SNR的提高。在发射机不知道信道信息的情况下,MIMO系统可以获得的阵列增益与接收天线数成正比复用增益。在采用空间复用方案的MIMO系统中,可以获得复用增益,即信道容量成倍增加。信道容量的增加与min(Nt,Nr)成正比分集增益。在采用空间分集方案的MIMO系统中,可以获得分集增益,即可靠性性能的改善。分集增益用独立衰落支路数来描述,即分集指数。在使用了空时编码的MIMO系统中,由于接收天线或发射天线之间的间距较远,可认为它们各自的大尺度衰落是相互独立的,因此分布式MIMO

  3. 动漫制作技巧如何制作动漫视频 - 2

    动漫制作技巧是很多新人想了解的问题,今天小编就来解答与大家分享一下动漫制作流程,为了帮助有兴趣的同学理解,大多数人会选择动漫培训机构,那么今天小编就带大家来看看动漫制作要掌握哪些技巧?一、动漫作品首先完成草图设计和原型制作。设计草图要有目的、有对象、有步骤、要形象、要简单、符合实际。设计图要一致性,以保证制作的顺利进行。二、原型制作是根据设计图纸和制作材料,可以是手绘也可以是3d软件创建。在此步骤中,要注意的问题是色彩和平面布局。三、动漫制作制作完成后,加工成型。完成不同的表现形式后,就要对设计稿进行加工处理,使加工的难易度降低,并得到一些基本准确的概念,以便于后续的大样、准确的尺寸制定。四、

  4. python ffmpeg 使用 pyav 转换 一组图像 到 视频 - 2

    2022/8/4更新支持加入水印水印必须包含透明图像,并且水印图像大小要等于原图像的大小pythonconvert_image_to_video.py-f30-mwatermark.pngim_dirout.mkv2022/6/21更新让命令行参数更加易用新的命令行使用方法pythonconvert_image_to_video.py-f30im_dirout.mkvFFMPEG命令行转换一组JPG图像到视频时,是将这组图像视为MJPG流。我需要转换一组PNG图像到视频,FFMPEG就不认了。pyav内置了ffmpeg库,不需要系统带有ffmpeg工具因此我使用ffmpeg的python包装p

  5. TimeSformer:抛弃CNN的Transformer视频理解框架 - 2

    Transformers开始在视频识别领域的“猪突猛进”,各种改进和魔改层出不穷。由此作者将开启VideoTransformer系列的讲解,本篇主要介绍了FBAI团队的TimeSformer,这也是第一篇使用纯Transformer结构在视频识别上的文章。如果觉得有用,就请点赞、收藏、关注!paper:https://arxiv.org/abs/2102.05095code(offical):https://github.com/facebookresearch/TimeSformeraccept:ICML2021author:FacebookAI一、前言Transformers(VIT)在图

  6. ruby-on-rails - 用于门户的 Ruby 技术 - 2

    我刚刚看到whitehouse.gov正在使用drupal作为CMS和门户技术。drupal的优点之一似乎是很容易添加插件,而且编程最少,即重新发明轮子最少。这实际上正是Ruby-on-Rails的DRY理念。所以:drupal的缺点是什么?Rails或其他基于Ruby的技术有哪些不符合whitehouse.org(或其他CMS门户)门户技术的资格? 最佳答案 Whatarethedrawbacksofdrupal?对于Ruby和Rails,这确实是一个相当主观的问题。Drupal是一个可靠的内容管理选项,非常适合面向社区的站点。它

  7. ruby - 如何更改此正则表达式以从未指定 v 参数的 Youtube URL 获取 Youtube 视频 ID? - 2

    目前我正在使用这个正则表达式从YoutubeURL中提取视频ID:url.match(/v=([^&]*)/)[1]我怎样才能改变它,以便它也可以从这个没有v参数的YoutubeURL获取视频ID:http://www.youtube.com/user/SHAYTARDS#p/u/9/Xc81AajGUMU感谢阅读。编辑:我正在使用ruby​​1.8.7 最佳答案 对于Ruby1.8.7,这就可以了。url_1='http://www.youtube.com/watch?v=8WVTOUh53QY&feature=feedf'url

  8. 建模分析 | 平面2R机器人(二连杆)运动学与动力学建模(附Matlab仿真) - 2

    目录0专栏介绍1平面2R机器人概述2运动学建模2.1正运动学模型2.2逆运动学模型2.3机器人运动学仿真3动力学建模3.1计算动能3.2势能计算与动力学方程3.3动力学仿真0专栏介绍?附C++/Python/Matlab全套代码?课程设计、毕业设计、创新竞赛必备!详细介绍全局规划(图搜索、采样法、智能算法等);局部规划(DWA、APF等);曲线优化(贝塞尔曲线、B样条曲线等)。?详情:图解自动驾驶中的运动规划(MotionPlanning),附几十种规划算法1平面2R机器人概述如图1所示为本文的研究本体——平面2R机器人。对参数进行如下定义:机器人广义坐标

  9. iNFTnews | 周杰伦18年前未发布的作品Demo,藏在了区块链技术里 - 2

    当音乐碰上区块链技术,会擦出怎样的火花?或许周杰伦已经给了我们答案。8月29日下午,B站独家首发周杰伦限定珍藏Demo独家访谈VCR,周杰伦在VCR里分享了《晴天》《青花瓷》《搁浅》《爱在西元前》四首经典歌曲Demo背后的创作故事,并首次公布18年前未发布的神秘作品《纽约地铁》的Demo。在VCR中,方文山和杰威尔音乐提及到“多亏了区块链技术,现在我们可以将这些Demos,变成独一无二具有收藏价值的艺术品,这些Demos可以在薄盒(国内数藏平台)上听到。”如何将音乐与区块链技术相结合,薄盒方面称:“薄盒作为区块链技术服务方,打破传统对于区块链技术只能作为数字收藏的理解。聚焦于区块链技术赋能,在

  10. 网站日志分析软件--让网站日志分析工作变得更简单 - 2

    网站的日志分析,是seo优化不可忽视的一门功课,但网站越大,每天产生的日志就越大,大站一天都可以产生几个G的网站日志,如果光靠肉眼去分析,那可能看到猴年马月都看不完,因此借助网站日志分析工具去分析网站日志,那将会使网站日志分析工作变得更简单。下面推荐两款网站日志分析软件。第一款:逆火网站日志分析器逆火网站日志分析器是一款功能全面的网站服务器日志分析软件。通过分析网站的日志文件,不仅能够精准的知道网站的访问量、网站的访问来源,网站的广告点击,访客的地区统计,搜索引擎关键字查询等,还能够一次性分析多个网站的日志文件,让你轻松管理网站。逆火网站日志分析器下载地址:https://pan.baidu.

随机推荐