diff --git a/src/dispose_listener.h b/src/dispose_listener.h index 4b6780d2..91eb0c2e 100644 --- a/src/dispose_listener.h +++ b/src/dispose_listener.h @@ -3,16 +3,48 @@ #include +/** + * 実装上の留意点:Sora Python SDK は Sora, AudioSource, VideoSource, Connection, Track など、 + * それぞれを Python で別々で扱うことができるようになっているが実態としては親を破棄すると子が止まる関係性が存在する。 + * これを適切にハンドリングしなければリークを引き起こしてしまうため、破棄された、もしくはされることを通知して、 + * 適切にハンドリングを行うためのクラス DisposeSubscriber, DisposePublisher を用意した。 + */ + +/** + * 破棄された通知を受ける DisposeSubscriber です。 + * + * これを継承することで、 DisposePublisher から破棄された通知を受け取ることができます。 + */ class DisposeSubscriber { public: + /** + * Subscribe している Publisher が破棄された際に呼び出される関数です。 + */ virtual void PublisherDisposed() = 0; }; +/** + * 破棄された際に DisposeSubscriber に通知を送る DisposePublisher です。 + * + * 継承して使うことを想定しています。 1 つのインスタンスで複数ので DisposePublisher に破棄を通知することができます。 + */ class DisposePublisher { public: + // クラスによって、 Disposed を呼ぶタイミングを調整する必要があるためデストラクタでの一律 Disposed 呼び出しは行わない + + /** + * Subscribe する際に呼ぶ関数です。 + * + * @param subscriber Subscribe する DisposeSubscriber + */ void AddSubscriber(DisposeSubscriber* subscriber) { subscribers_.push_back(subscriber); } + /** + * Subscribe を解除する際に呼ぶ関数です。 + * + * @param subscriber Subscribe を解除する DisposeSubscriber + */ void RemoveSubscriber(DisposeSubscriber* subscriber) { subscribers_.erase( std::remove_if(subscribers_.begin(), subscribers_.end(), @@ -21,6 +53,11 @@ class DisposePublisher { }), subscribers_.end()); } + /** + * Subscriber に破棄されたことを通知する際に呼ぶ関数です。 + * + * TODO(tnoho): 役割的に protected にして良いのでは。 + */ virtual void Disposed() { for (DisposeSubscriber* subscriber : subscribers_) { subscriber->PublisherDisposed(); diff --git a/src/dummy_audio_mixer.cpp b/src/dummy_audio_mixer.cpp index b7c69bdb..d8413fd8 100644 --- a/src/dummy_audio_mixer.cpp +++ b/src/dummy_audio_mixer.cpp @@ -16,8 +16,14 @@ void DummyAudioMixer::Mix(size_t number_of_channels, webrtc::AudioFrame* audio_frame_for_mixing) { webrtc::MutexLock lock(&mutex_); for (auto& source_and_status : audio_source_list_) { - // 第一引数の設定値にサンプリングレートがリサンプリングされる - // -1 を指定するとリサンプリングされなくなる + /** + * webrtc::AudioTrackSinkInterface の OnData はこの関数内で呼ばれる + * + * 第一引数の設定値にサンプリングレートがリサンプリングされるが、 + * -1 を指定するとリサンプリングされなくなる。 + * SoraAudioSinkImpl の OnData 内でリサンプリングするため、 + * ここでは -1 を指定している。 + */ source_and_status->audio_source->GetAudioFrameWithInfo( -1, &source_and_status->audio_frame); } @@ -41,6 +47,11 @@ void DummyAudioMixer::RemoveSource(Source* audio_source) { DummyAudioMixer::DummyAudioMixer(webrtc::TaskQueueFactory* task_queue_factory) : task_queue_factory_(task_queue_factory) { + /** + * 通常 webrtc::AudioMixer の Mix は音声出力デバイスのループで呼ばれるが、 + * sora::SoraClientContextConfig::use_audio_device を false にした際に設定される、 + * webrtc::AudioDeviceDummy はループを回さないため、ここでループを作ることとした。 + */ task_queue_ = std::make_unique(task_queue_factory_->CreateTaskQueue( "TestAudioDeviceModuleImpl", diff --git a/src/dummy_audio_mixer.h b/src/dummy_audio_mixer.h index b3bc2b05..6d2e0b0d 100644 --- a/src/dummy_audio_mixer.h +++ b/src/dummy_audio_mixer.h @@ -12,6 +12,18 @@ #include #include +/** + * webrtc::AudioMixer を継承した DummyAudioMixer です。 + * + * PeerConnectionFactory 生成時に渡す cricket::MediaEngineDependencies の + * audio_mixer を指定しない場合 webrtc::AudioMixerImpl が使用されます。 + * これはすべての AudioTrack の出力データのサンプリングレートとチャネル数を揃え、 + * ミキシングした上で音声出力デバイスに渡す役割を担います。 + * しかし、 Python SDK では音声をデバイスに出力することはありません。 + * ですが、 AudioTrack からデータを受け取る AudioSinkInterface::OnData は + * AudioMixer により駆動されているため、 AudioSinkInterface::OnData を呼び出す仕組みだけを持つ + * シンプルな webrtc::AudioMixer になっています。 + */ class DummyAudioMixer : public webrtc::AudioMixer { public: struct SourceStatus; diff --git a/src/sora.h b/src/sora.h index 23cd2765..0f064c0d 100644 --- a/src/sora.h +++ b/src/sora.h @@ -12,12 +12,77 @@ #include "sora_track_interface.h" #include "sora_video_source.h" +/** + * Sora Python SDK のベースになるクラスです。 + * + * SoraFactory を内包し Connection や AudioSource、VideoSource を生成します。 + * 一つの Sora インスタンスから複数の Connection、AudioSource、VideoSource が生成できます。 + * 同じ Sora インスタンス内でしか Connection や AudioSource、VideoSource を共有できないので、 + * 複数の Sora インスタンスを生成することは不具合の原因になります。 + */ class Sora : public DisposePublisher { public: + /** + * このタイミングで SoraFactory の生成まで行うため SoraFactory の生成にあたって必要な引数はここで設定します。 + * + * @param use_hardware_encoder (オプション)ハードウェアエンコーダーの有効無効 デフォルト: true + * @param openh264 (オプション) OpenH264 ライブラリへのパス + */ Sora(std::optional use_hardware_encoder, std::optional openh264); ~Sora(); + /** + * Sora と接続する Connection を生成します。 + * + * 実装上の留意点:Sora C++ SDK が observer に std::weak_ptr を要求するためポインタで返す Source とは異なり、 + * std::shared_ptr で返しますが Python での扱いは変わりません。 + * + * @param signaling_urls シグナリングに使用する URL のリスト + * @param role ロール recvonly | sendonly | sendrecv + * @param channel_id チャネル ID + * @param client_id (オプション)クライアント ID + * @param bundle_id (オプション)バンドル ID + * @param metadata (オプション)認証メタデータ + * @param signaling_notify_metadata (オプション)シグナリング通知メタデータ + * @param audio_source (オプション)音声ソース CreateAudioSource で生成した SoraAudioSource を渡してください + * @param video_source (オプション)映像ソース CreateVideoSource で生成した SoraVideoSource を渡してください + * @param audio (オプション)音声の有効無効 デフォルト: true + * @param video (オプション)映像の有効無効 デフォルト: true + * @param audio_codec_type (オプション)音声コーデック OPUS | LYRA デフォルト: OPUS + * @param video_codec_type (オプション)映像コーデック VP8 | VP9 | AV1 | H264 デフォルト: VP9 + * @param video_bit_rate (オプション)映像ビットレート kbps 単位です + * @param audio_bit_rate (オプション)音声ビットレート kbps 単位です + * @param video_vp9_params (オプション)映像コーデック VP9 設定 + * @param video_av1_params (オプション)映像コーデック AV1 設定 + * @param video_h264_params (オプション)映像コーデック H264 設定 + * @param simulcast (オプション)サイマルキャストの有効無効 + * @param spotlight (オプション)スポットライトの有効無効 + * @param spotlight_number (オプション)スポットライトのフォーカス数 + * @param simulcast_rid (オプション)サイマルキャストで受信したい RID + * @param spotlight_focus_rid (オプション)スポットライトでフォーカスしているときのサイマルキャスト RID + * @param spotlight_unfocus_rid (オプション)スポットライトでフォーカスしていないときのサイマルキャスト RID + * @param forwarding_filter (オプション)転送フィルター設定 + * @param data_channels (オプション) DataChannel 設定 + * @param data_channel_signaling (オプション)シグナリングを DataChannel に切り替える機能の有効無効 + * @param ignore_disconnect_websocket (オプション)シグナリングを DataChannel に切り替えた際に WebSocket が切断されても切断としない機能の有効無効 + * @param data_channel_signaling_timeout (オプション) DataChannel シグナリングタイムアウト + * @param disconnect_wait_timeout (オプション) 切断待ちタイムアウト + * @param websocket_close_timeout (オプション) WebSocket クローズタイムアウト + * @param websocket_connection_timeout (オプション) WebSocket 接続タイムアウト + * @param audio_codec_lyra_bitrate (オプション) 音声コーデック Lyra のビットレート + * @param audio_codec_lyra_usedtx (オプション) 音声コーデック Lyra で DTX の有効無効 + * @param check_lyra_version (オプション) 音声コーデック Lyra のバージョンチェック有効無効 + * @param audio_streaming_language_code (オプション) 音声ストリーミング機能で利用する言語コード設定 + * @param insecure (オプション) 証明書チェックの有効無効 デフォルト: false + * @param client_cert (オプション) クライアント証明書 + * @param client_key (オプション) クライアントシークレットキー + * @param proxy_url (オプション) Proxy URL + * @param proxy_username (オプション) Proxy ユーザー名 + * @param proxy_password (オプション) Proxy パスワード + * @param proxy_agent (オプション) Proxy エージェント + * @return SoraConnection インスタンス + */ std::shared_ptr CreateConnection( // 必須パラメータ const nb::handle& signaling_urls, @@ -67,10 +132,40 @@ class Sora : public DisposePublisher { std::optional proxy_password, std::optional proxy_agent); + /** + * Sora に音声データを送る受け口である SoraAudioSource を生成します。 + * + * AudioSource に音声データを渡すことで、 Sora に音声を送ることができます。 + * AudioSource は MediaStreamTrack として振る舞うため、 + * AudioSource と同一の Sora インスタンスから生成された複数の Connection で共用できます。 + * + * @param channels AudioSource に入力する音声データのチャネル数 + * @param sample_rate AudioSource に入力する音声データのサンプリングレート + * @return SoraAudioSource インスタンス + */ SoraAudioSource* CreateAudioSource(size_t channels, int sample_rate); + /** + * Sora に映像データを送る受け口である SoraVideoSource を生成します。 + * + * VideoSource にフレームデータを渡すことで、 Sora に映像を送ることができます。 + * VideoSource は MediaStreamTrack として振る舞うため、 + * VideoSource と同一の Sora インスタンスから生成された複数の Connection で共用できます。 + * + * @return SoraVideoSource インスタンス + */ SoraVideoSource* CreateVideoSource(); private: + /** + * Python で渡された値を boost::json::value に変換します。 + * + * metadata のように JSON の値として扱える内容であれば自由に指定できるものを、 + * nanobind::handle で受け取って Sora C++ SDK で使っている boost::json::value に変換します。 + * + * @param value Python から渡された値の nanobind::handle + * @param error_message 変換に失敗した際に nanobind::type_error で返す際のエラーメッセージ + * @return boost::json::value + */ boost::json::value ConvertJsonValue(nb::handle value, const char* error_message); std::vector ConvertDataChannels( diff --git a/src/sora_audio_sink.h b/src/sora_audio_sink.h index 2b921a5c..fcfdc3af 100644 --- a/src/sora_audio_sink.h +++ b/src/sora_audio_sink.h @@ -19,9 +19,23 @@ namespace nb = nanobind; +/** + * Sora からの音声を受け取る SoraAudioSinkImpl です。 + * + * Connection の OnTrack コールバックから渡されるリモート Track から音声を取り出すことができます。 + * Track からの音声はコンストラクタで設定したサンプリングレートとチャネル数に変換し、 + * SoraAudioSinkImpl 内のバッファに溜め込まれるため、任意のタイミングで音声を取り出すことができます。 + * 実装上の留意点:Track の参照保持のための Impl のない SoraAudioSink を __init__.py に定義しています。 + * SoraAudioSinkImpl を直接 Python から呼び出すことは想定していません。 + */ class SoraAudioSinkImpl : public webrtc::AudioTrackSinkInterface, public DisposeSubscriber { public: + /** + * @param track 音声を取り出す OnTrack コールバックから渡されるリモート Track + * @param output_sample_rate 音声の出力サンプリングレート + * @param output_channels 音声の出力チャネル数 + */ SoraAudioSinkImpl(SoraTrackInterface* track, int output_sample_rate, size_t output_channels); @@ -38,11 +52,22 @@ class SoraAudioSinkImpl : public webrtc::AudioTrackSinkInterface, size_t number_of_frames, absl::optional absolute_capture_timestamp_ms) override; + /** + * 実装上の留意点:コールバックと Read 関数の共存はパフォーマンスや使い方の面で難しいことが判明したので、 + * on_data_, on_format_ ともに廃止予定です。 + */ std::function>)> on_data_; std::function on_format_; + /** + * 受信済みのデータをバッファから読み出す + * + * @param frames 受け取るチャンネルごとのサンプル数。0 を指定した場合には、受信済みのすべてのサンプルを返す + * @param timeout 溜まっているサンプル数が frames で指定した数を満たさない場合の待ち時間。秒単位の float で指定する + * @return Tuple でインデックス 0 には成否が、成功した場合のみインデックス 1 には NumPy の配列 numpy.ndarray で チャンネルごとのサンプル数 x チャンネル数 になっている音声データ + */ nb::tuple Read(size_t frames, float timeout); private: diff --git a/src/sora_audio_source.h b/src/sora_audio_source.h index a9c4bb9f..7522028a 100644 --- a/src/sora_audio_source.h +++ b/src/sora_audio_source.h @@ -18,6 +18,12 @@ namespace nb = nanobind; +/** + * SoraAudioSourceInterface は SoraAudioSource の実体です。 + * + * 実装上の留意点:webrtc::Notifier を継承しているクラスは + * nanobind で直接的な紐付けを行うとエラーが出るため SoraAudioSource とはクラスを分けました。 + */ class SoraAudioSourceInterface : public webrtc::Notifier { public: @@ -55,6 +61,13 @@ class SoraAudioSourceInterface int64_t last_timestamp_; }; +/** + * Sora に音声データを送る受け口である SoraAudioSource です。 + * + * AudioSource に音声データを渡すことで、 Sora に音声を送ることができます。 + * AudioSource は MediaStreamTrack として振る舞うため、 + * AudioSource と同一の Sora インスタンスから生成された複数の Connection で共用できます。 + */ class SoraAudioSource : public SoraTrackInterface { public: SoraAudioSource(DisposePublisher* publisher, @@ -63,15 +76,43 @@ class SoraAudioSource : public SoraTrackInterface { size_t channels, int sample_rate); + /** + * Sora に送る音声データを渡します。 + * + * @param data 送信する 16bit PCM データの参照 + * @param samples_per_channel チャンネルごとのサンプル数 + * @param timestamp Python の time.time() で取得できるエポック秒で表されるフレームのタイムスタンプ + */ void OnData(const int16_t* data, size_t samples_per_channel, double timestamp); + /** + * Sora に送る音声データを渡します。 + * + * タイムスタンプは先に受け取ったデータと連続になっていると想定してサンプル数から自動生成します。 + * + * @param data 送信する 16bit PCM データの参照 + * @param samples_per_channel チャンネルごとのサンプル数 + */ void OnData(const int16_t* data, size_t samples_per_channel); + /** + * Sora に送る音声データを渡します。 + * + * @param ndarray NumPy の配列 numpy.ndarray で チャンネルごとのサンプル数 x チャンネル数 になっている音声データ + * @param timestamp Python の time.time() で取得できるエポック秒で表されるフレームのタイムスタンプ + */ void OnData(nb::ndarray, nb::c_contig, nb::device::cpu> ndarray, double timestamp); + /** + * Sora に送る音声データを渡します。 + * + * タイムスタンプは先に受け取ったデータと連続になっていると想定してサンプル数から自動生成します。 + * + * @param ndarray NumPy の配列 numpy.ndarray で チャンネルごとのサンプル数 x チャンネル数 になっている音声データ + */ void OnData(nb::ndarray, nb::c_contig, diff --git a/src/sora_connection.cpp b/src/sora_connection.cpp index 93606b9b..eef52d6e 100644 --- a/src/sora_connection.cpp +++ b/src/sora_connection.cpp @@ -34,6 +34,7 @@ void SoraConnection::PublisherDisposed() { } void SoraConnection::Init(sora::SoraSignalingConfig& config) { + // TODO(tnoho): 複数回の呼び出しは禁止なので、ちゃんと throw する ioc_.reset(new boost::asio::io_context(1)); config.io_context = ioc_.get(); conn_ = sora::SoraSignaling::Create(config); @@ -51,6 +52,7 @@ void SoraConnection::Connect() { conn_->Connect(); + // ioc_->run(); は別スレッドで呼ばなければ、この関数は切断されるまで返らなくなってしまう thread_.reset(new std::thread([this]() { auto guard = boost::asio::make_work_guard(*ioc_); ioc_->run(); @@ -76,6 +78,7 @@ void SoraConnection::Disconnect() { } void SoraConnection::SetAudioTrack(SoraTrackInterface* audio_source) { + // TODO(tnoho): audio_sender_ がないと意味がないので、エラーを返すようにするべき if (audio_sender_) { audio_sender_->SetTrack(audio_source->GetTrack().get()); } @@ -87,6 +90,7 @@ void SoraConnection::SetAudioTrack(SoraTrackInterface* audio_source) { } void SoraConnection::SetVideoTrack(SoraTrackInterface* video_source) { + // TODO(tnoho): video_sender_ がないと意味がないので、エラーを返すようにするべき if (video_sender_) { video_sender_->SetTrack(video_source->GetTrack().get()); } @@ -109,6 +113,7 @@ void SoraConnection::OnSetOffer(std::string offer) { audio_result = conn_->GetPeerConnection()->AddTrack( audio_source_->GetTrack(), {stream_id}); if (audio_result.ok()) { + // javascript でいう replaceTrack を実装するために webrtc::RtpSenderInterface の参照をとっておく audio_sender_ = audio_result.value(); } } @@ -154,7 +159,7 @@ void SoraConnection::OnMessage(std::string label, std::string data) { void SoraConnection::OnTrack( rtc::scoped_refptr transceiver) { if (on_track_) { - // shared_ptr になってないのでリークする + // shared_ptr になってないとリークする auto track = std::make_shared( this, transceiver->receiver()->track()); AddSubscriber(track.get()); @@ -163,7 +168,9 @@ void SoraConnection::OnTrack( } void SoraConnection::OnRemoveTrack( - rtc::scoped_refptr receiver) {} + rtc::scoped_refptr receiver) { + // TODO(tnoho): 要実装 +} void SoraConnection::OnDataChannel(std::string label) { if (on_data_channel_) { diff --git a/src/sora_connection.h b/src/sora_connection.h index 50309811..0008789c 100644 --- a/src/sora_connection.h +++ b/src/sora_connection.h @@ -23,24 +23,70 @@ namespace nb = nanobind; +/** + * Sora との接続ごとに生成する SoraConnection です。 + * + * Python に Connection を制御する関数を提供します。 + */ class SoraConnection : public sora::SoraSignalingObserver, public DisposePublisher, public DisposeSubscriber { public: + /** + * コンストラクタではインスタンスの生成のみで実際の生成処理は Init 関数で行います。 + */ SoraConnection(DisposePublisher* publisher); ~SoraConnection(); void Disposed() override; void PublisherDisposed() override; + /** + * SoraConnection の初期化を行う関数です。 + * + * この関数は現在記述されている 1 箇所以外での呼び出しは禁止です。 + * 実際に Sora との接続である sora::SoraSignaling を生成しているのはこの関数です。 + * Python から Connection に各種コールバックを容易に設定できるようにするために、 + * SoraConnection に sora::SoraSignalingObserver を継承させました。 + * しかし sora::SoraSignalingConfig::observer が sora::SoraSignalingObserver の弱参照を要求するので、 + * SoraConnection 生成時には何もせず、ここで sora::SoraSignalingConfig を受け取って初期化するようにしました。 + * + * @param config Sora への接続設定を持つ sora::SoraSignalingConfig + */ void Init(sora::SoraSignalingConfig& config); + /** + * Sora と接続する関数です。 + */ void Connect(); + /** + * Sora から切断する関数です。 + */ void Disconnect(); + /** + * 音声トラックを入れ替える javascript でいう replaceTrack に相当する関数です。 + * + * TODO(tnoho): Python で呼び出すことを想定しているが、動作確認していないため NB_MODULE に定義していない + * + * @param audio_source 入れ替える新しい音声トラック + */ void SetAudioTrack(SoraTrackInterface* audio_source); + /** + * 映像トラックを入れ替える javascript でいう replaceTrack に相当する関数です。 + * + * TODO(tnoho): Python で呼び出すことを想定しているが、動作確認していないため NB_MODULE に定義していない + * + * @param audio_source 入れ替える新しい映像トラック + */ void SetVideoTrack(SoraTrackInterface* video_source); + /** + * DataChannel でデータを送信する関数です。 + * + * @param label 送信する DataChannel の label + * @param data 送信するデータ + */ bool SendDataChannel(const std::string& label, nb::bytes& data); - // sora::SoraSignalingObserver + // sora::SoraSignalingObserver に定義されているコールバック関数 void OnSetOffer(std::string offer) override; void OnDisconnect(sora::SoraSignalingErrorCode ec, std::string message) override; @@ -53,6 +99,7 @@ class SoraConnection : public sora::SoraSignalingObserver, rtc::scoped_refptr receiver) override; void OnDataChannel(std::string label) override; + // sora::SoraSignalingObserver のコールバック関数が呼び出された時に対応して呼び出す Python の関数を保持する std::function on_set_offer_; std::function on_disconnect_; std::function on_notify_; @@ -66,8 +113,10 @@ class SoraConnection : public sora::SoraSignalingObserver, std::unique_ptr ioc_; std::shared_ptr conn_; std::unique_ptr thread_; + // javascript でいう replaceTrack された際に RemoveSubscriber を呼び出すために参照を保持する SoraTrackInterface* audio_source_ = nullptr; SoraTrackInterface* video_source_ = nullptr; + // javascript でいう replaceTrack を実装するために webrtc::RtpSenderInterface の参照を保持する rtc::scoped_refptr audio_sender_; rtc::scoped_refptr video_sender_; }; diff --git a/src/sora_factory.cpp b/src/sora_factory.cpp index 4d4d0ab4..0a64829c 100644 --- a/src/sora_factory.cpp +++ b/src/sora_factory.cpp @@ -36,6 +36,7 @@ SoraFactory::SoraFactory(std::optional use_hardware_encoder, #endif sora::SoraClientContextConfig context_config; + // Audio デバイスは使わない、 use_audio_device を true にしただけでデバイスを掴んでしまうので常に false context_config.use_audio_device = false; if (use_hardware_encoder) { context_config.use_hardware_encoder = *use_hardware_encoder; @@ -44,8 +45,10 @@ SoraFactory::SoraFactory(std::optional use_hardware_encoder, [use_hardware_encoder = context_config.use_hardware_encoder, openh264]( const webrtc::PeerConnectionFactoryDependencies& dependencies, cricket::MediaEngineDependencies& media_dependencies) { + // 通常の AudioMixer を使うと use_audio_device が false のとき、音声のループは全て止まってしまうので自前の AudioMixer を使う media_dependencies.audio_mixer = DummyAudioMixer::Create(media_dependencies.task_queue_factory); + // アンチエコーやゲインコントロール、ノイズサプレッションが必要になる用途は想定していないため nullptr media_dependencies.audio_processing = nullptr; #ifndef _WIN32 diff --git a/src/sora_factory.h b/src/sora_factory.h index b637157b..34f0271e 100644 --- a/src/sora_factory.h +++ b/src/sora_factory.h @@ -11,6 +11,9 @@ // Sora #include +/** + * sora::SoraClientContext を呼び出す必要がある処理をまとめたクラスです。 + */ class SoraFactory { public: SoraFactory(std::optional use_hardware_encoder, diff --git a/src/sora_sdk_ext.cpp b/src/sora_sdk_ext.cpp index de7593a9..17f317bd 100644 --- a/src/sora_sdk_ext.cpp +++ b/src/sora_sdk_ext.cpp @@ -18,20 +18,25 @@ namespace nb = nanobind; using namespace nb::literals; -/* - * コールバック関数のメンバー変数は Py_tp_traverse で visit コールバックを呼び出すようにする - * やっておかないと終了時にリークエラーが発生する +/** + * クラスにコールバック関数のメンバー変数がある場合は全て以下のように、 + * Py_VISIT を呼び出すことによりガベージコレクタにその存在を伝える関数を作る。 + * やっておかないと終了時にリークエラーが発生する。 */ - int audio_sink_tp_traverse(PyObject* self, visitproc visit, void* arg) { + // インスタンスを取得する SoraAudioSinkImpl* audio_sink = nb::inst_ptr(self); + // コールバックがある場合 if (audio_sink->on_format_) { + // コールバック変数の参照を取得して nb::object on_format = nb::cast(audio_sink->on_format_, nb::rv_policy::none); + // ガベージコレクタに伝える Py_VISIT(on_format.ptr()); } + // 上に同じ if (audio_sink->on_data_) { nb::object on_data = nb::cast(audio_sink->on_data_, nb::rv_policy::none); Py_VISIT(on_data.ptr()); @@ -40,6 +45,10 @@ int audio_sink_tp_traverse(PyObject* self, visitproc visit, void* arg) { return 0; } +/** + * PyType_Slot の Py_tp_traverse に先に作った関数を設定する。 + * 定義した PyType_Slot は NB_MODULE 内の対応するクラスに対して紐づける。 + */ PyType_Slot audio_sink_slots[] = { {Py_tp_traverse, (void*)audio_sink_tp_traverse}, {0, nullptr}}; @@ -107,6 +116,9 @@ PyType_Slot connection_slots[] = { {Py_tp_traverse, (void*)connection_tp_traverse}, {0, nullptr}}; +/** + * Python で利用するすべてのクラスと定数は以下のように定義しなければならない + */ NB_MODULE(sora_sdk_ext, m) { nb::enum_(m, "SoraSignalingErrorCode", nb::is_arithmetic()) diff --git a/src/sora_track_interface.h b/src/sora_track_interface.h index 531a992e..ec642ceb 100644 --- a/src/sora_track_interface.h +++ b/src/sora_track_interface.h @@ -7,6 +7,12 @@ #include "dispose_listener.h" +/** + * webrtc::MediaStreamTrackInterface を格納する SoraTrackInterface です。 + * + * webrtc::MediaStreamTrackInterface は rtc::scoped_refptr なので、 + * nanobind で直接のハンドリングが難しいので用意しました。 + */ class SoraTrackInterface : public DisposePublisher, public DisposeSubscriber { public: SoraTrackInterface( @@ -20,6 +26,11 @@ class SoraTrackInterface : public DisposePublisher, public DisposeSubscriber { Disposed(); } + /** + * Python で呼び出すための関数 + * この実装では track_ が nullptr になっているとクラッシュしてしまいますが、 + * その時には publisher_ も失われているため許容することとしました。 + */ std::string kind() const { return track_->kind(); } std::string id() const { return track_->id(); } bool enabled() const { return track_->enabled(); } @@ -27,6 +38,12 @@ class SoraTrackInterface : public DisposePublisher, public DisposeSubscriber { webrtc::MediaStreamTrackInterface::TrackState state() { return track_->state(); } + + /** + * webrtc::MediaStreamTrackInterface の実体を取り出すため Python SDK 内で使う関数です。 + * + * @return rtc::scoped_refptr + */ rtc::scoped_refptr GetTrack() { return track_; } @@ -36,7 +53,10 @@ class SoraTrackInterface : public DisposePublisher, public DisposeSubscriber { publisher_ = nullptr; track_ = nullptr; } - virtual void PublisherDisposed() override { Disposed(); } + virtual void PublisherDisposed() override { + // Track は生成元が破棄された後に再利用することはないので Disposed() を呼ぶ + Disposed(); + } protected: DisposePublisher* publisher_; diff --git a/src/sora_video_sink.cpp b/src/sora_video_sink.cpp index 7fc21d14..a2dc3f92 100644 --- a/src/sora_video_sink.cpp +++ b/src/sora_video_sink.cpp @@ -7,6 +7,12 @@ SoraVideoFrame::SoraVideoFrame( rtc::scoped_refptr i420_data) : width_(i420_data->width()), height_(i420_data->height()) { + /** + * データを取り出す際に Python 側で自由に FourCC を指定できる形にするのも手ですが、 + * その場合は関数を呼び出すたびに変換が走るので GIL を長く保持してしまいます。 + * また、複数回呼び出された際に毎回変換を行いパフォーマンスが悪化してしまうので、 + * ここで numpy の形式である 24BG に変換することとしました。 + */ argb_data_ = std::unique_ptr(new uint8_t[width_ * height_ * 3]); libyuv::ConvertFromI420( i420_data->DataY(), i420_data->StrideY(), i420_data->DataU(), @@ -27,6 +33,7 @@ SoraVideoSinkImpl::SoraVideoSinkImpl(SoraTrackInterface* track) track_->AddSubscriber(this); webrtc::VideoTrackInterface* video_track = static_cast(track_->GetTrack().get()); + // video_track にこの Sink を追加し OnFrame を呼び出してもらいます。 video_track->AddOrUpdateSink(this, rtc::VideoSinkWants()); } @@ -45,6 +52,7 @@ void SoraVideoSinkImpl::Disposed() { if (track_ && track_->GetTrack()) { webrtc::VideoTrackInterface* video_track = static_cast(track_->GetTrack().get()); + // video_track からこの Sink を削除します。 video_track->RemoveSink(this); } track_ = nullptr; @@ -58,6 +66,11 @@ void SoraVideoSinkImpl::OnFrame(const webrtc::VideoFrame& frame) { if (frame.width() == 0 || frame.height() == 0) return; if (on_frame_) { + /** + * 形式を問わず I420 でフレームデータを取得している。 + * 特殊なコーデックを選択しない限りはデコードされたフレームデータは I420 の形式になっているはずなので問題ないと考えた。 + * webrtc::VideoFrame を継承した特殊なフレームであったとしても ToI420 は実装されているはず。 + */ rtc::scoped_refptr i420_data = frame.video_frame_buffer()->ToI420(); on_frame_(std::make_shared(i420_data)); diff --git a/src/sora_video_sink.h b/src/sora_video_sink.h index 6af6587e..fbb3ca2f 100644 --- a/src/sora_video_sink.h +++ b/src/sora_video_sink.h @@ -18,34 +18,69 @@ namespace nb = nanobind; +/** + * Sora からのフレームを格納する SoraVideoFrame です。 + * + * on_frame_ コールバックで直接フレームデータの ndarray を返してしまうとメモリーリークしてしまうため、 + * フレームデータを Python で適切にハンドリングできるようにするために用意しました。 + */ class SoraVideoFrame { public: SoraVideoFrame(rtc::scoped_refptr i420_data); + /** + * SoraVideoFrame 内のフレームデータへの numpy.ndarray での参照を渡します。 + * + * @return NumPy の配列 numpy.ndarray で H x W x BGR になっているフレームデータ + */ nb::ndarray> Data(); private: + // width や height は ndarray に情報として含まれるため、これらを別で返す関数は不要 const int width_; const int height_; std::unique_ptr argb_data_; }; +/** + * Sora からの映像を受け取る SoraVideoSinkImpl です。 + * + * Connection の OnTrack コールバックから渡されるリモート Track から映像を取り出すことができます。 + * 実装上の留意点:Track の参照保持のための Impl のない SoraVideoSink を __init__.py に定義しています。 + * SoraVideoSinkImpl を直接 Python から呼び出すことは想定していません。 + */ class SoraVideoSinkImpl : public rtc::VideoSinkInterface, public DisposeSubscriber { public: + /** + * @param track 映像を取り出す OnTrack コールバックから渡されるリモート Track + */ SoraVideoSinkImpl(SoraTrackInterface* track); ~SoraVideoSinkImpl(); void Del(); void Disposed(); - // rtc::VideoSinkInterface + /** + * VideoTrack からフレームデータが来るたびに呼び出される関数です。 + * + * 継承している rtc::VideoSinkInterface で定義されています。 + * + * @param frame VideoTrack から渡されるフレームデータ + */ void OnFrame(const webrtc::VideoFrame& frame) override; // DisposeSubscriber void PublisherDisposed() override; - // このコールバックは shared_ptr にしないとリークする + /** + * フレームデータが来るたびに呼び出されるコールバック変数です。 + * + * フレームが受信される度に呼び出されます。 + * このコールバック関数内では重い処理は行わないでください。サンプルを参考に queue を利用するなどの対応を推奨します。 + * また、この関数はメインスレッドから呼び出されないため、関数内で cv2.imshow を実行しても macOS の場合は表示されません。 + * 実装上の留意点:このコールバックで渡す引数は shared_ptr にしておかないとリークします。 + */ std::function)> on_frame_; private: diff --git a/src/sora_video_source.h b/src/sora_video_source.h index 624089ba..bc598141 100644 --- a/src/sora_video_source.h +++ b/src/sora_video_source.h @@ -22,6 +22,14 @@ namespace nb = nanobind; +/** + * Sora に映像データを送る受け口である SoraVideoSource です。 + * + * VideoSource にフレームデータを渡すことで、 Sora に映像を送ることができます。 + * 送信時通信状況によってはフレームのリサイズやドロップが行われます。 + * VideoSource は MediaStreamTrack として振る舞うため、 + * VideoSource と同一の Sora インスタンスから生成された複数の Connection で共用できます。 + */ class SoraVideoSource : public SoraTrackInterface { public: SoraVideoSource(DisposePublisher* publisher, @@ -30,15 +38,45 @@ class SoraVideoSource : public SoraTrackInterface { void Disposed() override; void PublisherDisposed() override; + /** + * Sora に映像データとして送るフレームを渡します。 + * + * この関数が呼び出された時点のタイムスタンプでフレームを送信します。 + * 映像になるように一定のタイミングで呼び出さない場合、受信側でコマ送りになります。 + * + * @param ndarray NumPy の配列 numpy.ndarray で H x W x BGR になっているフレームデータ + */ void OnCaptured(nb::ndarray, nb::c_contig, nb::device::cpu> ndarray); + /** + * Sora に映像データとして送るフレームを渡します。 + * + * timestamp 引数で渡されたタイムスタンプでフレームを送信します。 + * フレームのタイムスタンプを指定できるようにするため用意したオーバーロードです。 + * timestamp が映像になるように一定の時間差がない場合、受信側で正しく表示されない場合があります。 + * 表示側で音声データの timestamp と同期を取るため遅延が発生する場合があります。 + * + * @param ndarray NumPy の配列 numpy.ndarray で H x W x BGR になっているフレームデータ + * @param timestamp Python の time.time() で取得できるエポック秒で表されるフレームのタイムスタンプ + */ void OnCaptured(nb::ndarray, nb::c_contig, nb::device::cpu> ndarray, double timestamp); + /** + * Sora に映像データとして送るフレームを渡します。 + * + * timestamp_us 引数で渡されたマイクロ秒精度の整数で表されるタイムスタンプでフレームを送信します。 + * libWebRTC のタイムスタンプはマイクロ秒精度のため用意したオーバーロードです。 + * timestamp が映像になるように一定の時間差がない場合、受信側で正しく表示されない場合があります。 + * 表示側で音声データの timestamp と同期を取るため遅延が発生する場合があります。 + * + * @param ndarray NumPy の配列 numpy.ndarray で H x W x BGR になっているフレームデータ + * @param timestamp_us マイクロ秒単位の整数で表されるフレームのタイムスタンプ + */ void OnCaptured(nb::ndarray, nb::c_contig,