Medley Developer Blog

株式会社メドレーのエンジニア・デザイナーによるブログです

オンライン診察機能に画面共有を実装した話

事業本部 プロダクト開発室エンジニアの日下です。

オンライン診療・服薬指導・クラウド診療支援システム「CLINICS」の、患者・医療機関に向けたアプリケーションの機能開発、開発基盤、インフラ周りを担当しております。

今回 CLINICS が提供するオンライン診療機能に「画面共有機能」を追加しましたので、その背景・技術的な話をまとめます。

画面共有機能実装の背景

CLINICS とオンライン診療

普段皆さんが病院にかかるとき、多くの場合は病院に行き、医師の診察を対面で受け、会計をして帰るといった流れになるかと思います。

CLINICS のオンライン診療はこの流れをインターネットを通して提供するサービスです。

※ オンライン診療は、一度、初診等で対面診療を受けた際に医師が可能と判断した場合、次回以降の診察において可能になります。また、現在は新型コロナウイルス感染症対策時限措置として、初診からオンライン診療を受けることが可能となっています。

CLINICS を利用した場合、事前に予約した時間にスマホまたは PC で待機をする、医師の診察をビデオチャットで受け、会計はクレジットカードで行われるという流れとなっています。

クラウド診療支援システムとしての CLINICS は 2016 年に「オンライン診療のためのシステム」としてローンチされ、2018 年にはクラウド型電子カルテ機能を2019 年には予約管理システム機能を2020 年にはかかりつけ薬局支援システム Pharms との連携機能も追加し、患者向けアプリからオンライン服薬指導をシームレスに受けることができるようになりました。

プロダクト開発室ではこれらオンライン診療機能・電子カルテ機能・予約管理機能・連携機能の改善を日々行っています。

画面共有機能の需要の高まり、実装の決定

このように CLINICS の改善を日々行なっている中、昨年から始まった新型コロナウイルス感染症(COVID-19)の流行に伴った需要の増加により、オンライン診療の件数が急増しました。

CLINICS も数多くの医療機関にご利用いただく中で、オンライン診療に関わるさまざまなご要望をいただくようになりました。その中でも特に多かったものが、今回紹介する画面共有機能です。

対面での診察の際に医師が検査結果などを患者に見せながら説明するように、オンラインで診察する場合でも資料をリアルタイムで共有しながら説明ができるようになれば、今まで以上にオンラインでも質の高い診察を行えるようになります。

こういったユースケース、要望などを検討した結果、CLINICS を利用するすべての医療機関及び患者にとって大きな恩恵が見込まれたため、オンライン診察(ビデオチャット)中に医師の PC 画面をリアルタイムで患者に共有する機能として実装をすることにしました。

f:id:medley_inc:20210729115806p:plain

画面共有機能の実装

画面共有をする医師側向けのコードでどういった実装方法があるのか、大まかな流れをまとめます。

※ 以下に記載しているコードは説明のための疑似コードですので、このままでは動作しないことにご注意ください。また、医師側の実装例を掲載しているため、患者側(画面共有を受ける側)の実装は別途必要になります。

オンライン診察開始までの処理

オンライン診察を開始するには医師側のマイクとカメラで取得した情報を患者側に送付する必要があります。ここではそこまでの実装の流れを見ていきます。

カメラ・マイクのストリームの取得

オンライン診察開始時点で医師側のマイク・カメラの情報を共有するため、まずはそれらのストリームを取得する必要があります。こういったメディアコンテンツのストリームを司るインターフェイスとして MediaStream が定義されています。

マイク・カメラの MediaStream は、例えば MediaDevices.getUserMedia() を利用して取得できます。

const userMediaStream: MediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });

SkyWay 経由でオンライン診察を始める

WebRTC で P2Pビデオチャットを利用するためには、初期の接続のための処理及び接続の維持などの処理を行う必要があります。弊社ではこのあたりの処理を WebRTC SaaSSkyWay 及びその SDK を利用することで簡略化しています。

オンライン診察開始時には、先程取得した医師側のマイク・カメラの MediaStream を SkyWay の SDK に渡すことで、一対一でのリアルタイムビデオチャットを実現できます。

import Peer, { MediaConnection } from "skyway-js";

const peer = new Peer({ key: "your-api-key" });

// 事前に患者と共有しておいたpeer idに対してcallメソッドとMediaStreamを渡すことで診察を開始できる。
const mediaConnection: MediaConnection = peer.call("shared-peer-id", userMediaStream);

// 注: 患者側は送付された処理をハンドリングする機能を実装する必要がある

ここまでがオンライン診察を開始するまでの処理です。

※ 詳細は SkyWay 公式のチュートリアルなどを参照ください。

画面共有の処理

ここまでで患者に対して医師側のカメラ・マイクで取得された映像・音声が表示されている状態のため、これを切り替える処理が必要になります。今回は現在接続に利用している MediaStream を、画面共有用の MediaStream に入れ替えることで実現しました。

画面の MediaStream の取得

まずは共有する画面の MediaStream を取得する必要があります。これは MediaDevices.getDisplayMedia() を使うことで実現できます。

const displayStream: MediaStream = await navigator.mediaDevices.getDisplayMedia({ video: true });

画面共有用の MediaStream を作る

getDisplayMedia() から共有する画面の MediaStream を取得できるものの、そのまま利用するとマイクの音声が入りません。

これは getDisplayMedia() から取れる MediaStream にマイクの音声が含まれていないことが原因なので、必要な画像・音声の組み合わせを持った画面共有用の MediaStream を作成することで対処ができます。

MediaStreamTrack を組みあわせて画面共有用の MediaStream を作る

