sora_sdk 2026.2.0

Sora Rust SDK
//! 音声コーデックの実装情報と capability トレイト。
use shiguredo_webrtc::{
    AudioCodecInfo, AudioCodecSpec, AudioCodecType, AudioDecoder, AudioEncoder,
    AudioEncoderFactoryOptions, EnvironmentRef, SdpAudioFormatRef,
};

use nojson::{DisplayJson, JsonFormatter, JsonParseError, RawJsonValue};

use crate::codec_direction::CodecDirection;

/// 音声コーデックの実装情報。
///
/// `name` は `AudioCodecPreference` との突き合わせに利用される識別子で、
/// 実装ごとに一意である必要がある。
#[derive(Debug, Clone, Eq)]
pub struct AudioCodecImplementation {
    name: String,
    description: String,
}

impl AudioCodecImplementation {
    /// 新しい `AudioCodecImplementation` を生成する。
    pub fn new(name: &'static str, description: &'static str) -> Self {
        Self {
            name: name.to_string(),
            description: description.to_string(),
        }
    }

    fn new_internal(name: String, description: String) -> Self {
        Self { name, description }
    }

    /// 実装名を返す。
    pub fn name(&self) -> &str {
        self.name.as_str()
    }

    /// 実装の説明文を返す。
    pub fn description(&self) -> &str {
        self.description.as_str()
    }
}

impl PartialEq for AudioCodecImplementation {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name
    }
}

/// `AudioCodecCapability` は、各 codec 実装ごとの差分を吸収するためのインターフェース。
///
/// 各エンコーダー/デコーダーの実装ごとに `AudioCodecCapability` を実装することで
/// Sora クライアントから利用可能になる。
pub trait AudioCodecCapability: Send {
    /// この capability を識別する実装情報を返す。
    ///
    /// 実装名は `AudioCodecPreference` との突き合わせに利用されるため、実装ごとに一意である必要がある。
    fn get_implementation(&self) -> AudioCodecImplementation;

    /// 指定したエンコーダー/デコーダーでサポートされているコーデック仕様のリストを返す。
    fn get_supported_codec_specs(&self, direction: CodecDirection) -> Vec<AudioCodecSpec>;

    /// 指定方向で `codec_type` が利用可能かどうかを返す。
    ///
    /// デフォルト実装では [Self::get_supported_codec_specs] のフォーマット名で判定する。
    fn is_supported(&self, direction: CodecDirection, codec_type: AudioCodecType) -> bool {
        let Some(codec_name) = codec_type.as_str() else {
            return false;
        };
        self.get_supported_codec_specs(direction)
            .iter()
            .any(|spec| spec.format().name().ok().as_deref() == Some(codec_name))
    }

    /// `format` に対応するコーデック情報を返す。
    ///
    /// `format` はネゴシエーションで決まったフォーマット。
    ///
    /// 指定方向で `format` を利用できるかを判定し、利用できる場合にコーデック情報を返す。
    /// 対応しないフォーマットの場合は None を返す。
    fn query(
        &self,
        direction: CodecDirection,
        format: SdpAudioFormatRef<'_>,
    ) -> Option<AudioCodecInfo>;

    /// 指定したフォーマットでエンコーダーがサポートされている場合は AudioEncoder を返す。
    ///
    /// `format` はネゴシエーションで決まったフォーマット。
    ///
    /// `options` はネゴシエーションで決まる音声エンコーダーの設定であり、
    /// ペイロードタイプやコーデックペア ID (Redundant Encoding 用) が含まれる。
    /// エンコーダーへ渡す際は設定を捨てずにそのまま引き継ぐこと。
    ///
    /// 対応しないフォーマットで呼び出された場合の動作は実装に依存するため、
    /// None が返されることは保証されない。
    #[expect(unused_variables)]
    fn create_audio_encoder(
        &self,
        env: EnvironmentRef<'_>,
        format: SdpAudioFormatRef<'_>,
        options: &AudioEncoderFactoryOptions,
    ) -> Option<AudioEncoder> {
        None
    }

    /// 指定したフォーマットでデコーダーがサポートされている場合は AudioDecoder を返す。
    ///
    /// `format` はネゴシエーションで決まったフォーマット。
    ///
    /// 対応しないフォーマットで呼び出された場合の動作は実装に依存するため、
    /// None が返されることは保証されない。
    #[expect(unused_variables)]
    fn create_audio_decoder(
        &self,
        env: EnvironmentRef<'_>,
        format: SdpAudioFormatRef<'_>,
    ) -> Option<AudioDecoder> {
        None
    }
}

