
シェア:
ベンはセカンドキャリアの開発者で、以前は成人教育、コミュニティ組織化、非営利団体運営の分野で10年を過ごした。彼はVonageの開発者支援者として働いていた。コミュニティ開発とテクノロジーの交差点について定期的に執筆している。南カリフォルニア出身で、長年ニューヨークに住んでいたが、現在はイスラエルのテルアビブ近郊に在住。
Ruby on RailsとVonage Video APIでパーティーを作ろう Part 2
所要時間:4 分
これは、Vonage Video APIとRuby on Railsを使ってVideoウォッチ・パーティー・アプリケーションを作成する2部構成のシリーズの第2部です。
最初の記事では 最初の記事では、アプリのバックエンドを構築する手順を説明した。その記事をまだ読んでいないのであれば、そこから始めるのがいいだろう。今度は、アプリケーションのフロントエンドに焦点を当てよう。バックエンドは主にRubyで書かれましたが、フロントエンドはクライアントサイドのJavaScriptを多用します。
それが終われば、ビデオ鑑賞パーティーのアプリができあがり、友達とチャットしたり、一緒にビデオを見たりできるようになる!

始めよう!
tl;drこのアプリをデプロイしたいのであれば、アプリのコードはすべて GitHub.
何を作るのか
コーディングを始める前に、少し時間をとって、これから何を作るかについて話し合っておくとよいだろう。
最初の投稿を思い出すと、私たちは Video API セッション ID をインスタンス化し、各参加者のトークンをアクティブに作成しています。この情報は、ERB ビューファイルで新しく作成された JavaScript 変数によってフロントエンドに渡されます。さらに、環境変数のデータもフロントエンドに渡しています。私たちは、アプリのエクスペリエンスを作成するために記述するコードで、これらの情報をすべて使用します。
Ruby on Railsは、バージョン5.1からRailsにWebpackを導入したことで、クライアントサイドJavaScriptをスタックに直接統合できるようになりました。JavaScriptは パックの中に配置され /app/javascript/packsの中に配置され importまたは require()ステートメントとして application.jsファイル内に追加される。
私たちのコードのさまざまな懸念事項を別のファイルに分離して、最終的にあなたのフォルダーには以下のファイルがあるようにする:
各ファイルには application.js以外の各ファイルには、それぞれの懸念事項をカバーするコードが含まれている:
app_helpers.js:フロントエンド全体で必要とされるクロスファンクショナルなコードchat.js:クラス作成Chatテキストチャットのインスタンスを生成するためのクラスを作成する。opentok_screenshare.js:スクリーンシェア・ビューのクライアント側コードopentok_video.js:Video Chat ビューのクライアント側コードparty.js:ビデオチャットのインスタンスをインスタンス化するPartyVideoチャットのインスタンスを生成するためのクラスを作成する。screenshare.js:クラス作成Screenshareスクリーンシェア機能のインスタンスを生成するために使用されるクラスの作成
コードを作成する前に、これらのファイルを application.jsファイルに追加して、実行時にコンパイルするように Webpack に指示します:
// application.js
import './app_helpers.js'
import './opentok_video.js'
import './opentok_screenshare.js' JavaScriptパックの作成
各サブセクションでは、上で列挙したJavaScriptファイルを作成する。
についてapp_helpers.jsファイル
この app_helpers.jsファイルには汎用のヘルパー関数が含まれ、アプリ全体で使用できるように残りのコードにエクスポートする。私たちは screenshareMode(), setButtonDisplay(), formatChatMsg()そして streamLayout()関数を作ります。
この screenshareMode()関数は、Vonage Video API Signal API を利用して、すべての参加者のブラウザにメッセージを送信し、変更をトリガーします。 window.locationをトリガーします。Signal API は、テキストチャットに使用するのと同じ API で、最も単純な使用例です。しかし、この関数で見るように、Signal API は、たくさんのコードを書く必要なく、参加者全員に対して同時にアプリケーションのフローを指示する直感的で強力な方法を提供します:
export function screenshareMode(session, mode) {
if (mode == 'on') {
window.location = '/screenshare?name=' + name;
session.signal({
type: 'screenshare',
data: 'on'
});
} else if (mode == 'off') {
window.location = '/party?name=' + name;
session.signal({
type: 'screenshare',
data: 'off'
});
};
};次の関数 setButtonDisplay()は、"Watch Mode On/Off "ボタンを含む HTML 要素のスタイルを blockまたは noneに変更します。より安全な方法を含め、これを行う方法は他にもたくさんあります。しかし、友人同士でビデオを見るためのこのアプリでは、物事をシンプルに保つために、最小限のものにしておきます:
export function setButtonDisplay(element) {
if (name == moderator_env_name) {
element.style.display = "block";
} else {
element.style.display = "none";
};
};この formatChatMsg()関数は参加者が送信したテキストメッセージを引数として受け取り、サイト上で表示するためにフォーマットします。この関数は、2つのコロンで括られたテキストを探し、コロン内のテキストを絵文字として解析しようとします。また、各メッセージに参加者の名前を追加し、誰が話しているのかがわかるようにします。
絵文字を追加するには、ノードパッケージ node-emojiというnodeパッケージをインストールする必要がある。 const emoji = require('node-emoji);ファイルの先頭に yarn add node-emojiをコマンドラインで実行する。この関数は match()この関数は、正規表現を使って2つのコロンでブックマークされた文字列を検索し、マッチすれば emojiを呼び出してその文字列を絵文字に変換する:
export function formatChatMsg(message) {
var message_arr;
message_arr = message.split(' ').map(function(word) {
if (word.match(/(?:\:)\b(\w*)\b(?=\:)/g)) {
return word = emoji.get(word);
} else {
return word;
}
})
message = message_arr.join(' ');
return `${name}: ${message}`
};最後の関数は app_helpers.jsの中に作る必要がある最後の関数は streamLayout()で、HTML要素の引数と参加者数を受け取ります。この関数は、ビデオチャットのプレゼンテーションをグリッド形式に変更するために、参加者の数に応じて要素に CSS クラスを追加または削除します:
export function streamLayout(element, count) {
if (count >= 6) {
element.classList.add("grid9");
} else if (count == 5) {
element.classList.remove("grid9");
element.classList.add("grid4");
} else if (count < 5) {
element.classList.remove("grid4");
}
};
についてchat.jsファイル
この chat.jsコードは Chatクラスを作成します。 constructor().この Chatクラスは、ビデオチャットとスクリーンシェア・ビューの両方で呼び出され、インスタンス化されます:
// chat.js
import { formatChatMsg } from './app_helpers.js';
export default class Chat {
constructor(session) {
this.session = session;
this.form = document.querySelector('form');
this.msgTxt = document.querySelector('#message');
this.msgHistory = document.querySelector('#history');
this.chatWindow = document.querySelector('.chat');
this.showChatBtn = document.querySelector('#showChat');
this.closeChatBtn = document.querySelector('#closeChat');
this.setupEventListeners();
}にいくつかのプロパティを与えました。 Chatには、主に DOM 内のさまざまな要素と Video API セッションに基づいて、いくつかのプロパティを与えています。最後のもの、 this.setupEventListeners()は、ファイルに追加する必要がある関数を呼び出しています:
setupEventListeners() {
let self = this;
this.form.addEventListener('submit', function(event) {
event.preventDefault();
self.session.signal({
type: 'msg',
data: formatChatMsg(self.msgTxt.value)
}, function(error) {
if (error) {
console.log('Error sending signal:', error.name, error.message);
} else {
self.msgTxt.value = '';
}
});
});
this.session.on('signal:msg', function signalCallback(event) {
var msg = document.createElement('p');
msg.textContent = event.data;
msg.className = event.from.connectionId === self.session.connection.connectionId ? 'mine' : 'theirs';
self.msgHistory.appendChild(msg);
msg.scrollIntoView();
});
this.showChatBtn.addEventListener('click', function(event) {
self.chatWindow.classList.add('active');
});
this.closeChatBtn.addEventListener('click', function(event) {
self.chatWindow.classList.remove('active');
});
}
}setupEventListeners()テキストチャット用の EventListenerテキストチャット submitボタンを作成します。新しいメッセージが送信されると、Signal APIに送られ、処理されてすべての参加者に送信されます。同様に、新しいメッセージが受信されると、新しい <p>タグが追加され、参加者のテキストチャットウィンドウはそれを見るためにスクロールされます。
次に作成する2つのファイルは、ビデオチャットパーティー用とスクリーンシェアビュー用の新しいクラスを作成する際に、同様の機能を実行します。
についてparty.jsファイル
このファイルでは Partyクラスを作成します:
// party.js
import { screenshareMode, setButtonDisplay, streamLayout } from './app_helpers.js';
export default class Party {
constructor(session) {
this.session = session;
this.watchLink = document.getElementById("watch-mode");
this.subscribers = document.getElementById("subscribers");
this.participantCount = document.getElementById("participant-count");
this.videoPublisher = this.setupVideoPublisher();
this.clickStatus = 'off';
this.setupEventHandlers();
this.connectionCount = 0;
setButtonDisplay(this.watchLink);
}この constructor()関数は Video API セッションを引数として受け取り、それを this.session.残りのプロパティは定義され、値が与えられます。残りのプロパティは watchLink, subscribers, participantCountプロパティはHTML要素に由来し videoPublisherはその値として関数が与えられ clickStatusはデフォルトで off.
この時点で setupVideoPublisher()関数を作成します。この関数は Video API JavaScript SDK を呼び出します。 initPublisher()関数を呼び出して Video の公開を開始します。この関数はオプションの引数を取ることができ、ここでは Video が要素の幅と高さを 100% 占め、要素に追加されるように指定します:
setupVideoPublisher() {
return OT.initPublisher('publisher', {
insertMode: 'append',
width: "100%",
height: "100%"
}, function(error) {
if (error) {
console.error('Failed to initialise publisher', error);
};
});
}イベントリスナーを作成し、クラスに追加する必要があるアクションもいくつかあります。セッションが接続されたとき、Videoストリームが作成されたとき、接続が追加されたとき、接続が破棄されたときをリスニングする必要があります。接続が追加または破棄されたとき、参加者カウントをインクリメントまたはデクリメントし、参加者カウントの参加者数を共有します。 <div>要素で参加者数を共有します:
setupEventHandlers() {
let self = this;
this.session.on({
// This function runs when session.connect() asynchronously completes
sessionConnected: function(event) {
// Publish the publisher we initialzed earlier (this will trigger 'streamCreated' on other
// clients)
self.session.publish(self.videoPublisher, function(error) {
if (error) {
console.error('Failed to publish', error);
}
});
},
// This function runs when another client publishes a stream (eg. session.publish())
streamCreated: function(event) {
// Subscribe to the stream that caused this event, and place it into the element with id="subscribers"
self.session.subscribe(event.stream, 'subscribers', {
insertMode: 'append',
width: "100%",
height: "100%"
}, function(error) {
if (error) {
console.error('Failed to subscribe', error);
}
});
},
// This function runs whenever a client connects to a session
connectionCreated: function(event) {
self.connectionCount++;
self.participantCount.textContent = `${self.connectionCount} Participants`;
streamLayout(self.subscribers, self.connectionCount);
},
// This function runs whenever a client disconnects from the session
connectionDestroyed: function(event) {
self.connectionCount--;
self.participantCount.textContent = `${self.connectionCount} Participants`;
streamLayout(self.subscribers, self.connectionCount);
}
});最後に、もうひとつイベント・リスナーを追加する。このイベント・リスナーは clickアクションにアタッチされます。クリックされると、クリック・ステータスがオフの場合、スクリーンシェア・ビューに移動します。クラスの作成時に、クリック・ステータスがデフォルトでオフに設定されていることを思い出してください:
this.watchLink.addEventListener('click', function(event) {
event.preventDefault();
if (self.clickStatus == 'off') {
// Go to screenshare view
screenshareMode(self.session, 'on');
};
});
}
} についてscreenshare.jsファイル
最後に作成するクラスは Screenshareクラスです。この constructor()関数は Video API セッションと参加者の名前を引数にとります:
// screenshare.js
import { screenshareMode } from './app_helpers.js';
export default class Screenshare {
constructor(session, name) {
this.session = session;
this.name = name;
this.watchLink = document.getElementById("watch-mode");
this.clickStatus = 'on';
}クラスとは異なり Partyクラスとは異なり clickStatusにデフォルト設定されています。 onに設定されているため、モデレーターが "Watch Mode On/Off "ボタンをクリックすると、スクリーンシェアからビデオチャットモードに戻ります。
また toggle()を使用して、参加者がモデレーターの場合は参加者の画面を共有し、それ以外の場合はスクリーンシェアを購読します:
toggle() {
if (this.name === moderator_env_name) {
this.shareScreen();
} else {
this.subscribe();
}
}で呼び出される shareScreen()関数で呼び出される toggle()を定義する必要がある:
shareScreen() {
this.setupPublisher();
this.setupAudioPublisher();
this.setupClickStatus();
}この関数自体にも3つの関数を作成する必要がある。最初の関数は司会者の画面を公開します。しかし、画面の公開だけでは音声は含まれません。そのため、2つ目の関数で司会者のパソコンから音声を公開します。そして shareScreen()の最後の関数は、"Watch Mode On/Off "ボタンがクリックされた場合、ビデオチャットビューに戻ります:
setupClickStatus() {
// screen share mode off if clicked off
// Set click status
let self = this;
this.watchLink.addEventListener('click', function(event) {
event.preventDefault();
if (self.clickStatus == 'on') {
self.clickStatus = 'off';
screenshareMode(self.session, 'off');
};
});
}
setupAudioPublisher() {
var self = this;
var audioPublishOptions = {};
audioPublishOptions.insertMode = 'append';
audioPublishOptions.publishVideo = false;
var audio_publisher = OT.initPublisher('audio', audioPublishOptions,
function(error) {
if (error) {
console.log(error);
} else {
self.session.publish(audio_publisher, function(error) {
if (error) {
console.log(error);
}
});
};
}
);
}
setupPublisher() {
var self = this;
var publishOptions = {};
publishOptions.videoSource = 'screen';
publishOptions.insertMode = 'append';
publishOptions.height = '100%';
publishOptions.width = '100%';
var screen_publisher = OT.initPublisher('screenshare', publishOptions,
function(error) {
if (error) {
console.log(error);
} else {
self.session.publish(screen_publisher, function(error) {
if (error) {
console.log(error);
};
});
};
}
);
}上記はすべて、司会者のためにスクリーンシェアを作成するためです。アプリ内の他の誰もが、そのスクリーンシェアを購読したいと思うでしょう。そのために subscribe()関数を使います。これがファイル内の最後の関数になります:
subscribe() {
var self = this;
this.watchLink.style.display = "none";
this.session.on({
streamCreated: function(event) {
console.log(event);
if (event.stream.hasVideo == true) {
self.session.subscribe(event.stream, 'screenshare', {
insertMode: 'append',
width: '100%',
height: '100%'
}, function(error) {
if (error) {
console.error('Failed to subscribe to video feed', error);
}
});
} else if (event.stream.hasVideo == false ) {
self.session.subscribe(event.stream, 'audio', {
insertMode: 'append',
width: '0px',
height: '0px'
}, function(error) {
if (error) {
console.error('Failed to subscribe to audio feed', error);
}
});
};
}
});
}
}これで、定義したこれらのクラスのインスタンスを opentok_screenshare.jsと opentok_video.jsファイルでインスタンスを作成します。
創造するopentok_video.js
この opentok_video.jsファイルは新しいビデオチャット体験を構築する。ほとんどの作業は上で定義したクラスで行われたので、このファイルは比較的小さい。まず Chatクラスと Partyクラスをインポートしましょう:
// opentok_video.js
import Chat from './chat.js'
import Party from './party.js'次に、Video API セッションを保持する空のグローバル変数を定義する:
var session = ''そして、残りのコードを3つのチェックで囲み、正しいWebサイトのパスであること、DOMが完全にロードされていること、参加者の名前が空でないことを確認します:
if (window.location.pathname == '/party') {
document.addEventListener('DOMContentLoaded', function() {
if (name != '') {残りのコードでは、Video API セッションが存在しない場合、新しい Video API セッションを開始し、Video API セッションのインスタンスを作成します。 Chatと new Party.最後に、Signal API が screenshareの値を持つデータ・メッセージが送信されるのを待ちます。 on.そのメッセージを受信すると window.locationは /screenshare:
// Initialize an OpenTok Session object
if (session == '') {
session = OT.initSession(api_key, session_id);
}
new Chat(session);
new Party(session);
// Connect to the Session using a 'token'
session.connect(token, function(error) {
if (error) {
console.error('Failed to connect', error);
}
});
// Listen for Signal screenshare message
session.on('signal:screenshare', function screenshareCallback(event) {
if (event.data == 'on') {
window.location = '/screenshare?name=' + name;
};
});
};
});
} 創造するopentok_screenshare.js
最後に作成するJavaScriptファイルは、前回とよく似ている。これはスクリーンシェア・ビューを担当し Screenshareと Chatクラスを活用しています:
import Screenshare from './screenshare.js'
import Chat from './chat.js'
// declare empty global session variable
var session = ''
if (window.location.pathname == '/screenshare') {
document.addEventListener('DOMContentLoaded', function() {
// Initialize an OpenTok Session object
if (session == '') {
session = OT.initSession(api_key, session_id);
}
// Hide or show watch party link based on participant
if (name != '' && window.location.pathname == '/screenshare') {
new Chat(session);
new Screenshare(session, name).toggle();
// Connect to the Session using a 'token'
session.connect(token, function(error) {
if (error) {
console.error('Failed to connect', error);
}
});
// Listen for Signal screenshare message
session.on('signal:screenshare', function screenshareCallback(event) {
if (event.data == 'off') {
window.location = '/party?name=' + name;
};
});
}
});
};
これをまとめる前に、最後になりますが、アプリケーションのフロントエンドのスタイルを定義する必要があります。参加者がアクセスできなければ、このコードはすべて無意味です。
アプリケーションのスタイリング
このアプリケーションのスタイルシートは、私の友人であり元同僚の助けなしには実現しなかっただろう、 ホイ・ジン・チェン彼はこのプロセスを通じて、フロントエンド・デザインについて多くのことを教えてくれた。このアプリでは主に フレックスボックスグリッドを使用しています。
まずは custom.cssファイルを app/javascript/stylesheets.このファイルがアプリケーションに含まれていることを確認したいので、同じフォルダー内の application.scssにインポート行を追加します、 @import './custom.css';.
まず、コアとなるスタイルを custom.css:
:root {
--main: #343a40;
--txt-alt: white;
--txt: black;
--background: white;
--bgImage: url('~images/01.png');
--chat-bg: rgba(255, 255, 255, 0.75);
--chat-mine: darkgreen;
--chat-theirs: indigo;
}
html {
box-sizing: border-box;
height: 100%;
}
*,
*::before,
*::after {
box-sizing: inherit;
margin: 0;
padding: 0;
}
body {
height: 100%;
display: flex;
flex-direction: column;
background-color: var(--background);
background-image: var(--bgImage);
overflow: hidden;
}
main {
flex: 1;
display: flex;
position: relative;
}
input {
font-size: inherit;
padding: 0.5em;
border-radius: 4px;
border: 1px solid currentColor;
}
button,
input[type="submit"] {
font-size: inherit;
padding: 0.5em;
border: 0;
background-color: var(--main);
color: var(--txt-alt);
border-radius: 4px;
}
header {
background-color: var(--main);
color: var(--txt-alt);
padding: 0.5em;
height: 4em;
display: flex;
align-items: center;
justify-content: space-between;
}次に、ランディングページのスタイリングを追加しよう:
.landing {
margin: auto;
text-align: center;
font-size: 125%;
}
.landing form {
display: flex;
flex-direction: column;
margin: auto;
position: relative;
}
.landing input,
.landing p {
margin-bottom: 1em;
}
.landing .error {
color: maroon;
position: absolute;
bottom: -2em;
width: 100%;
text-align: center;
}また、テキストチャットのスタイリングも追加したい。特に、チャットが所定の位置に留まり、ページ全体がスクロールしないようにしたい:
.chat {
width: 100%;
display: flex;
flex-direction: column;
height: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 2;
background-color: var(--chat-bg);
transform: translateX(-100%);
transition: transform 0.5s ease;
}
.chat.active {
transform: translateX(0);
}
.chat-header {
padding: 0.5em;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.24);
display: flex;
justify-content: space-between;
}
.btn-chat {
height: 5em;
width: 5em;
border-radius: 50%;
box-shadow: 0 3px 6px 0 rgba(0, 0, 0, .2), 0 3px 6px 0 rgba(0, 0, 0, .19);
position: fixed;
right: 1em;
bottom: 1em;
cursor: pointer;
}
.btn-chat svg {
height: 4em;
width: 2.5em;
}
.btn-close {
height: 2em;
width: 2em;
background: transparent;
border: none;
cursor: pointer;
}
.btn-close svg {
height: 1em;
width: 1em;
}
.messages {
flex: 1;
display: flex;
flex-direction: column;
overflow-y: scroll;
padding: 1em;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.24);
scrollbar-color: #c1c1c1 transparent;
}
.messages p {
margin-bottom: 0.5em;
}
.mine {
color: var(--chat-mine);
}
.theirs {
color: var(--chat-theirs);
}
.chat form {
display: flex;
padding: 1em;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12), 0 1px 3px rgba(0, 0, 0, 0.24);
}
.chat input[type="text"] {
flex: 1;
border-top-left-radius: 0px;
border-bottom-left-radius: 0px;
background-color: var(--background);
color: var(--txt);
min-width: 0;
}
.chat input[type="submit"] {
border-top-right-radius: 0px;
border-bottom-right-radius: 0px;
}それでは、ビデオチャットとスクリーンシェア要素のスタイリングを作成しましょう:
.videos {
flex: 1;
display: flex;
position: relative;
}
.subscriber.grid4 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(25em, 1fr));
}
.subscriber.grid9 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(18em, 1fr));
}
.subscriber,
.screenshare {
width: 100%;
height: 100%;
display: flex;
}
.publisher {
position: absolute;
width: 25vmin;
height: 25vmin;
min-width: 8em;
min-height: 8em;
align-self: flex-end;
z-index: 1;
}
.audio {
position: absolute;
opacity: 0;
z-index: -1;
}
.audio {
display: none;
}
.dark {
--background: black;
--chat-mine: lime;
--chat-theirs: violet;
--txt: white;
}最後に、小さい画面でもテキストチャットの比率を保つメディアクエリを追加します:
@media screen and (min-aspect-ratio: 1 / 1) {
.chat {
width: 20%;
min-width: 16em;
}
}これで終わりです!アプリケーションは、バックエンドとフロントエンドの両方が作成されました。これですべてをまとめる準備ができました。
すべてをまとめる
アプリケーションはRubyとJavaScriptという複数のプログラミング言語の組み合わせで、バックエンドとフロントエンドが絡み合っているにもかかわらず、実行は比較的簡単だ。Railsを使えば、1つのコマンドですべてをシームレスに統合できるからだ。
コマンドラインから bundle exec rails sを実行すると、Railsサーバーが起動します。また、アプリを初めて実行すると、コンソール出力に次のようなほとんど魔法のような行が表示されます:
実際、JavaScriptやCSSパックに変更を加えるたびに、この出力が表示されます。この出力は、RailsがWebpackを使ってすべてのパックをコンパイルし、アプリケーションに組み込んでいることを示しています。コンパイルが [Webpacker] Compiling...が完了すると、コンパイル済みのすべてのパックのリストが表示されます:
ファイル名は、コンパイルされたことを反映していますが、よく見るとパック名が残っています。 opentok_screenshare, party, app_helpersなど。
アプリケーションをローカルで実行するのは、自分自身とテストするのには最適ですが、おそらく、友人を招待して一緒に参加したいと思うでしょう!
ngrokのようなツールを使えば、ローカルで動作しているアプリケーションに外部からアクセスできるリンクを作成できる。これは、ローカル環境の外部URLを提供します。Nexmo Developer Platformに ngrokの立ち上げと実行にガイドがあるので、それに従ってください。
すぐに使い始めたい場合は、次のアプリケーションをワンクリックでデプロイできます。 GitHubから直接Herokuにデプロイすることもできます。
Vonage Video APIを使って何を作ったか、ぜひお聞かせください!私たちの コミュニティ・スラックで会話に参加し、あなたのストーリーを共有してください!