如何用Javascript构建一个屏幕录像机应用程序

107 阅读5分钟

用Javascript构建屏幕录像机应用程序

计算机是我们日常业务工作中必不可少的工具。有时,我们可能会发现录制在线缩放会议、从幻灯片中创建演示视频或通过视频教程支持客户完成任务是很有帮助的。

为了实现这样的行动,需要具有屏幕捕捉能力的实用软件。屏幕记录器应用可以通过实时记录电脑或手机屏幕上的镜头活动来生成数字视频内容。

本文指导我们在网络浏览器中使用React和Node.js实现这一功能。

前提条件

  • 对[JavaScript]编程语言的了解。
  • React库的基础知识。。
  • 一个代码编辑器,如[VS Code]或一个IDE。
  • 在你的开发环境中拥有Node.js LTS或更高版本。

开始使用

我们的屏幕录像机应用程序将使用全栈的JavaScript。在客户端的主要库包括。

  • React- 一个用于动态应用程序的前端JavaScript库。
  • socket.io-client 用于与网络服务器进行交互。

对于后端,我们将使用。

  • Express.js - 一个用于服务器的Node.js框架。
  • Socket.io - 一个用于开发实时双向应用的Node.js库,使用web sockets。
  • FFmpeg - 一个用于处理音频和视频等多媒体流的开源工具。

应用程序设置

我们的应用程序结构包括一个后台(server 目录)和一个Reactclient 文件夹。

首先,我们将创建我们的应用程序文件夹并将其命名为screen-recorder-app 。然后,我们将在该文件夹中设置两个目录;server 文件夹用于后台,client 文件夹用于React应用程序。

在你的bash终端,创建screen-recorder-app 项目文件夹。

mkdir screen-recorder-app

导航该目录,使用create-react-app 工具创建React客户端。执行以下命令。

cd screen-recorder-app

npx create-react-app client

create-react-app 命令行工具为我们的应用程序创建一个模板代码。然而,我们的整个代码将在src/App.js 文件上。

我们的应用程序需要网络套接字接口来到达后端。为了这个功能,让我们添加socket.io-clientreact-loader-spinner 模块。

npm install socket.io-client react-loader-spinner

最后,在你的IDE上打开这个文件夹。对于VS Code,运行该命令。

code  .

为了设置我们的组件,前往你的src/App.js 文件并导入useEffectuseRefuseState 钩子。接下来,从我们的socket.io-client 模块中,导入io 对象来初始化我们的客户端。

import { useEffect, useRef, useState, Fragment } from 'react';
import { io } from 'socket.io-client';

// adding a simple loading spinner component
import Loader from 'react-loader-spinner';

在我们的主App.js 文件的顶部,声明应用程序的变量,包括。

  • 后台本地服务器地址为http://localhost:5000
  • 记录数据的data_chunks和MediaRecorder 实例接口将提供一个API来记录MediaStream
// server address
const LOCAL_SERVER = 'http://localhost:5000';
let data_chunks = [];

// MediaRecorder instance
let media_recorder = null;

我们将在App.js 文件中为我们的客户端React编写一切。让我们添加一个从JSX中渲染的功能组件,带有Recorder App的<h1> 标签。

function App() {

// return a JSX of h1
  return (
    <Fragment>
      <h1>Recorder App</h1>
    </Fragment>
  )
}

如果我们使用npm start 的CLI命令启动我们的服务器,然后前往我们的浏览器,我们应该看到类似的东西。

local server

App组件

在你的App.js 组件中,在你的返回语句上方添加以下代码。

function App() {
  // a random username
  const username = useRef(`User_${Date.now().toString().slice(-4)}`)

  const socketRef = useRef(io(LOCAL_SERVER))

  const linkRef = useRef()
  const videoRef = useRef()

  // hold state for audio stream from device microphone
  const [voiceStream, setVoiceStream] = useState()

  // A stream of a video captured from the screen
  const [screenStream, setScreenStream] = useState()

  // loading status indicator
  const [loading, setLoading] = useState(true)

  // recording status indicator
  const [recording, setRecording] = useState(false)

  return (
    <Fragment>
      <h1>Recorder App</h1>
    </Fragment>
  )
}

由于我们的应用程序不对任何用户进行认证,我们需要使用useRef 钩子从当前的时间戳中生成一个随机的用户名,以创建一个对DOM元素的引用。

socketRef将使用服务器的URL启动对我们的后端Web socket连接的调用。这创建了一个接口来启动发送和接收数据的流。videoRef 钩子映射到DOM,允许用户下载视频格式的屏幕截图。

WebSockets API的目标是在一个单一的TCP连接上创建一个全双工的通信通道。为了触发一个事件,socket.emit 方法将接受事件类型以及发送的数据。

在客户端连接结束后,我们通过使用socket.on 方法监听WebSockets来处理事件。这个方法接受事件类型作为一个参数,并接受事件发出后要执行的回调函数。

接下来,我们需要捕捉屏幕。

  /**
   *  First, the client needs to notify the server
   *  when a new user has connected from the random username
  */

  useEffect(() => {
    ;(async () => {
      if (navigator.mediaDevices.getDisplayMedia) {

        try {
        //  grant screen
          const screenStream = await navigator.mediaDevices.getDisplayMedia({
            video: true
          })
           // get the video stream
          setScreenStream(screenStream)
        }
        // exception handling
        catch (err) {
          setLoading(false)
          console.log('getDisplayMedia', err)
        }

      } else {
        setLoading(false)
        console.log('getDisplayMedia is not supported...')
      }

    })()
  }, [])

Navigator是一个浏览器窗口对象。在navigator.mediaDevices 对象下,我们可以访问所有连接的媒体输入,包括麦克风、摄像头和屏幕共享。

