近年、ABEMAをはじめとする動画配信サービスでは、視聴中にリアルタイムでコメントが画面上に表示される機能が広く用いられています。このような「動画配信と同時にコメントが流れる仕組み」を、実際に自分で構築してみたいと考えたことはないでしょうか。
構成概要(今回のステップ分割)
使用技術
| 項目 | 技術 |
|---|---|
| フロントエンド | HTML / CSS / JavaScript / Vue.js |
| リアルタイム通信 | Socket.IO(Node.js) |
| データベース | MariaDB(XAMPP環境) |
| サーバーサイド | Node.js + Express |
| 動画 | ローカル or 外部ソースのMP4ファイル |
| コメント表示 | 上方向に流れるチャットUI |
- [Step 1]: フロントエンドの基本構成(Vue.js でUI作成)
- [Step 2]: コメントリアルタイム通信(Socket.IO接続)
- [Step 3]: サーバー側(Node.js + Socket.IO + DB連携)
本記事では、Vue.js、Socket.IO、Node.js、そしてMariaDB(XAMPP環境)を組み合わせて、動画を再生しながらリアルタイムでコメントを共有・表示するシステムの構築方法を、ステップごとに解説してまいります。
視聴中の動画上にコメントが即時に表示され、他のユーザーと同じタイミングでやりとりができる仕組みを、実際に動作する形で実装していきます。いわば「ミニABEMA」とも言えるような構成を目指しており、初学者の方にも理解しやすいよう、使用する技術の選定理由や各工程の手順について説明いたします。
Step 1: フロントエンドの基本構成(Vue.js)
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>リアルタイムコメント付き動画</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app">
<div id="video-container">
<video id="main-video" controls>
<source src="./movie.mp4" type="video/mp4">
</video>
<div id="comments-side">
<div id="comments-container" v-if="comments.length > 0">
<div class="comment" v-for="comment in comments" :key="comment.id">
{{ comment.text }}
</div>
</div>
<div id="comment-input-container">
<input
type="text"
v-model="newComment"
@keyup.enter="addComment"
placeholder="コメントを入力">
</div>
</div>
</div>
</div>
<script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
<script src="main.js"></script>
</body>
</html>HTMLstyle.css
html, body {
height: 100%;
margin: 0;
background-color: #000;
color: #fff;
font-family: sans-serif;
}
#app {
height: 100%;
display: flex;
flex-direction: column;
}
#video-container {
flex: 1;
display: flex;
flex-direction: row;
height: 100%;
overflow: hidden;
}
#main-video {
width: 75%;
height: 100%;
object-fit: cover;
}
/* 右側コメント全体 */
#comments-side {
width: 25%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end; /* ← 必須 */
background-color: #111;
border-left: 1px solid #333;
box-sizing: border-box;
}
/* コメント一覧(上部) */
#comments-container {
flex: 1;
overflow-y: auto;
display: flex;
flex-direction: column-reverse;
padding: 10px;
}
/* コメント1つの表示 */
.comment {
padding: 5px 10px;
border-bottom: 1px solid #444;
font-size: 14px;
}
/* 入力欄(下部に固定) */
#comment-input-container {
width: 100%;
padding: 10px;
background-color: #111;
border-top: 1px solid #333;
box-sizing: border-box;
}
#comment-input-container input {
width: 100%;
padding: 10px;
font-size: 14px;
background-color: #222;
border: 1px solid #555;
color: #fff;
border-radius: 4px;
box-sizing: border-box;
outline: none;
}CSSmain.js
const { createApp } = Vue;
createApp({
data() {
return {
newComment: '',
comments: []
};
},
methods: {
addComment() {
if (this.newComment.trim()) {
this.comments.unshift({
id: Date.now(),
text: this.newComment.trim()
});
this.newComment = '';
}
}
}
}).mount('#app');JavaScriptフロントエンドをXAMPPで動かす手順(Windows前提)
フォルダ構成を準備
- C:\xampp\htdocs に移動
- 任意のプロジェクトフォルダを作成(例:live-comment-app)
C:\xampp\htdocs\live-comment-appBash- この中に以下の3ファイルを保存
- index.html(すでに上で提供したHTMLをコピーして保存してください。)
- style.css(同じく、提供済みのCSSを保存します。)
main.js(提供済みのVueとSocket.IOのロジック入りJSファイルを保存。)
🎥 4. 動画ファイルを追加(仮のMP4ファイル)
任意の .mp4 ファイルを video.mp4 という名前でフォルダに入れてください。
C:\xampp\htdocs\live-comment-app\video.mp4Bash🌐 5. ブラウザで確認
XAMPPのApacheを起動し、以下のURLにアクセス:
http://localhost/live-comment-app/index.htmlBashこれで、次のようになっていれば成功です。
- 動画が表示される
- コメント入力欄と送信ボタンが表示される(まだリアルタイム通信は未接続)

