WebRTC で高精細 1080p/4K 画面共有:contentHint「detail」と DPI 最適化
Web 会議やリモートワークが一般化した現在、画面共有の品質は業務効率を大きく左右します。特にデザイナーやエンジニアが細かいコードレビューを行う際、ぼやけた画面では文字が読めず、何度も「もう少しズームしてもらえますか?」と依頼することになりますよね。
本記事では、WebRTC を使った画面共有で1080p や 4K といった高精細映像を実現するための具体的な手法をご紹介します。contentHint プロパティの「detail」設定と DPI 最適化を組み合わせることで、テキストや UI が鮮明に映る画面共有を実装できるでしょう。
背景
WebRTC における画面共有の仕組み
WebRTC は、ブラウザ間でリアルタイムに音声・映像・データをやり取りするための技術です。getDisplayMedia() API を使うことで、ユーザーの画面、ウィンドウ、タブのいずれかをキャプチャし、相手に送信できます。
javascript// 画面共有の基本的な取得方法
const stream = await navigator.mediaDevices.getDisplayMedia(
{
video: true,
audio: false,
}
);
この基本的な実装では、ブラウザがデフォルトの解像度とビットレートを選択します。多くの場合、720p 程度の解像度に制限され、文字が小さい画面では読みにくくなってしまうのです。
画面共有で求められる品質
一般的な動画配信とは異なり、画面共有では以下のような特性が求められます。
- 高精細なテキスト表示:コードエディタやドキュメントの文字が鮮明に見える
- シャープな UI 要素:ボタンやアイコンの輪郭がはっきりしている
- 色の正確性:デザインツールでの色確認が正確にできる
- 動きの滑らかさ:マウスカーソルの動きが追従できる
以下の図は、WebRTC における画面共有の基本的なデータフローを示しています。
mermaidflowchart TB
user["ユーザー"] -->|画面選択| gdm["getDisplayMedia()"]
gdm -->|MediaStream| track["VideoTrack"]
track -->|エンコード| encoder["VP8/VP9/H.264<br/>エンコーダー"]
encoder -->|RTP| network["ネットワーク送信"]
network -->|受信| decoder["デコーダー"]
decoder -->|表示| remote["リモート参加者"]
このフローの中で、エンコーダーの設定が画質を大きく左右します。
課題
デフォルト設定の限界
WebRTC のデフォルト設定では、以下のような課題があります。
解像度の自動調整
ネットワーク帯域に応じて、ブラウザが自動的に解像度を下げてしまいます。4K モニターで作業している内容を共有しても、相手には 720p 程度に圧縮された映像しか届きません。
typescript// デフォルトでは解像度が制限される
const constraints = {
video: true, // 詳細な指定がない
};
ビットレートの不足
高解像度で送信しても、ビットレートが不足していると、ブロックノイズや文字のにじみが発生します。特に画面全体を共有する場合、情報量が多いため十分なビットレートが必要です。
エンコーダーの最適化不足
WebRTC のエンコーダーは、デフォルトでは動画配信を想定した設定になっています。画面共有のような静止画中心のコンテンツには最適化されていないため、以下の問題が起こります。
| # | 問題 | 影響 |
|---|---|---|
| 1 | フレームレート優先 | 解像度が犠牲になる |
| 2 | 動き検出の誤作動 | 静止画面でもビットレートを消費 |
| 3 | テキストへの最適化不足 | 文字がぼやける |
高 DPI 環境での表示問題
macOS の Retina ディスプレイや Windows の高 DPI 設定では、物理ピクセルと論理ピクセルが異なります。
javascript// DPI情報の取得例
const dpr = window.devicePixelRatio; // Retinaでは2.0、4Kでは3.0以上
console.log(`Device Pixel Ratio: ${dpr}`);
デバイスピクセル比が 2.0 の場合、1920x1080 の論理解像度は実際には 3840x2160 の物理ピクセルで表示されています。この情報を考慮せずに画面共有すると、本来の鮮明さが失われてしまうのです。
以下の図は、DPI 設定が画質に与える影響を示しています。
mermaidflowchart LR
screen["高DPIディスプレイ<br/>3840x2160物理"] -->|DPR=2.0| logical["論理解像度<br/>1920x1080"]
logical -->|デフォルト| cap1["キャプチャ<br/>1280x720"]
logical -->|最適化| cap2["キャプチャ<br/>1920x1080以上"]
cap1 -->|圧縮| blur["ぼやけた映像"]
cap2 -->|高ビットレート| sharp["鮮明な映像"]
高 DPI 環境では、論理解像度ではなく物理解像度を考慮した設定が必要になります。
解決策
contentHint「detail」の活用
WebRTC の MediaStreamTrack には、contentHint プロパティがあります。このプロパティを 「detail」 に設定することで、エンコーダーに対して「この映像は細部が重要です」と伝えられます。
typescript// contentHintの設定
const stream = await navigator.mediaDevices.getDisplayMedia(
{
video: true,
}
);
const videoTrack = stream.getVideoTracks()[0];
typescript// エンコーダーへのヒント設定
if ('contentHint' in videoTrack) {
videoTrack.contentHint = 'detail';
console.log('contentHint設定完了: detail');
}
contentHint には以下の値を設定できます。
| # | 値 | 用途 | 最適化の方向性 |
|---|---|---|---|
| 1 | motion | 動画・ゲーム配信 | フレームレート優先 |
| 2 | detail | 画面共有・プレゼン | 解像度・シャープネス優先 |
| 3 | text | テキスト中心 | 文字の可読性優先 |
画面共有では 「detail」または「text」 が適切です。これにより、エンコーダーは動きの滑らかさよりも静止画の品質を優先するようになります。
高解像度の制約設定
getDisplayMedia() には、解像度の制約を指定できます。
typescript// 1080p の制約設定
const hdConstraints = {
video: {
width: { ideal: 1920 },
height: { ideal: 1080 },
frameRate: { ideal: 30, max: 30 },
},
audio: false,
};
typescript// 4K の制約設定
const uhd Constraints = {
video: {
width: { ideal: 3840 },
height: { ideal: 2160 },
frameRate: { ideal: 30, max: 30 }
},
audio: false
};
typescript// 制約を適用して画面共有を開始
const stream = await navigator.mediaDevices.getDisplayMedia(
uhdConstraints
);
ideal を使うことで、可能な限り指定された解像度に近づけます。ネットワーク状況が厳しい場合は自動的に下がりますが、min を指定することで最低解像度を保証することも可能です。
RTCPeerConnection でのビットレート制御
高解像度で送信するには、十分なビットレートの確保が必要です。RTCPeerConnection の setParameters() を使って、ビットレートを明示的に設定しましょう。
typescript// PeerConnectionの作成
const peerConnection = new RTCPeerConnection(config);
// VideoTrackをSenderに追加
const sender = peerConnection.addTrack(videoTrack, stream);
typescript// エンコーディングパラメータの取得と設定
const parameters = sender.getParameters();
if (!parameters.encodings) {
parameters.encodings = [{}];
}
typescript// 1080p用のビットレート設定(5Mbps)
parameters.encodings[0].maxBitrate = 5000000; // 5Mbps
parameters.encodings[0].scaleResolutionDownBy = 1.0; // ダウンスケールしない
await sender.setParameters(parameters);
console.log('ビットレート設定完了: 5Mbps');
ビットレートの目安は以下の通りです。
| # | 解像度 | 推奨ビットレート | 用途 |
|---|---|---|---|
| 1 | 720p | 2〜3 Mbps | 標準的な画面共有 |
| 2 | 1080p | 5〜8 Mbps | 高品質な画面共有 |
| 3 | 4K | 15〜25 Mbps | 超高精細な画面共有 |
ネットワーク帯域が十分にあることを確認してから、高ビットレートを設定してください。
DPI を考慮した解像度調整
高 DPI 環境では、devicePixelRatio を考慮した解像度設定が重要です。
typescript// デバイスピクセル比の取得
const dpr = window.devicePixelRatio;
console.log(`Device Pixel Ratio: ${dpr}`);
typescript// DPRを考慮した制約の計算
const baseWidth = 1920;
const baseHeight = 1080;
const constraints = {
video: {
width: { ideal: Math.floor(baseWidth * dpr) },
height: { ideal: Math.floor(baseHeight * dpr) },
frameRate: { ideal: 30, max: 30 },
},
};
typescript// 最大値の設定(4Kを超えないように)
const maxWidth = 3840;
const maxHeight = 2160;
constraints.video.width.ideal = Math.min(
constraints.video.width.ideal,
maxWidth
);
constraints.video.height.ideal = Math.min(
constraints.video.height.ideal,
maxHeight
);
この設定により、Retina ディスプレイ(DPR=2.0)では 1920x1080 の設定が自動的に 3840x2160 相当になり、物理ピクセルに合わせた高精細なキャプチャが可能になります。
以下の図は、最適化された画面共有の設定フローを示しています。
mermaidflowchart TD
start["画面共有開始"] --> dpr["DPR取得"]
dpr --> calc["解像度計算<br/>base × DPR"]
calc --> limit["上限チェック<br/>max 4K"]
limit --> constraint["制約オブジェクト生成"]
constraint --> gdm["getDisplayMedia()"]
gdm --> track["VideoTrack取得"]
track --> hint["contentHint = detail"]
hint --> peer["PeerConnectionに追加"]
peer --> bitrate["ビットレート設定"]
bitrate --> send["高精細送信開始"]
具体例
実践的な実装例
ここでは、1080p 画面共有を実現する完全な実装例をご紹介します。
初期設定とユーティリティ関数
typescript// 型定義
interface ScreenShareConfig {
targetWidth: number;
targetHeight: number;
maxBitrate: number;
frameRate: number;
}
typescript// DPRを考慮した制約を生成する関数
function createDisplayConstraints(
config: ScreenShareConfig
): MediaStreamConstraints {
const dpr = window.devicePixelRatio;
// DPRを考慮した解像度計算
const idealWidth = Math.min(
Math.floor(config.targetWidth * dpr),
3840 // 4Kを上限とする
);
const idealHeight = Math.min(
Math.floor(config.targetHeight * dpr),
2160 // 4Kを上限とする
);
return {
video: {
width: { ideal: idealWidth },
height: { ideal: idealHeight },
frameRate: {
ideal: config.frameRate,
max: config.frameRate,
},
},
audio: false,
};
}
画面共有の開始処理
typescript// 高精細画面共有を開始する関数
async function startHighQualityScreenShare(
config: ScreenShareConfig
): Promise<MediaStream> {
try {
// 制約の生成
const constraints = createDisplayConstraints(config);
console.log('制約:', constraints);
// 画面共有の取得
const stream =
await navigator.mediaDevices.getDisplayMedia(
constraints
);
return stream;
} catch (error) {
console.error('Error: 画面共有の取得に失敗', error);
throw error;
}
}
typescript// contentHintの設定
function applyContentHint(stream: MediaStream): void {
const videoTrack = stream.getVideoTracks()[0];
if (!videoTrack) {
throw new Error('Error: VideoTrackが見つかりません');
}
// contentHintの対応チェック
if ('contentHint' in videoTrack) {
videoTrack.contentHint = 'detail';
console.log('contentHint設定: detail');
} else {
console.warn(
'Warning: このブラウザはcontentHintに対応していません'
);
}
// 設定の確認
const settings = videoTrack.getSettings();
console.log(
'実際の解像度:',
settings.width,
'x',
settings.height
);
console.log('フレームレート:', settings.frameRate);
}
PeerConnection への統合
typescript// PeerConnectionにトラックを追加し、ビットレートを設定
async function addTrackWithBitrate(
peerConnection: RTCPeerConnection,
stream: MediaStream,
maxBitrate: number
): Promise<RTCRtpSender> {
const videoTrack = stream.getVideoTracks()[0];
// トラックを追加
const sender = peerConnection.addTrack(
videoTrack,
stream
);
// エンコーディングパラメータの設定
const parameters = sender.getParameters();
if (
!parameters.encodings ||
parameters.encodings.length === 0
) {
parameters.encodings = [{}];
}
return sender;
}
typescript// ビットレートとスケーリングの設定
async function configureEncoding(
sender: RTCRtpSender,
maxBitrate: number
): Promise<void> {
const parameters = sender.getParameters();
// ビットレートの設定
parameters.encodings[0].maxBitrate = maxBitrate;
// 解像度のダウンスケールを無効化
parameters.encodings[0].scaleResolutionDownBy = 1.0;
// 設定を適用
await sender.setParameters(parameters);
console.log(
`ビットレート設定完了: ${maxBitrate / 1000000}Mbps`
);
}
メイン処理の実装
typescript// すべてを統合したメイン関数
async function setupHighQualityScreenShare(): Promise<void> {
// 1080p、5Mbpsの設定
const config: ScreenShareConfig = {
targetWidth: 1920,
targetHeight: 1080,
maxBitrate: 5000000, // 5Mbps
frameRate: 30,
};
try {
// ステップ1: 画面共有の取得
console.log('画面共有を開始します...');
const stream = await startHighQualityScreenShare(
config
);
// ステップ2: contentHintの適用
applyContentHint(stream);
// 次のステップに続く...
} catch (error) {
console.error(
'Error: セットアップに失敗しました',
error
);
throw error;
}
}
typescript// PeerConnection設定(続き)
async function setupHighQualityScreenShare(): Promise<void> {
const config: ScreenShareConfig = {
targetWidth: 1920,
targetHeight: 1080,
maxBitrate: 5000000,
frameRate: 30,
};
const stream = await startHighQualityScreenShare(config);
applyContentHint(stream);
// ステップ3: PeerConnectionの作成
const peerConnection = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});
// ステップ4: トラックの追加とビットレート設定
const sender = await addTrackWithBitrate(
peerConnection,
stream,
config.maxBitrate
);
await configureEncoding(sender, config.maxBitrate);
console.log('高精細画面共有のセットアップが完了しました');
}
エラーハンドリング
実際の運用では、さまざまなエラーに対応する必要があります。
typescript// エラーハンドリングを含む実装
async function setupWithErrorHandling(): Promise<void> {
try {
await setupHighQualityScreenShare();
} catch (error) {
// エラーの種類に応じた処理
if (error instanceof Error) {
if (error.name === 'NotAllowedError') {
console.error(
'Error NotAllowedError: ユーザーが画面共有を拒否しました'
);
alert('画面共有が許可されていません');
} else if (error.name === 'NotFoundError') {
console.error(
'Error NotFoundError: 共有可能な画面が見つかりません'
);
alert('共有できる画面がありません');
} else if (error.name === 'NotReadableError') {
console.error(
'Error NotReadableError: 画面のキャプチャに失敗しました'
);
alert(
'画面のキャプチャに失敗しました。他のアプリを閉じてください。'
);
} else {
console.error('Error:', error.message);
alert(`エラーが発生しました: ${error.message}`);
}
}
}
}
主なエラーと対処法を以下にまとめます。
| # | エラーコード | 原因 | 解決方法 |
|---|---|---|---|
| 1 | NotAllowedError | ユーザーが共有を拒否 | 権限の再要求、説明の追加 |
| 2 | NotFoundError | 共有可能な画面なし | 対応デバイスの確認 |
| 3 | NotReadableError | キャプチャ失敗 | 他アプリの終了、再試行 |
| 4 | OverconstrainedError | 制約が厳しすぎる | 解像度・フレームレートの緩和 |
品質モニタリング
送信中の品質を監視することで、問題の早期発見が可能です。
typescript// 統計情報の取得と監視
async function monitorQuality(
peerConnection: RTCPeerConnection
): Promise<void> {
const sender = peerConnection.getSenders()[0];
if (!sender) {
console.warn('Warning: Senderが見つかりません');
return;
}
// 統計情報の取得
const stats = await sender.getStats();
stats.forEach((report) => {
if (
report.type === 'outbound-rtp' &&
report.kind === 'video'
) {
console.log('送信統計:', {
解像度: `${report.frameWidth}x${report.frameHeight}`,
フレームレート: report.framesPerSecond,
ビットレート: `${Math.round(
(report.bytesSent * 8) / report.timestamp / 1000
)}kbps`,
送信フレーム数: report.framesSent,
});
}
});
}
typescript// 定期的な監視の設定
function startQualityMonitoring(
peerConnection: RTCPeerConnection,
intervalMs: number = 5000
): number {
return window.setInterval(async () => {
await monitorQuality(peerConnection);
}, intervalMs);
}
// 使用例
const monitoringId = startQualityMonitoring(peerConnection);
// 停止する場合
// clearInterval(monitoringId);
以下の図は、品質監視のフローを示しています。
mermaidsequenceDiagram
participant App as アプリケーション
participant PC as PeerConnection
participant Sender as RTCRtpSender
participant Stats as 統計情報
App->>PC: getSenders()
PC->>App: RTCRtpSender[]
App->>Sender: getStats()
Sender->>Stats: 統計収集
Stats->>App: RTCStatsReport
App->>App: 解析・ログ出力
Note over App: 5秒ごとに繰り返し
React での実装例
実際のアプリケーションでは、React などのフレームワークと組み合わせることが多いでしょう。
typescript// Reactカスタムフックの実装
import { useRef, useCallback, useEffect } from 'react';
interface UseScreenShareReturn {
startShare: () => Promise<void>;
stopShare: () => void;
isSharing: boolean;
localStream: MediaStream | null;
}
typescript// カスタムフックの定義
function useHighQualityScreenShare(
config: ScreenShareConfig
): UseScreenShareReturn {
const streamRef = useRef<MediaStream | null>(null);
const [isSharing, setIsSharing] = useState(false);
// 画面共有開始
const startShare = useCallback(async () => {
try {
const stream = await startHighQualityScreenShare(
config
);
applyContentHint(stream);
streamRef.current = stream;
setIsSharing(true);
// 共有終了時のイベントリスナー
const videoTrack = stream.getVideoTracks()[0];
videoTrack.addEventListener('ended', () => {
stopShare();
});
} catch (error) {
console.error('Error: 画面共有の開始に失敗', error);
throw error;
}
}, [config]);
return {
startShare,
stopShare,
isSharing,
localStream: streamRef.current,
};
}
typescript// 画面共有停止
const stopShare = useCallback(() => {
if (streamRef.current) {
streamRef.current
.getTracks()
.forEach((track) => track.stop());
streamRef.current = null;
setIsSharing(false);
console.log('画面共有を停止しました');
}
}, []);
typescript// コンポーネントでの使用例
function ScreenShareComponent() {
const config: ScreenShareConfig = {
targetWidth: 1920,
targetHeight: 1080,
maxBitrate: 5000000,
frameRate: 30,
};
const { startShare, stopShare, isSharing, localStream } =
useHighQualityScreenShare(config);
return (
<div>
<button onClick={startShare} disabled={isSharing}>
画面共有開始
</button>
<button onClick={stopShare} disabled={!isSharing}>
画面共有停止
</button>
{isSharing && <p>画面を共有中です</p>}
</div>
);
}
4K 画面共有への対応
さらに高精細な 4K 画面共有も、同じ手法で実現できます。
typescript// 4K設定の定義
const uhd Config: ScreenShareConfig = {
targetWidth: 3840,
targetHeight: 2160,
maxBitrate: 20000000, // 20Mbps
frameRate: 30
};
typescript// ネットワーク帯域の事前チェック
async function checkNetworkBandwidth(): Promise<boolean> {
// Network Information APIを使用(対応ブラウザのみ)
if ('connection' in navigator) {
const connection = (navigator as any).connection;
const downlink = connection.downlink; // Mbps
console.log(`推定ダウンリンク速度: ${downlink}Mbps`);
// 4Kには25Mbps以上を推奨
if (downlink < 25) {
console.warn(
'Warning: 帯域が不足している可能性があります'
);
return false;
}
}
return true;
}
typescript// 4K画面共有の開始
async function start4KScreenShare(): Promise<void> {
// 帯域チェック
const hasEnoughBandwidth = await checkNetworkBandwidth();
if (!hasEnoughBandwidth) {
const proceed = confirm(
'ネットワーク帯域が不足している可能性があります。続行しますか?'
);
if (!proceed) return;
}
// 4K設定で画面共有を開始
const stream = await startHighQualityScreenShare(
uhdConfig
);
applyContentHint(stream);
console.log('4K画面共有を開始しました');
}
4K 画面共有を安定して配信するには、以下の環境が必要です。
- アップロード帯域: 25Mbps 以上
- CPU 性能: エンコードに十分な処理能力
- 有線 LAN: Wi-Fi よりも安定した接続を推奨
まとめ
WebRTC で高精細な 1080p/4K 画面共有を実現するためには、以下の 3 つのポイントが重要です。
contentHint の適切な設定によって、エンコーダーに画面共有の特性を伝えられます。「detail」または「text」を指定することで、静止画の品質を優先したエンコーディングが行われ、テキストや UI が鮮明に表示されるようになりますね。
高解像度とビットレートの明示的な指定も欠かせません。getDisplayMedia()の制約パラメータで 1080p や 4K を指定し、RTCPeerConnection の setParameters()で十分なビットレートを確保することで、ネットワーク帯域を最大限に活用できます。
DPI を考慮した解像度調整により、Retina ディスプレイや Windows の高 DPI 環境でも物理ピクセルに合わせた高精細なキャプチャが可能になるでしょう。devicePixelRatio を取得して解像度を動的に計算することで、あらゆる環境で最適な画質を実現できます。
これらの手法を組み合わせることで、リモートワークでのコードレビューやデザインフィードバックが格段にやりやすくなります。ぜひ実際のプロジェクトに導入して、クリアな画面共有体験を提供してください。
関連リンク
articleWebRTC で高精細 1080p/4K 画面共有:contentHint「detail」と DPI 最適化
articleWebRTC Simulcast 設計ベストプラクティス:レイヤ数・ターゲットビットレート・切替条件
articleWebRTC SDP 用語チートシート:m=・a=・bundle・rtcp-mux を 10 分で総復習
articleWebRTC で自前 TURN サーバー構築:coturn を TLS/443 で堅牢運用する方法
articleWebRTC 最新動向 2025:AV1・SVC・WHIP/WHEP が変えるリアルタイム配信設計
articleWebRTC 本番運用の SLO 設計:接続成功率・初画出し時間・通話継続率の基準値
articleWebSocket Close コード早見表:正常終了・プロトコル違反・ポリシー違反の実務対応
articleStorybook 品質ゲート運用:Lighthouse/A11y/ビジュアル差分を PR で自動承認
articleWebRTC で高精細 1080p/4K 画面共有:contentHint「detail」と DPI 最適化
articleSolidJS フォーム設計の最適解:コントロール vs アンコントロールドの棲み分け
articleWebLLM 使い方入門:チャット UI を 100 行で実装するハンズオン
articleShell Script と Ansible/Make/Taskfile の比較:小規模自動化の最適解を検証
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来