在这种情况下,我们要将屏幕数据捕获作为screenStream 的实时流。

在Chrome和Microsoft Edge中,getDisplayMedia 方法可以捕获音频内容。

为了开始接收来自用户设备的媒体流,用以下代码创建一个startRecording 函数。

  function startRecording() {
    if (screenStream && voiceStream && !mediaRecorder) {

      // set recording state to true
      setRecording(true)

      videoRef.current.removeAttribute('src')
      linkRef.current.removeAttribute('href')
      linkRef.current.removeAttribute('download')

      let mediaStream
      if (voiceStream === 'unavailable') {
        mediaStream = screenStream
      }

      // update media streams (... spread operator)
      else {
        mediaStream = new MediaStream([
          ...screenStream.getVideoTracks(),
          ...voiceStream.getAudioTracks()
        ])
      }

      // mediaRecorder instance
      mediaRecorder = new MediaRecorder(mediaStream)
      mediaRecorder.ondataavailable = ({ data }) => {
        dataChunks.push(data)
        socketRef.current.emit('screenData:start', {
          username: username.current,
          data
        })
      }

      mediaRecorder.onstop = stopRecording;

      // ..
      mediaRecorder.start(250);
    }
  }

我们准备在没有声音的情况下写入屏幕,所以如果出现任何与接收音频流有关的错误(包括用户拒绝授予使用麦克风的权限),我们就把语音流设置为不可用。

让我们看一下标记。

function stopRecording() {
    setRecording(false)

    socketRef.current.emit('screenData:end', username.current)

    const videoBlob = new Blob(dataChunks, {
      type: 'video/webm' //... blob type of video web media
    })

    const videoSrc = URL.createObjectURL(videoBlob) //

    //...Refs and video source
    videoRef.current.src = videoSrc
    linkRef.current.href = videoSrc
    linkRef.current.download = `${Date.now()}-${username.current}.webm`

    //...
    mediaRecorder = null
    dataChunks = []
  }

  // bind the onClick method to a DOM button
  // to start or stop recording
  const onClick = () => {
    if (!recording) {
      startRecording()
    } else {
      if (mediaRecorder) {
        mediaRecorder.stop()
      }
    }
  }
  // loading spinner: we show the user a loading spinner till all needed permissions are granted.

  if (loading) return <Loader type='Oval' width='50' color='#027' />

返回语句中的JSX包括。

  • video 查看项目的项目
  • 一个下载视频记录的链接
  • 一个开始或停止录音的按钮
 return (
    <>
      <h1>Recorder App</h1>

      {/* */}
      <video controls ref={videoRef}></video>
      <a ref={linkRef}>Download</a>

      {/**/}
      <button onClick={onClick} disabled={!voiceStream}>
        {!recording ? 'Start' : 'Stop'}
      </button>
    </>
  )

在后端工作

我们将使用screen-record 项目文件夹内的server 文件夹作为后端。使用命令初始化一个新的Node.js项目。

 npm init -y

由于Node.js的LTS和更高版本支持ES6导入模块语法,我们需要在我们的package.json 文件上添加一个module 类型,以便在我们的后端启用它。

"type": "module",

为了在变化时自动监测并重新运行我们的服务器,让我们添加一个nodemon 模块。

npm install -D nodemon

触发这个事件的脚本是。

  "scripts": {
    "start": "node index.js",
    "dev": "nodemon index.js"
  },

接下来,在你的index.js 根文件中,从socket.io 中导入expressServer 对象。然后,onConnectionHandler 函数将处理我们的网络套接字连接。

import express from 'express';
import { Server } from 'socket.io';

// nodejs native module
import http from 'http';

// sockets connection event handler
import { onConnectionHandler } from './socket-io/onConnectionHandler.js';

index.js 文件中的导入下面,实例化套接字连接,并表达。

const app = express()
const server_app = http.createServer(app)
const io = new Server(server_app, {
  cors: {
    origin: 'http://localhost:3000'
  }
})

// listen to connection event before web sockets trigger
io.on('connection', onConnectionHandler);

接下来,创建一个utils 文件夹。在该文件夹内,添加一个saveData.js 文件,其中有一个函数来保存我们应用程序的录音。粘贴以下代码。

import { saveData } from '../utils/saveData.js'

const socketByUser = {};

// data chunks
const dataChunks = {};

export const onConnection = (socket) => {
  // user connection event
  socket.on('user:connected', (username) => {

    // create socket id from username
    if (!socketByUser[socket.id]) {
      socketByUser[socket.id] = username
    };
  });

  // push data chunks once recording starts
  socket.on('screenData:start', ({ data, username }) => {
    if (dataChunks[username]) {
      dataChunks[username].push(data)
    }

    else {
      dataChunks[username] = [data]
    }
  })

  socket.on('screenData:end', (username) => {
    if (dataChunks[username] && dataChunks[username].length) {
      saveData(dataChunks[username], username)
      dataChunks[username] = []
    }
  })

  // event handler on disconnect
  socket.on('disconnect', () => {
    const username = socketByUser[socket.id]
    if (dataChunks[username] && dataChunks[username].length) {
      saveData(dataChunks[username], username)
      dataChunks[username] = []
    }
  })
}

最后,在5000端口上启动服务器,带有一个回调函数,一旦事件被触发就会记录。

server.listen(5000, () => {
  console.log('Server ready... ');
})

一个运行中的演示

demo 1 demo 2

GitHub上查看源代码。

总结

在屏幕录像应用程序的帮助下,我们可以保存、复制和重复使用对企业或客户有帮助的视频,完成一系列企业任务。

在这篇文章中,我们已经了解了屏幕录制软件,它是什么,如何使用React和Node.js构建一个,以及它的一些好处。谢谢你的阅读!