impl DisplayJson for AudioCodecImplementation {
    fn fmt(&self, f: &mut JsonFormatter<'_, '_>) -> std::fmt::Result {
        f.object(|f| {
            f.member("name", &self.name)?;
            f.member("description", &self.description)
        })
    }
}

impl<'text, 'raw> TryFrom<RawJsonValue<'text, 'raw>> for AudioCodecImplementation {
    type Error = JsonParseError;

    fn try_from(value: RawJsonValue<'text, 'raw>) -> std::result::Result<Self, Self::Error> {
        let name: String = value.to_member("name")?.required()?.try_into()?;
        let description: String = value.to_member("description")?.required()?.try_into()?;
        Ok(Self::new_internal(name, description))
    }
}

/// `capabilities` の中から指定した実装名の capability を探す。
pub(crate) fn find_audio_capability<'a>(
    capabilities: &'a [Box<dyn AudioCodecCapability>],
    implementation: &AudioCodecImplementation,
) -> Option<&'a dyn AudioCodecCapability> {
    let implementation_name = implementation.name();
    capabilities
        .iter()
        .map(|capability| capability.as_ref())
        .find(|capability| capability.get_implementation().name() == implementation_name)
}

#[cfg(test)]
mod tests {
    use super::*;
    use nojson::Json;
    use shiguredo_webrtc::{AudioCodecType, SdpAudioFormat};

    use crate::testing::{TestAudioCodecCapability, TestingAudioCodecRecorders};

    #[test]
    fn audio_codec_implementation_round_trip() {
        let value = AudioCodecImplementation::new("internal", "WebRTC built-in");
        let json_text = Json(&value).to_string();
        let parsed: Json<AudioCodecImplementation> =
            json_text.parse().expect("JSON のパースに失敗しました");
        assert_eq!(parsed.0, value);
    }

    #[test]
    fn audio_trait_works_with_trait_object() {
        let capability: Box<dyn AudioCodecCapability> = Box::new(TestAudioCodecCapability::new(
            AudioCodecImplementation::new("test", "Test Codec"),
            vec![AudioCodecType::Opus],
            vec![AudioCodecType::Opus],
        ));
        assert_eq!(capability.get_implementation().name(), "test");
        assert!(capability.is_supported(CodecDirection::Encoder, AudioCodecType::Opus));
        assert!(capability.is_supported(CodecDirection::Decoder, AudioCodecType::Opus));
        let opus = SdpAudioFormat::new("opus", 48000, 2);
        let env = shiguredo_webrtc::Environment::new();
        let mut options = shiguredo_webrtc::AudioEncoderFactoryOptions::new();
        options.set_payload_type(111);
        assert!(
            capability
                .create_audio_encoder(env.as_ref(), opus.as_ref(), &options)
                .is_some()
        );
        assert!(
            capability
                .create_audio_decoder(env.as_ref(), opus.as_ref())
                .is_some()
        );
    }

    /// `create_audio_encoder` が Options を素通しで受け取り、codec_pair_id まで
    /// 引き継げることを検証する。
    ///
    /// `AudioEncoderFactoryOptions` を引数で受け取り作り直さずに下位へ渡す契約なので、
    /// codec_pair_id (Redundant Encoding のペアリング) も保持される。
    #[test]
    fn audio_encoder_create_forwards_codec_pair_id_in_options() {
        let recorders = TestingAudioCodecRecorders::new();
        let capability = TestAudioCodecCapability::new_with_recorders(
            AudioCodecImplementation::new("test", "Test Codec"),
            vec![AudioCodecType::Opus],
            vec![AudioCodecType::Opus],
            recorders.clone(),
        );
        let env = shiguredo_webrtc::Environment::new();
        let opus = SdpAudioFormat::new("opus", 48000, 2);
        let mut options = shiguredo_webrtc::AudioEncoderFactoryOptions::new();
        options.set_payload_type(111);
        let pair_id = shiguredo_webrtc::AudioCodecPairId::create();
        options.set_codec_pair_id(Some(&pair_id));

        assert!(
            capability
                .create_audio_encoder(env.as_ref(), opus.as_ref(), &options)
                .is_some()
        );
        assert_eq!(
            recorders.codec_pair_id(),
            Some(pair_id.numeric_representation()),
            "codec_pair_id が create_audio_encoder まで素通しされていません"
        );
    }
}