Step 2:リアルタイム通信サーバーの構築(Node.js + Socket.IO)
手動でserver,jsを作成し、他のディレクトリ、ファイルをコマンドで作成します。
ディレクトリ構成(Node.js 側)
Vue側(C:\xampp\htdocs\live-comment-app)とは別に、Node.js のプロジェクトを別フォルダで管理します(例:live-comment-server)。
C:\live-comment-server
│ server.js
│ package.json
└─ node_modules/Bashlive-comment-serverは、C:\live-comment-server、直下ではなく別な場所でもいいです。
Node.jsプロジェクト初期化
ターミナルで以下を実行(PowerShell または コマンドプロンプト):
mkdir C:\live-comment-server
cd C:\live-comment-server
npm init -y
npm install express socket.io corsBashnpm init -y→package.json作成。
npm install express socket.io cors→node_modules/作成。
server.js の作成
// server.js
const express = require('express');
const http = require('http');
const socketIO = require('socket.io');
const cors = require('cors');
// サーバー設定
const app = express();
const server = http.createServer(app);
const io = socketIO(server, {
cors: {
origin: "http://localhost", // フロントエンドがホストされているオリジン
methods: ["GET", "POST"]
}
});
// CORS許可
app.use(cors());
// テスト用ルート
app.get('/', (req, res) => {
res.send('Socket.IO Server is running!');
});
// Socket.IO イベント
io.on('connection', (socket) => {
console.log('ユーザーが接続しました');
// コメント受信 → 全体に送信
socket.on('comment', (msg) => {
console.log('コメント:', msg);
io.emit('comment', msg); // 全クライアントにブロードキャスト
});
socket.on('disconnect', () => {
console.log('ユーザーが切断しました');
});
});
// ポート3000で起動
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Socket.IO サーバーがポート${PORT}で起動しました`);
console.log(`http://localhost:${PORT}`);
});JavaScriptサーバー起動
コマンドラインで以下を実行。
node server.jsBash✅
Socket.IO サーバーがポート3000で起動しましたと表示されればOK!
フロントエンドの修正(main.js)
const socket = io('http://127.0.0.1:3000', {
transports: ['websocket']
});
const { createApp } = Vue;
createApp({
data() {
return {
newComment: '',
comments: []
};
},
methods: {
addComment() {
if (this.newComment.trim()) {
const comment = {
id: Date.now(),
text: this.newComment.trim()
};
this.comments.unshift(comment); // 自分に即時反映
socket.emit('comment', comment); // 他のクライアントに送信
this.newComment = '';
}
}
},
mounted() {
socket.on('comment', (comment) => {
this.comments.unshift(comment); // 他のユーザーからのコメントを表示
if (this.comments.length > 20) {
this.comments.pop();
}
});
console.log('✅ Socket.IO クライアント接続済み');
}
}).mount('#app');JavaScriptthis.comments.unshift(comment);を削除すると、サーバーからのコメントのみ送信されます。
接続設定
const socket = io('http://127.0.0.1:3000', {
transports: ['websocket']
});JavaScript- Socket.IOサーバーにWebSocketで接続。
- ポート3000のNode.jsサーバーに接続。
Vueアプリの初期化
const { createApp } = Vue;JavaScript- Vue 3 の Composition APIを使用。
- #app をマウント対象にしてリアクティブなコメント機能を構築。
data
data() {
return {
newComment: '',
comments: []
};
}JavaScript- newComment: 入力中のコメント
- comments: 表示中のコメント一覧
addComment() メソッド
addComment() {
if (this.newComment.trim()) {
const comment = { id: Date.now(), text: this.newComment.trim() };
this.comments.unshift(comment); // 自分に即時反映
socket.emit('comment', comment); // 他クライアントに送信
this.newComment = '';
}
}JavaScript- 空でなければコメントを作成
- 即時反映
- Socket.IOで送信
- 入力欄をリセット
this.comments.unshift(comment);削除して、問題ないです。別ブラウザとのテストで試していただくと、コメントの挙動が分かりやすいかなと思います。
mounted() 内のSocket受信
socket.on('comment', (comment) => {
this.comments.unshift(comment); // 他ユーザーからのコメント受信
if (this.comments.length > 20) this.comments.pop();
});
JavaScript- 他のクライアントから送られたコメントを受信
- 最新20件だけを保持
全体としての流れ
- 入力 → 自分に即時反映
- Socket.IOで送信
- 他クライアントが受信 → 表示
テスト方法
- Apache(XAMPP)を起動 → http://localhost/live-comment-app/ にアクセス
- Nodeサーバーを起動中(node server.js)
- コメントを送信すると、即座に画面に表示される(Socket.IOでリアルタイム送信)
- ブラウザをもう1つ開いて同じURLにアクセス
- 片方でコメントすると、もう片方にもリアルタイムに表示される
[Step 3]: サーバー側(Node.js + Socket.IO + DB連携)
ABEMAのようなリアルタイムコメント付き配信サービスでは、コメントを**リアルタイム表示するだけで十分なのか?という疑問が出てきます。
結論から言えば、保存した方がよいケースが多いです。ABEMAも実際にコメントを保存しており、再放送や見逃し配信時に「当時のコメント」が再現される仕組みになっています。これはただの演出ではなく、以下のような利点があります。youtubeのリアルタイムコメントもアーカイブが残るので、過去の視聴は保存されたデータからコメントを取得します。
今回、(Node.js + Socket.IO + DB連携)DBでの連携ですが、まずは簡易的な所から、jsonとファイルでの保存のコードも記載しておきます!
jsonに保存
このコードでは、fs.readFileSync()で既存のcomments.jsonファイルを読み取り、JSON.parse()で配列として展開します。新しいコメント(msg)をオブジェクトとして配列にpush()し、currentDataというオブジェクトをJSON.stringifyで文字列に変換し、filePathというファイルに保存。その後fs.writeFileSync()でファイル全体を上書き保存します。
// server.js
const express = require('express');
const http = require('http');
const socketIO = require('socket.io');
const fs = require('fs');
const path = require('path');
const cors = require('cors');
// サーバー設定
const app = express();
const server = http.createServer(app);
const io = socketIO(server, {
cors: {
origin: "http://localhost", // フロントエンドがホストされているオリジン
methods: ["GET", "POST"]
}
});
// CORS許可
app.use(cors());
// テスト用ルート
app.get('/', (req, res) => {
res.send('Socket.IO Server is running!');
});
// Socket.IO イベント
io.on('connection', (socket) => {
console.log('ユーザーが接続しました');
// コメント受信 → 全体に送信
socket.on('comment', (msg) => {
// 受信データを保存用に整形
const comment = {
text: msg.text,
time: new Date().toISOString()
};
// 既存のJSONファイルを読み込んで追加
const filePath = path.join(__dirname, 'comments.json');
const currentData = fs.existsSync(filePath)
? JSON.parse(fs.readFileSync(filePath))
: [];
currentData.push(comment);
fs.writeFileSync(filePath, JSON.stringify(currentData, null, 2));
// ブロードキャスト
console.log('コメント:', msg);
io.emit('comment', msg); // 全クライアントにブロードキャスト
});
socket.on('disconnect', () => {
console.log('ユーザーが切断しました');
});
});
// ポート3000で起動
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Socket.IO サーバーがポート${PORT}で起動しました`);
console.log(`http://localhost:${PORT}`);
});JavaScriptテキストファイルに1行ずつ保存
こちらは、fs.appendFileSync()を使って、コメントを文字列として1行ずつファイル(例:comments.txt)に追記する方式です。文字列は[タイムスタンプ] コメント内容\nという形で整えられており、ログのように扱われます。
この形式は、処理が軽く、記録のためだけにコメントを保存したい場合や、後から人間が読むログとして使いたい場合に向いています。ただし、データに構造がないため、後からプログラムで処理する場合は、整形や解析が必要です。
// server.js
const express = require('express');
const http = require('http');
const socketIO = require('socket.io');
const fs = require('fs');
const path = require('path');
const cors = require('cors');
const LOG_FILE = path.join(__dirname, 'comments.txt');
// サーバー設定
const app = express();
const server = http.createServer(app);
const io = socketIO(server, {
cors: {
origin: "http://localhost", // フロントエンドがホストされているオリジン
methods: ["GET", "POST"]
}
});
// CORS許可
app.use(cors());
// テスト用ルート
app.get('/', (req, res) => {
res.send('Socket.IO Server is running!');
});
// Socket.IO イベント
io.on('connection', (socket) => {
console.log('ユーザーが接続しました');
// コメント受信 → 全体に送信
socket.on('comment', (msg) => {
console.log('コメント:', msg);
const line = `[${new Date().toISOString()}] ${msg.text}\n`;
fs.appendFileSync(LOG_FILE, line); // 追記
io.emit('comment', msg); // 全クライアントにブロードキャスト
});
socket.on('disconnect', () => {
console.log('ユーザーが切断しました');
});
});
// ポート3000で起動
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Socket.IO サーバーがポート${PORT}で起動しました`);
console.log(`http://localhost:${PORT}`);
});JavaScriptデータベースに保存
まずは、phpMyAdmin または MySQL を起動し、以下の SQL を実行してください。これにより、データベースの作成とテーブル(列)定義が一括で行われます。今回は XAMPP 上に構築された MySQL(MariaDB)サーバーを想定していますが、他の環境でも同様に利用可能です。
xamppをまだ構築していない方はこちらの記事もご参考下さいませ。
XAMPPのダウンロード、インストール。開発環境を構築する
XAMPP 初期設定と確認事項
CREATE DATABASE IF NOT EXISTS comment_app
CHARACTER SET utf8mb4
COLLATE utf8mb4_general_ci;
USE comment_app;
CREATE TABLE IF NOT EXISTS comments (
id INT AUTO_INCREMENT PRIMARY KEY,
text TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);SQLserver.js ファイルと同じ階層で、以下のコマンドを実行してください。
npm install mysql2Bashインストールが終わり、npm lsでパッケージの確認をすると4つのパッケージが確認できると思います。
D:\xampp\htdocs\live-comment-server>npm ls
live-comment-server@1.0.0 D:\xampp\htdocs\live-comment-server
├── cors@2.8.5
├── express@4.21.2
├── mysql2@3.14.0
└── socket.io@4.8.1Bash// MariaDB接続設定
const db = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '0000',
database: 'comment_app',
charset: 'utf8mb4'
});JavaScript下記のコードをごそっと、server.jsに張り付けます。userとpasswordの箇所は変更してください。
const express = require('express');
const http = require('http');
const socketIO = require('socket.io');
const mysql = require('mysql2');
const app = express();
const server = http.createServer(app);
const io = socketIO(server, {
cors: {
origin: 'http://localhost', // フロントのオリジン
methods: ['GET', 'POST']
}
});
// MariaDB接続設定
const db = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '0000',
database: 'comment_app',
charset: 'utf8mb4'
});
db.connect((err) => {
if (err) {
console.error('❌ DB接続エラー:', err);
} else {
console.log('✅ MariaDBに接続しました');
}
});
// Socket.IOコメント処理
io.on('connection', (socket) => {
console.log('🟢 ユーザー接続');
socket.on('comment', (msg) => {
// DBに保存
const sql = 'INSERT INTO comments (text) VALUES (?)';
db.query(sql, [msg.text], (err) => {
if (err) {
console.error('❌ コメント保存エラー:', err);
} else {
console.log('💾 コメント保存成功');
}
});
// 全クライアントにブロードキャスト
io.emit('comment', msg);
});
});
// ポート3000で起動
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Socket.IO サーバーがポート${PORT}で起動しました`);
console.log(`http://localhost:${PORT}`);
});JavaScript今回のステップ3では、コメントをデータベース(MariaDB)に保存する処理まで実装しましたが、保存したコメントを再び取得して画面に表示する機能は、実装していません。長くなることもあり今回の実装としてはここまでになります。コメントを保存しておくことには、いくつかの重要な意味があります。
- 荒らし行為や迷惑ユーザーの監視・対策に備えてログを残す
- NGワード検出や傾向分析などの機械的処理に活用
- アーカイブ再生時にコメントを再生するプレミアム機能への応用
など、将来的な機能拡張や運営管理の観点から、データの保存は有用なステップとなります。
実際の動画配信サービスでも、配信中のコメントは、番組単位でコメントがスレッド化、そして「リアルタイム通信+直近の履歴API」によって視聴中のすべてのユーザー間で共有されており、番組終了後には、保存されたコメントを元に再生用コメントとして利用されます。
データの保管場所
システム開発において、「データがどこに一時的または永続的に保存されているのか」を理解することは非常に重要です。ここでは、今回使用する各技術におけるデータの保管場所と性質について解説します。
Vueのデータ保管場所(ブラウザの一時メモリ)
Vueでは、data()やsetup()で定義された変数は、ユーザーのブラウザ上のメモリ(RAM)に一時的に保持されます。これはJavaScriptのオブジェクトとして管理され、ページの再読み込みやタブを閉じた時点で破棄されます。つまり、Vueのデータは「クライアント側の一時データ」です。
- ユーザー操作に応じて即時に画面表示を切り替えたいとき
- フォーム入力や一時的な状態管理(例:コメント入力欄)に使いたいとき
- ページ内だけで完結するUIの動的制御を行いたいとき
Socket.IOの一時データ(サーバーの一時メモリ)
Socket.IOを使用すると、クライアントとサーバー間でリアルタイムにデータを送受信できますが、Socket.IO自体はデータを保持しません。送受信されるデータはNode.jsのプロセス内、つまりサーバーのメモリ上で一時的に存在し、処理後は消えます。持続的に残すには、外部のデータベース等に保存する必要があります。
- 複数ユーザー間でリアルタイムにデータをやり取りしたいとき
- チャットやコメント、通知など即時に全体へ反映させる場面
- 瞬間的な通信イベントとして処理すればよいケース(永続化しない)
JSONデータ(オブジェクト形式 or 一時ストレージ)
JavaScriptにおけるJSONデータは、基本的にはオブジェクトとしてメモリ上に展開されます。保存の用途によって扱い方が異なり、クライアント側であればlocalStorageやsessionStorageなどに保存できます。サーバー側では、ファイルとして保存したり、DBに格納することが一般的です。
- 外部APIから取得したデータを一時的に扱いたいとき
- 設定ファイルやデータのテンプレートとして扱うとき
- クライアント側で軽量なデータを保持・再利用したいとき(localStorageなど)
テキストファイル(サーバーのファイルシステム)
Node.jsなどのサーバー環境では、テキストファイルはサーバーの**ファイルシステム(ストレージ)**に保存されます。たとえば、ログファイルや設定ファイル、簡易的なデータ保存などに使用されます。読み書きにはfsモジュールを使用し、保存された内容はサーバーのディスク上に残ります。
- 簡易ログ、ユーザー履歴、設定ファイルを扱うとき
- データベースを使うまでもない軽量な情報を保存したいとき
- ファイルベースでエクスポート・インポートしたいとき
MariaDB(データベースの永続保存)
MariaDBのようなRDBMSは、データをサーバーのストレージ上のデータベースファイルに永続的に保存します。テーブルごとにデータファイルが管理され、クエリを通して高速に検索・更新が可能です。コメントなどのユーザー投稿情報は、長期保存や履歴管理のためにここへ格納されます。
- ユーザーの投稿、コメント、アカウント情報などを長期保存したいとき
- 複数ユーザーで共有・検索・更新するデータを扱うとき
- データの整合性、検索性、正規化が重要な場合
データの取り扱い
機密情報を扱う場合は、その重要度に応じて適切な保管場所を選ぶことが大切です。Vueのdata()やブラウザのlocalStorage・sessionStorageは、画面上の一時的な操作や状態を管理するのに便利ですが、セキュリティ面では脆弱なため、漏れても問題のない情報に限定して使うのが基本です。
Socket.IOは、HTTPSやWSSと組み合わせることで安全な通信が可能で、コメントや通知などリアルタイム性が求められるデータに適しています。ただし、Socket.IO自体には保存機能がないため、長期的な保管にはデータベースとの連携が必要です。
MariaDBのようなサーバー側のデータベースは、機密情報を安全に管理するのに適しています。アクセス制御やSQLインジェクション対策、暗号化などを施すことで、高いセキュリティを実現できます。
なお、今回紹介しているコードはローカル環境を想定しており、セキュリティ対策(暗号化や認証処理など)は実装していません。本番環境で利用する際には、必ず必要な対策を講じるようにしてください。
ステップの振り返り
お疲れさまでした!ここまでのステップを通じ、Vue.js + Socket.IO + Node.js を使って、「リアルタイムコメント付き動画アプリ」を構築しました。
- Vue.js でスタイリッシュなUIを作成。コメント入力欄は右下に固定し、ABEMA風の見た目を再現。
- Socket.IO で複数のブラウザ間でコメントが同期。ライブ感ある体験が完成。
- リアルタイム表示に加えて、コメントを JSON・テキスト・DB に保存可能に。再利用や分析にも対応。
ここまでの構成ではPlayボタン付きの動画を使っていましたが、ABEMAでは再生はサービス側で管理され、視聴者が操作することはありません。本格的に再現する場合は、Playボタンを外し、自動再生に切り替えるのが理想です。今後の発展としては、ニコニコ動画のようにコメントが画面上を横に流れる演出、ユーザーごとのカラー名表示や通知機能や、動画の再生時間に合わせて過去のコメントを再表示する機能なども追加できます。また、NGワードの自動フィルタリングや、モデレーター向けのコメント管理ツールを導入すれば、安全性や運営性も高まります。


コメント