画面共有用の MediaStream を作成する前にまず、MediaStreamTrack と MediaStream の関係を理解する必要があります。

MediaStreamTrack はストリームに含まれる一つのメディアトラックを表現するものです。 kind という読み取り専用プロパティがあり、オーディオトラックであれば "audio" が、ビデオトラックであれば "video" が設定されています。

また、 MediaStream は複数の MediaStreamTrack から成り、オーディオトラック・ビデオトラックを取り出すメソッドがそれぞれ MediaStream.getAudioTracks()MediaStream.getVideoTracks() として実装されています。

これらを組み合わせることで、マイクと画面の MediaStreamTrack を持つ MediaStream を作ることができ、これを SkyWay の SDK に渡すことで、画面共有を実現できます。

const [displayVideoTrack]: MediaStreamTrack[] = displayStream.getVideoTracks();
// 画面共有の音声はマイクの音声を利用したいので、userMediaStreamからaudioTrackを取り出しておく
const [userAudioTrack]: MediaStreamTrack[] = userMediaStream.getAudioTracks();

// 画面共有するためのMediaStreamを作成する(画面のvideoTrack、マイクのaudioTrackを持つMediaStreamを作る)
const sharingMediaStream: MediaStream = new MediaStream([displayVideoTrack, userAudioTrack]);

MediaStream の入れ替え

最後に画面共有状態への切り替えです。マイク・カメラが共有されている状態からの切り替えにはいくつかの方法が考えられます。

例えば、多重化であれば MediaConnection( Skyway の SDK の単位で、「接続先 Peer へのメディアチャネル接続」を管理する)の多重化、MediaStream の多重化、MediaStreamTrack の多重化がそれぞれ考えられます。これらの方法はマイク・カメラの切り替え時のチラつき抑制など実装上の選択肢が増えるメリットがある一方で、通信量が多くなってしまう点がデメリットと言えます。

今回は多重化をせずに既存の MediaStream を切り替える実装を紹介します。この方法のメリットは、多重化に比べると通信量が少なく、またすでに MediaStream が一つである前提で作られている場合は、画面共有を受ける側の実装の変更が不要という点です。

この方法は、 SkyWay の SDK であれば MediaConnection の replaceStream というメソッドに対して新しい MediaStream を渡すことで実現ができます。

// 画面共有用のMediaStreamを渡すことで、画面共有を開始する
// MediaConnection は先程 `peer.call` した際の返り値として取れているため、それを利用する
mediaConnection.replaceStream(sharingMediaStream);

実装前に懸念していたマイク・カメラの切り替え時のチラつきなども気になるほどはなく、実用に足るような品質を保つことができることを確認しています。

実装の全体概要

以上の流れを実装すると、次のようなコードになります。

import Peer, { MediaConnection } from "skyway-js";

/** 医師側のマイク・カメラを共有してオンライン診察開始するところまで **/
// getUserMedia()でカメラ・マイクのストリームを取得
const userMediaStream: MediaStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });

// Skyway sdkの初期化処理
const peer = new Peer({ key: "your-api-key" });

// オンライン診察の開始
const mediaConnection: MediaConnection = peer.call("peerId", userMediaStream);

/** 画面共有を開始する処理 **/
// 画面共有する画面のstreamを取る
const displayStream: MediaStream = await navigator.mediaDevices.getDisplayMedia({ video: true });
const [displayVideoTrack]: MediaStreamTrack[] = displayStream.getVideoTracks();

// 画面共有の音声はマイクの音声を利用したいので、userMediaStreamからaudioTrackを取り出しておく
const [userAudioTrack]: MediaStreamTrack[] = userMediaStream.getAudioTracks();

// 画面共有するためのMediaStreamを作成する(画面のvideoTrack、マイクのaudioTrackを持つMediaStreamを作る)
const sharingStream: MediaStream = new MediaStream([displayVideoTrack, userAudioTrack]);

// 画面共有用のMediaStreamを渡すことで、画面共有を開始する
mediaConnection.replaceStream(sharingStream);

開発中に遭遇した問題への対応

スリープモード・共有を停止ボタンを押したときの対応

Google Chrome で画面共有の際に表示される「共有を停止」ボタンを押下したり、PC をスリープモードにすると、画面の MediaStreamTrack が途切れてしまいます。

これは該当の MediaStreamTrack に "ended" のイベントリスナを登録しておくことでハンドリングできます。

displayVideoTrack.addEventListener("ended", handleEndedEvent, { once: true });

TypeScript の型の対応

現状 TypeScript の型が getDisplayMedia() に対応していなかったため、今回は実装の参考にしている skyway-conf で利用されている型を流用する形で対応をしました。

declare global {
    interface MediaDevices {
        getDisplayMedia(constraints: MediaStreamConstraints): Promise<MediaStream>;
    }
}

これは根本的には TypeScript の dom.d.ts に型定義が入っていないことが起因していますが、TypeScript4.4 で対応がされるようです

まとめ

昨今の状況により、オンライン診察のニーズが高まり、画面共有機能の重要性が高まりました。 診察中の画面共有機能は以下の api を組み合わせることで実現することができます。

  • PC 画面の MediaStream は getDisplayMedia() を使うことで取得
  • MediaStream に含める音声・画像ストリームを変更したい場合は MediaStreamTrack の組み合わせを変えることで作成
  • 接続中の MediaStream の変更は SkyWay の SDKMediaConnection.replaceStream() を使う

最後に

CLINICS では本稿で紹介した画面共有などの新規機能の導入や日々の改善を通じて、医療機関・患者双方に支持されるプロダクトを目指し開発を行っています。興味を持たれたエンジニアの方がいらっしゃいましたらぜひこちらにご連絡いただければと思います。

www.medley.jp