T-CREATOR

WebRTC で高精細 1080p/4K 画面共有:contentHint「detail」と DPI 最適化

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 には以下の値を設定できます。

#用途最適化の方向性
1motion動画・ゲーム配信フレームレート優先
2detail画面共有・プレゼン解像度・シャープネス優先
3textテキスト中心文字の可読性優先

画面共有では 「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 でのビットレート制御

高解像度で送信するには、十分なビットレートの確保が必要です。RTCPeerConnectionsetParameters() を使って、ビットレートを明示的に設定しましょう。

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');

ビットレートの目安は以下の通りです。

#解像度推奨ビットレート用途
1720p2〜3 Mbps標準的な画面共有
21080p5〜8 Mbps高品質な画面共有
34K15〜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}`);
      }
    }
  }
}

主なエラーと対処法を以下にまとめます。

#エラーコード原因解決方法
1NotAllowedErrorユーザーが共有を拒否権限の再要求、説明の追加
2NotFoundError共有可能な画面なし対応デバイスの確認
3NotReadableErrorキャプチャ失敗他アプリの終了、再試行
4OverconstrainedError制約が厳しすぎる解像度・フレームレートの緩和

品質モニタリング

送信中の品質を監視することで、問題の早期発見が可能です。

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 を取得して解像度を動的に計算することで、あらゆる環境で最適な画質を実現できます。

これらの手法を組み合わせることで、リモートワークでのコードレビューやデザインフィードバックが格段にやりやすくなります。ぜひ実際のプロジェクトに導入して、クリアな画面共有体験を提供してください。

関連リンク