openvidu-live-captions tutorial
This commit is contained in:
parent
4e90828d80
commit
f0358d0680
5
ai-services/openvidu-live-captions/README.md
Normal file
5
ai-services/openvidu-live-captions/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# OpenVidu Captions
|
||||
|
||||
This is the basic JavaScript tutorial extended to show live captions. It uses OpenVidu AI Services to generate captions from the audio Tracks of Participants in the Rooms.
|
||||
|
||||
Visit [Live Captions tutorial](https://openvidu.io/latest/docs/tutorials/ai-services/openvidu-live-captions/)
|
||||
231
ai-services/openvidu-live-captions/src/app.js
Normal file
231
ai-services/openvidu-live-captions/src/app.js
Normal file
@ -0,0 +1,231 @@
|
||||
// When running OpenVidu locally, leave these variables empty
|
||||
// For other deployment type, configure them with correct URLs depending on your deployment
|
||||
var APPLICATION_SERVER_URL = "";
|
||||
var LIVEKIT_URL = "";
|
||||
configureUrls();
|
||||
|
||||
const LivekitClient = window.LivekitClient;
|
||||
var room;
|
||||
|
||||
function configureUrls() {
|
||||
// If APPLICATION_SERVER_URL is not configured, use default value from OpenVidu Local deployment
|
||||
if (!APPLICATION_SERVER_URL) {
|
||||
if (window.location.hostname === "localhost") {
|
||||
APPLICATION_SERVER_URL = "http://localhost:6080/";
|
||||
} else {
|
||||
APPLICATION_SERVER_URL = "https://" + window.location.hostname + ":6443/";
|
||||
}
|
||||
}
|
||||
|
||||
// If LIVEKIT_URL is not configured, use default value from OpenVidu Local deployment
|
||||
if (!LIVEKIT_URL) {
|
||||
if (window.location.hostname === "localhost") {
|
||||
LIVEKIT_URL = "ws://localhost:7880/";
|
||||
} else {
|
||||
LIVEKIT_URL = "wss://" + window.location.hostname + ":7443/";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function joinRoom() {
|
||||
// Disable 'Join' button
|
||||
document.getElementById("join-button").disabled = true;
|
||||
document.getElementById("join-button").innerText = "Joining...";
|
||||
|
||||
// Initialize a new Room object
|
||||
room = new LivekitClient.Room();
|
||||
|
||||
// Specify the actions when events take place in the room
|
||||
// On every new Track received...
|
||||
room.on(
|
||||
LivekitClient.RoomEvent.TrackSubscribed,
|
||||
(track, _publication, participant) => {
|
||||
addTrack(track, participant.identity);
|
||||
}
|
||||
);
|
||||
|
||||
// On every new Track destroyed...
|
||||
room.on(
|
||||
LivekitClient.RoomEvent.TrackUnsubscribed,
|
||||
(track, _publication, participant) => {
|
||||
track.detach();
|
||||
document.getElementById(track.sid)?.remove();
|
||||
|
||||
if (track.kind === "video") {
|
||||
removeVideoContainer(participant.identity);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
room.registerTextStreamHandler(
|
||||
"lk.transcription",
|
||||
async (reader, participantInfo) => {
|
||||
const message = await reader.readAll();
|
||||
const isFinal =
|
||||
reader.info.attributes["lk.transcription_final"] === "true";
|
||||
|
||||
if (isFinal) {
|
||||
const audioTrackId = reader.info.attributes["lk.transcribed_track_id"];
|
||||
|
||||
// Due to a bug in LiveKit Server the participantInfo object may be empty.
|
||||
// You can still get the participant owning the audio track like below:
|
||||
const participant = [room.localParticipant]
|
||||
.concat(Array.from(room.remoteParticipants.values()))
|
||||
.find((p) => p.audioTrackPublications.has(audioTrackId));
|
||||
|
||||
const captionsTextarea = document.getElementById("captions");
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const participantIdentity =
|
||||
participant == room.localParticipant ? "You" : participant.identity;
|
||||
captionsTextarea.value += `[${timestamp}] ${participantIdentity}: ${message}\n`;
|
||||
captionsTextarea.scrollTop = captionsTextarea.scrollHeight;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
// Get the room name and participant name from the form
|
||||
const roomName = document.getElementById("room-name").value;
|
||||
const userName = document.getElementById("participant-name").value;
|
||||
|
||||
// Get a token from your application server with the room name and participant name
|
||||
const token = await getToken(roomName, userName);
|
||||
|
||||
// Connect to the room with the LiveKit URL and the token
|
||||
await room.connect(LIVEKIT_URL, token);
|
||||
|
||||
// Hide the 'Join room' page and show the 'Room' page
|
||||
document.getElementById("room-title").innerText = roomName;
|
||||
document.getElementById("join").hidden = true;
|
||||
document.getElementById("room").hidden = false;
|
||||
|
||||
// Publish your camera and microphone
|
||||
await room.localParticipant.enableCameraAndMicrophone();
|
||||
const localVideoTrack = this.room.localParticipant.videoTrackPublications
|
||||
.values()
|
||||
.next().value.track;
|
||||
addTrack(localVideoTrack, userName, true);
|
||||
} catch (error) {
|
||||
console.log("There was an error connecting to the room:", error.message);
|
||||
await leaveRoom();
|
||||
}
|
||||
}
|
||||
|
||||
function addTrack(track, participantIdentity, local = false) {
|
||||
const element = track.attach();
|
||||
element.id = track.sid;
|
||||
|
||||
/* If the track is a video track, we create a container and append the video element to it
|
||||
with the participant's identity */
|
||||
if (track.kind === "video") {
|
||||
const videoContainer = createVideoContainer(participantIdentity, local);
|
||||
videoContainer.append(element);
|
||||
appendParticipantData(
|
||||
videoContainer,
|
||||
participantIdentity + (local ? " (You)" : "")
|
||||
);
|
||||
} else {
|
||||
document.getElementById("layout-container").append(element);
|
||||
}
|
||||
}
|
||||
|
||||
async function leaveRoom() {
|
||||
// Leave the room by calling 'disconnect' method over the Room object
|
||||
await room.disconnect();
|
||||
|
||||
// Remove all HTML elements inside the layout container
|
||||
removeAllLayoutElements();
|
||||
|
||||
// Clear the captions textarea
|
||||
document.getElementById("captions").value = "";
|
||||
|
||||
// Back to 'Join room' page
|
||||
document.getElementById("join").hidden = false;
|
||||
document.getElementById("room").hidden = true;
|
||||
|
||||
// Enable 'Join' button
|
||||
document.getElementById("join-button").disabled = false;
|
||||
document.getElementById("join-button").innerText = "Join!";
|
||||
}
|
||||
|
||||
window.onbeforeunload = () => {
|
||||
room?.disconnect();
|
||||
};
|
||||
|
||||
window.onload = generateFormValues;
|
||||
|
||||
function generateFormValues() {
|
||||
document.getElementById("room-name").value = "Test Room";
|
||||
document.getElementById("participant-name").value =
|
||||
"Participant" + Math.floor(Math.random() * 100);
|
||||
}
|
||||
|
||||
function createVideoContainer(participantIdentity, local = false) {
|
||||
const videoContainer = document.createElement("div");
|
||||
videoContainer.id = `camera-${participantIdentity}`;
|
||||
videoContainer.className = "video-container";
|
||||
const layoutContainer = document.getElementById("layout-container");
|
||||
|
||||
if (local) {
|
||||
layoutContainer.prepend(videoContainer);
|
||||
} else {
|
||||
layoutContainer.append(videoContainer);
|
||||
}
|
||||
|
||||
return videoContainer;
|
||||
}
|
||||
|
||||
function appendParticipantData(videoContainer, participantIdentity) {
|
||||
const dataElement = document.createElement("div");
|
||||
dataElement.className = "participant-data";
|
||||
dataElement.innerHTML = `<p>${participantIdentity}</p>`;
|
||||
videoContainer.prepend(dataElement);
|
||||
}
|
||||
|
||||
function removeVideoContainer(participantIdentity) {
|
||||
const videoContainer = document.getElementById(
|
||||
`camera-${participantIdentity}`
|
||||
);
|
||||
videoContainer?.remove();
|
||||
}
|
||||
|
||||
function removeAllLayoutElements() {
|
||||
const layoutElements = document.getElementById("layout-container").children;
|
||||
Array.from(layoutElements).forEach((element) => {
|
||||
element.remove();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* --------------------------------------------
|
||||
* GETTING A TOKEN FROM YOUR APPLICATION SERVER
|
||||
* --------------------------------------------
|
||||
* The method below request the creation of a token to
|
||||
* your application server. This prevents the need to expose
|
||||
* your LiveKit API key and secret to the client side.
|
||||
*
|
||||
* In this sample code, there is no user control at all. Anybody could
|
||||
* access your application server endpoints. In a real production
|
||||
* environment, your application server must identify the user to allow
|
||||
* access to the endpoints.
|
||||
*/
|
||||
async function getToken(roomName, participantName) {
|
||||
const response = await fetch(APPLICATION_SERVER_URL + "token", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
roomName,
|
||||
participantName,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(`Failed to get token: ${error.errorMessage}`);
|
||||
}
|
||||
|
||||
const token = await response.json();
|
||||
return token.token;
|
||||
}
|
||||
94
ai-services/openvidu-live-captions/src/index.html
Normal file
94
ai-services/openvidu-live-captions/src/index.html
Normal file
@ -0,0 +1,94 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>OpenVidu Live Captions</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="shortcut icon" href="resources/images/favicon.ico" type="image/x-icon" />
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<link
|
||||
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
|
||||
rel="stylesheet"
|
||||
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<script
|
||||
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
|
||||
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
|
||||
crossorigin="anonymous"
|
||||
></script>
|
||||
|
||||
<!-- Font Awesome -->
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css"
|
||||
integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A=="
|
||||
crossorigin="anonymous"
|
||||
referrerpolicy="no-referrer"
|
||||
/>
|
||||
|
||||
<link rel="stylesheet" href="styles.css" type="text/css" media="screen" />
|
||||
<script src="https://cdn.jsdelivr.net/npm/livekit-client@2.13.4/dist/livekit-client.umd.js"></script>
|
||||
<script src="app.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<a href="/" title="Home"><h1>Basic JavaScript</h1></a>
|
||||
<div id="links">
|
||||
<a
|
||||
href="https://github.com/OpenVidu/openvidu-livekit-tutorials/tree/master/application-client/openvidu-js"
|
||||
title="GitHub Repository"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="fa-brands fa-github"></i>
|
||||
</a>
|
||||
<a
|
||||
href="https://livekit-tutorials.openvidu.io/tutorials/application-client/javascript/"
|
||||
title="Documentation"
|
||||
target="_blank"
|
||||
>
|
||||
<i class="fa-solid fa-book"></i>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div id="join">
|
||||
<div id="join-dialog">
|
||||
<h2>Join a Video Room</h2>
|
||||
<form onsubmit="joinRoom(); return false">
|
||||
<div>
|
||||
<label for="participant-name">Participant</label>
|
||||
<input id="participant-name" class="form-control" type="text" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="room-name">Room</label>
|
||||
<input id="room-name" class="form-control" type="text" required />
|
||||
</div>
|
||||
<button id="join-button" class="btn btn-lg btn-success" type="submit">Join!</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="room" hidden>
|
||||
<div id="room-header">
|
||||
<h2 id="room-title"></h2>
|
||||
<button class="btn btn-danger" id="leave-room-button" onclick="leaveRoom()">Leave Room</button>
|
||||
</div>
|
||||
<div id="layout-container"></div>
|
||||
<div id="captions-container">
|
||||
<textarea id="captions" class="form-control" rows="3"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<p class="text">Made with love by <span>OpenVidu Team</span></p>
|
||||
<a href="http://www.openvidu.io/" target="_blank">
|
||||
<img id="openvidu-logo" src="resources/images/openvidu_logo.png" alt="OpenVidu logo" />
|
||||
</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
283
ai-services/openvidu-live-captions/src/styles.css
Normal file
283
ai-services/openvidu-live-captions/src/styles.css
Normal file
@ -0,0 +1,283 @@
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-top: 50px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
header {
|
||||
height: 50px;
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 30px;
|
||||
background-color: #4d4d4d;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
header a {
|
||||
color: #ccc;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
header a:hover {
|
||||
color: #a9a9a9;
|
||||
}
|
||||
|
||||
header i {
|
||||
padding: 5px 5px;
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#join {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#join-dialog {
|
||||
width: 70%;
|
||||
max-width: 900px;
|
||||
padding: 60px;
|
||||
border-radius: 6px;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
#join-dialog h2 {
|
||||
color: #4d4d4d;
|
||||
font-size: 60px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#join-dialog form {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#join-dialog label {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
color: #0088aa;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
margin-bottom: 10px;
|
||||
box-sizing: border-box;
|
||||
color: #0088aa;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
color: #0088aa;
|
||||
border-color: #0088aa;
|
||||
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(0, 136, 170, 0.6);
|
||||
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(0, 136, 170, 0.6);
|
||||
}
|
||||
|
||||
#join-dialog button {
|
||||
display: block;
|
||||
margin: 20px auto 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background-color: #06d362;
|
||||
border-color: #06d362;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background-color: #1abd61;
|
||||
border-color: #1abd61;
|
||||
}
|
||||
|
||||
#room {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#room-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
padding: 0 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#room-title {
|
||||
font-size: 2em;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#layout-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 10px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
max-width: 1000px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
position: relative;
|
||||
background: #3b3b3b;
|
||||
aspect-ratio: 16/9;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-container video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.video-container .participant-data {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.participant-data p {
|
||||
background: #f8f8f8;
|
||||
margin: 0;
|
||||
padding: 0 5px;
|
||||
color: #777777;
|
||||
font-weight: bold;
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
|
||||
footer {
|
||||
height: 60px;
|
||||
width: 100%;
|
||||
padding: 10px 30px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #4d4d4d;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #ffffff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
footer .text {
|
||||
color: #ccc;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
footer .text span {
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#openvidu-logo {
|
||||
height: 35px;
|
||||
-webkit-transition: all 0.1s ease-in-out;
|
||||
-moz-transition: all 0.1s ease-in-out;
|
||||
-o-transition: all 0.1s ease-in-out;
|
||||
transition: all 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
#openvidu-logo:hover {
|
||||
-webkit-filter: grayscale(0.5);
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
/* Media Queries */
|
||||
@media screen and (max-width: 768px) {
|
||||
header {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
#join-dialog {
|
||||
width: 90%;
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
#join-dialog h2 {
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
#layout-container {
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
header {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
#join-dialog {
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#join-dialog h2 {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
#layout-container {
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
}
|
||||
|
||||
.video-container {
|
||||
aspect-ratio: 9/16;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
#captions-container {
|
||||
width: 100%;
|
||||
padding: 25px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
min-height: 100px;
|
||||
padding: 10px;
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user