Skip to main content

player_plugin_loader/
diagnostics.rs

1use super::*;
2use player_plugin::PluginReference;
3
4/// Codec/media request used when matching decoder plugin capabilities.
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct DecoderPluginMatchRequest {
7    pub codec: String,
8    pub media_kind: DecoderMediaKind,
9}
10
11impl DecoderPluginMatchRequest {
12    pub fn video(codec: impl Into<String>) -> Self {
13        Self {
14            codec: codec.into(),
15            media_kind: DecoderMediaKind::Video,
16        }
17    }
18
19    pub fn audio(codec: impl Into<String>) -> Self {
20        Self {
21            codec: codec.into(),
22            media_kind: DecoderMediaKind::Audio,
23        }
24    }
25}
26
27/// Structured codec entry reported by one decoder plugin.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct DecoderPluginCodecSummary {
30    pub codec: String,
31    pub media_kind: DecoderMediaKind,
32}
33
34impl From<&DecoderCodecCapability> for DecoderPluginCodecSummary {
35    fn from(capability: &DecoderCodecCapability) -> Self {
36        Self {
37            codec: capability.codec.clone(),
38            media_kind: capability.media_kind,
39        }
40    }
41}
42
43/// Compact capability summary for one decoder plugin.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct DecoderPluginCapabilitySummary {
46    pub typed_codecs: Vec<DecoderPluginCodecSummary>,
47    pub codecs: Vec<String>,
48    pub supports_native_frame_output: bool,
49    pub native_requirements: Option<DecoderNativeRequirements>,
50    pub supports_hardware_decode: bool,
51    pub supports_cpu_video_frames: bool,
52    pub supports_audio_packets: bool,
53    pub supports_audio_frames: bool,
54    pub supports_pcm_frames: bool,
55    pub supports_gpu_handles: bool,
56    pub supports_presentation_release: bool,
57    pub supports_flush: bool,
58    pub supports_drain: bool,
59    pub max_sessions: Option<u32>,
60}
61
62impl From<&DecoderCapabilities> for DecoderPluginCapabilitySummary {
63    fn from(capabilities: &DecoderCapabilities) -> Self {
64        Self::from_capabilities(capabilities, false, None)
65    }
66}
67
68/// Compact capability summary for one frame processor plugin.
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct FrameProcessorPluginCapabilitySummary {
71    pub accepted_input_handle_kinds: Vec<NativeHandleKind>,
72    pub output_handle_kinds: Vec<NativeHandleKind>,
73    pub accepted_input_pipeline_profiles: Vec<NativeFramePipelineProfile>,
74    pub output_pipeline_profiles: Vec<NativeFramePipelineProfile>,
75    pub supports_video_frames: bool,
76    pub supports_in_place_passthrough: bool,
77    pub preserves_dimensions: bool,
78    pub may_change_dimensions: bool,
79    pub preserves_color_metadata: bool,
80    pub preserves_hdr_metadata: bool,
81    pub supports_flush: bool,
82    pub max_sessions: Option<u32>,
83    pub max_in_flight_frames: Option<u32>,
84}
85
86impl From<&FrameProcessorCapabilities> for FrameProcessorPluginCapabilitySummary {
87    fn from(capabilities: &FrameProcessorCapabilities) -> Self {
88        Self {
89            accepted_input_handle_kinds: capabilities.accepted_input_handle_kinds.clone(),
90            output_handle_kinds: capabilities.output_handle_kinds.clone(),
91            accepted_input_pipeline_profiles: capabilities.accepted_input_pipeline_profiles.clone(),
92            output_pipeline_profiles: capabilities.output_pipeline_profiles.clone(),
93            supports_video_frames: capabilities.supports_video_frames,
94            supports_in_place_passthrough: capabilities.supports_in_place_passthrough,
95            preserves_dimensions: capabilities.preserves_dimensions,
96            may_change_dimensions: capabilities.may_change_dimensions,
97            preserves_color_metadata: capabilities.preserves_color_metadata,
98            preserves_hdr_metadata: capabilities.preserves_hdr_metadata,
99            supports_flush: capabilities.supports_flush,
100            max_sessions: capabilities.max_sessions,
101            max_in_flight_frames: capabilities.max_in_flight_frames,
102        }
103    }
104}
105
106/// Compact capability summary for one packet-stream source normalizer plugin.
107#[derive(Debug, Clone, PartialEq, Eq)]
108pub struct SourceNormalizerPacketPluginCapabilitySummary {
109    pub supported_runtime_profiles: Vec<String>,
110    pub max_level: player_plugin::SourceNormalizerNormalizeLevel,
111    pub media_kinds: Vec<player_plugin::SourceNormalizerPacketMediaKind>,
112    pub codecs: Vec<String>,
113    pub bitstream_formats: Vec<player_plugin::DecoderBitstreamFormat>,
114    pub supports_seek: bool,
115    pub supports_flush: bool,
116    pub required_capabilities: player_plugin::SourceNormalizerRequiredCapabilities,
117    pub max_sessions: Option<u32>,
118}
119
120impl From<&SourceNormalizerPacketCapabilities> for SourceNormalizerPacketPluginCapabilitySummary {
121    fn from(capabilities: &SourceNormalizerPacketCapabilities) -> Self {
122        Self {
123            supported_runtime_profiles: capabilities.supported_runtime_profiles.clone(),
124            max_level: capabilities.max_level,
125            media_kinds: capabilities.media_kinds.clone(),
126            codecs: capabilities.codecs.clone(),
127            bitstream_formats: capabilities.bitstream_formats.clone(),
128            supports_seek: capabilities.supports_seek,
129            supports_flush: capabilities.supports_flush,
130            required_capabilities: capabilities.required_capabilities.clone(),
131            max_sessions: capabilities.max_sessions,
132        }
133    }
134}
135
136/// Compact capability summary for one resource-output source normalizer plugin.
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct SourceNormalizerResourcePluginCapabilitySummary {
139    pub supported_runtime_profiles: Vec<String>,
140    pub supported_output_routes: Vec<String>,
141    pub max_level: player_plugin::SourceNormalizerNormalizeLevel,
142    pub content_types: Vec<String>,
143    pub supports_growing_resources: bool,
144    pub supports_range_reads: bool,
145    pub supports_cancel: bool,
146    pub required_capabilities: player_plugin::SourceNormalizerRequiredCapabilities,
147    pub cache_policy: player_plugin::SourceNormalizerResourceCachePolicy,
148    pub max_sessions: Option<u32>,
149}
150
151impl From<&SourceNormalizerResourceCapabilities>
152    for SourceNormalizerResourcePluginCapabilitySummary
153{
154    fn from(capabilities: &SourceNormalizerResourceCapabilities) -> Self {
155        Self {
156            supported_runtime_profiles: capabilities.supported_runtime_profiles.clone(),
157            supported_output_routes: capabilities
158                .supported_output_routes
159                .iter()
160                .map(|route| route.wire_name().to_owned())
161                .collect(),
162            max_level: capabilities.max_level,
163            content_types: capabilities.content_types.clone(),
164            supports_growing_resources: capabilities.supports_growing_resources,
165            supports_range_reads: capabilities.supports_range_reads,
166            supports_cancel: capabilities.supports_cancel,
167            required_capabilities: capabilities.required_capabilities.clone(),
168            cache_policy: capabilities.cache_policy.clone(),
169            max_sessions: capabilities.max_sessions,
170        }
171    }
172}
173
174/// Capability summary for one loaded plugin.
175#[derive(Debug, Clone, PartialEq, Eq)]
176pub enum PluginCapabilitySummary {
177    Decoder(DecoderPluginCapabilitySummary),
178    FrameProcessor(FrameProcessorPluginCapabilitySummary),
179    SourceNormalizerPacket(SourceNormalizerPacketPluginCapabilitySummary),
180    SourceNormalizerResource(SourceNormalizerResourcePluginCapabilitySummary),
181}
182
183impl DecoderPluginCapabilitySummary {
184    fn from_capabilities(
185        capabilities: &DecoderCapabilities,
186        supports_native_frame_output: bool,
187        native_requirements: Option<DecoderNativeRequirements>,
188    ) -> Self {
189        let typed_codecs = capabilities
190            .codecs
191            .iter()
192            .map(DecoderPluginCodecSummary::from)
193            .collect::<Vec<_>>();
194        let codecs = capabilities
195            .codecs
196            .iter()
197            .map(|codec| format!("{:?}:{}", codec.media_kind, codec.codec))
198            .collect();
199        Self {
200            typed_codecs,
201            codecs,
202            supports_native_frame_output,
203            native_requirements,
204            supports_hardware_decode: capabilities.supports_hardware_decode,
205            supports_cpu_video_frames: capabilities.supports_cpu_video_frames,
206            supports_audio_packets: capabilities
207                .codecs
208                .iter()
209                .any(|codec| codec.media_kind == DecoderMediaKind::Audio),
210            supports_audio_frames: capabilities.supports_audio_frames,
211            supports_pcm_frames: capabilities.supports_pcm_frames,
212            supports_gpu_handles: capabilities.supports_gpu_handles,
213            supports_presentation_release: capabilities.supports_presentation_release,
214            supports_flush: capabilities.supports_flush,
215            supports_drain: capabilities.supports_drain,
216            max_sessions: capabilities.max_sessions,
217        }
218    }
219}
220
221/// Loader-side diagnostic status for one plugin path.
222#[derive(Debug, Clone, Copy, PartialEq, Eq)]
223pub enum PluginDiagnosticStatus {
224    Loaded,
225    LoadFailed,
226    UnsupportedKind,
227    DecoderSupported,
228    DecoderUnsupported,
229    FrameProcessorSupported,
230    FrameProcessorUnsupported,
231    SourceNormalizerSupported,
232    SourceNormalizerUnsupported,
233}
234
235/// Capability availability derived from loader inspection only.
236#[derive(Debug, Clone, Copy, PartialEq, Eq)]
237pub enum PluginCapabilityAvailability {
238    /// The record does not describe a probed capability.
239    Unknown,
240    /// The probed capability is available for later selection.
241    Available,
242    /// Loading or capability inspection rejected the plugin.
243    Unavailable,
244}
245
246impl PluginCapabilityAvailability {
247    pub const fn wire_name(self) -> &'static str {
248        match self {
249            Self::Unknown => "unknown",
250            Self::Available => "available",
251            Self::Unavailable => "unavailable",
252        }
253    }
254}
255
256/// Diagnostic capability family independent from the native ABI layout.
257#[derive(Debug, Clone, Copy, PartialEq, Eq)]
258pub enum PluginCapabilityKind {
259    PostDownloadProcessor,
260    PipelineEventHook,
261    Decoder,
262    BenchmarkSink,
263    FrameProcessor,
264    SourceNormalizer,
265}
266
267impl PluginCapabilityKind {
268    pub const fn wire_name(self) -> &'static str {
269        match self {
270            Self::PostDownloadProcessor => "post_download_processor",
271            Self::PipelineEventHook => "pipeline_event_hook",
272            Self::Decoder => "decoder",
273            Self::BenchmarkSink => "benchmark_sink",
274            Self::FrameProcessor => "frame_processor",
275            Self::SourceNormalizer => "source_normalizer",
276        }
277    }
278}
279
280impl PluginDiagnosticStatus {
281    pub const fn wire_name(self) -> &'static str {
282        match self {
283            Self::Loaded => "loaded",
284            Self::LoadFailed => "loadFailed",
285            Self::UnsupportedKind => "unsupportedKind",
286            Self::DecoderSupported => "decoderSupported",
287            Self::DecoderUnsupported => "decoderUnsupported",
288            Self::FrameProcessorSupported => "frameProcessorSupported",
289            Self::FrameProcessorUnsupported => "frameProcessorUnsupported",
290            Self::SourceNormalizerSupported => "sourceNormalizerSupported",
291            Self::SourceNormalizerUnsupported => "sourceNormalizerUnsupported",
292        }
293    }
294
295    /// Returns availability established by this loader diagnostic.
296    ///
297    /// Availability never implies route selection or runtime participation.
298    pub const fn capability_availability(self) -> PluginCapabilityAvailability {
299        match self {
300            Self::DecoderSupported
301            | Self::FrameProcessorSupported
302            | Self::SourceNormalizerSupported => PluginCapabilityAvailability::Available,
303            Self::Loaded => PluginCapabilityAvailability::Unknown,
304            Self::LoadFailed
305            | Self::UnsupportedKind
306            | Self::DecoderUnsupported
307            | Self::FrameProcessorUnsupported
308            | Self::SourceNormalizerUnsupported => PluginCapabilityAvailability::Unavailable,
309        }
310    }
311}
312
313/// Structured diagnostic record for one dynamic plugin path.
314#[derive(Debug, Clone, PartialEq, Eq)]
315pub struct PluginDiagnosticRecord {
316    pub path: PathBuf,
317    pub status: PluginDiagnosticStatus,
318    pub plugin_name: Option<String>,
319    pub plugin_kind: Option<PluginCapabilityKind>,
320    pub capability_summary: Option<PluginCapabilitySummary>,
321    pub message: Option<String>,
322}
323
324pub(crate) fn decoder_capability_summary(
325    record: &PluginDiagnosticRecord,
326) -> Option<&DecoderPluginCapabilitySummary> {
327    match record.capability_summary.as_ref() {
328        Some(PluginCapabilitySummary::Decoder(summary)) => Some(summary),
329        _ => None,
330    }
331}
332
333pub(crate) fn source_normalizer_packet_capability_summary(
334    record: &PluginDiagnosticRecord,
335) -> Option<&SourceNormalizerPacketPluginCapabilitySummary> {
336    match record.capability_summary.as_ref() {
337        Some(PluginCapabilitySummary::SourceNormalizerPacket(summary)) => Some(summary),
338        _ => None,
339    }
340}
341
342pub(crate) fn source_normalizer_resource_capability_summary(
343    record: &PluginDiagnosticRecord,
344) -> Option<&SourceNormalizerResourcePluginCapabilitySummary> {
345    match record.capability_summary.as_ref() {
346        Some(PluginCapabilitySummary::SourceNormalizerResource(summary)) => Some(summary),
347        _ => None,
348    }
349}
350
351impl PluginDiagnosticRecord {
352    /// Returns the capability availability established during inspection.
353    pub const fn capability_availability(&self) -> PluginCapabilityAvailability {
354        self.status.capability_availability()
355    }
356
357    pub(crate) fn from_native_decoder_interface(
358        path: impl Into<PathBuf>,
359        plugin: &LoadedNativePlugin,
360        reference: &PluginReference,
361        decoder_match: &DecoderPluginMatchRequest,
362    ) -> Self {
363        let path = path.into();
364        match plugin.resolve_native_decoder(reference) {
365            Ok(factory) => {
366                let capabilities = factory.capabilities();
367                let native_requirements = factory.native_requirements();
368                let capability_summary = DecoderPluginCapabilitySummary::from_capabilities(
369                    &capabilities,
370                    true,
371                    Some(native_requirements),
372                );
373                let supported =
374                    capabilities.supports_codec(&decoder_match.codec, decoder_match.media_kind);
375                let status = if supported {
376                    PluginDiagnosticStatus::DecoderSupported
377                } else {
378                    PluginDiagnosticStatus::DecoderUnsupported
379                };
380                let message = if supported {
381                    format!(
382                        "{} instance `{}` advertises {:?} {} support with native-frame output",
383                        factory.name(),
384                        reference.capability_instance_id().unwrap_or("unknown"),
385                        decoder_match.media_kind,
386                        decoder_match.codec
387                    )
388                } else {
389                    format!(
390                        "{} instance `{}` does not advertise {:?} {} support",
391                        factory.name(),
392                        reference.capability_instance_id().unwrap_or("unknown"),
393                        decoder_match.media_kind,
394                        decoder_match.codec
395                    )
396                };
397                Self {
398                    path,
399                    status,
400                    plugin_name: Some(factory.name().to_owned()),
401                    plugin_kind: Some(PluginCapabilityKind::Decoder),
402                    capability_summary: Some(PluginCapabilitySummary::Decoder(capability_summary)),
403                    message: Some(message),
404                }
405            }
406            Err(error) => Self {
407                path,
408                status: PluginDiagnosticStatus::DecoderUnsupported,
409                plugin_name: Some(plugin.plugin_name().to_owned()),
410                plugin_kind: Some(PluginCapabilityKind::Decoder),
411                capability_summary: None,
412                message: Some(format!(
413                    "instance `{}` is unavailable: {error}",
414                    reference.capability_instance_id().unwrap_or("unknown")
415                )),
416            },
417        }
418    }
419
420    pub(crate) fn from_native_frame_processor_interface(
421        path: impl Into<PathBuf>,
422        plugin: &LoadedNativePlugin,
423        reference: &PluginReference,
424    ) -> Self {
425        let path = path.into();
426        match plugin.resolve_frame_processor(reference) {
427            Ok(factory) => {
428                let capabilities = factory.capabilities();
429                let capability_summary = FrameProcessorPluginCapabilitySummary::from(&capabilities);
430                let supported =
431                    capabilities.supports_video_frames && !capabilities.may_change_dimensions;
432                let status = if supported {
433                    PluginDiagnosticStatus::FrameProcessorSupported
434                } else {
435                    PluginDiagnosticStatus::FrameProcessorUnsupported
436                };
437                let message = if supported {
438                    format!(
439                        "{} frame processor instance `{}` loaded",
440                        factory.name(),
441                        reference.capability_instance_id().unwrap_or("unknown")
442                    )
443                } else if capabilities.may_change_dimensions {
444                    format!(
445                        "{} frame processor instance `{}` changes frame dimensions, which the current interface does not allow",
446                        factory.name(),
447                        reference.capability_instance_id().unwrap_or("unknown")
448                    )
449                } else {
450                    format!(
451                        "{} frame processor instance `{}` does not advertise video frame processing support",
452                        factory.name(),
453                        reference.capability_instance_id().unwrap_or("unknown")
454                    )
455                };
456                Self {
457                    path,
458                    status,
459                    plugin_name: Some(factory.name().to_owned()),
460                    plugin_kind: Some(PluginCapabilityKind::FrameProcessor),
461                    capability_summary: Some(PluginCapabilitySummary::FrameProcessor(
462                        capability_summary,
463                    )),
464                    message: Some(message),
465                }
466            }
467            Err(error) => Self {
468                path,
469                status: PluginDiagnosticStatus::FrameProcessorUnsupported,
470                plugin_name: Some(plugin.plugin_name().to_owned()),
471                plugin_kind: Some(PluginCapabilityKind::FrameProcessor),
472                capability_summary: None,
473                message: Some(format!(
474                    "instance `{}` is unavailable: {error}",
475                    reference.capability_instance_id().unwrap_or("unknown")
476                )),
477            },
478        }
479    }
480
481    pub(crate) fn from_native_source_packet_interface(
482        path: impl Into<PathBuf>,
483        plugin: &LoadedNativePlugin,
484        reference: &PluginReference,
485    ) -> Self {
486        let path = path.into();
487        match plugin.resolve_source_packet(reference) {
488            Ok(factory) => {
489                let capabilities = factory.packet_capabilities();
490                let capability_summary =
491                    SourceNormalizerPacketPluginCapabilitySummary::from(&capabilities);
492                let supported = !capabilities.supported_runtime_profiles.is_empty()
493                    && !capabilities.media_kinds.is_empty();
494                let status = if supported {
495                    PluginDiagnosticStatus::SourceNormalizerSupported
496                } else {
497                    PluginDiagnosticStatus::SourceNormalizerUnsupported
498                };
499                let message = if supported {
500                    format!(
501                        "{} source normalizer packet instance `{}` loaded",
502                        factory.name(),
503                        reference.capability_instance_id().unwrap_or("unknown")
504                    )
505                } else if capabilities.supported_runtime_profiles.is_empty() {
506                    format!(
507                        "{} packet instance `{}` does not advertise source normalizer runtime profiles",
508                        factory.name(),
509                        reference.capability_instance_id().unwrap_or("unknown")
510                    )
511                } else {
512                    format!(
513                        "{} packet instance `{}` does not advertise source normalizer media kinds",
514                        factory.name(),
515                        reference.capability_instance_id().unwrap_or("unknown")
516                    )
517                };
518                Self {
519                    path,
520                    status,
521                    plugin_name: Some(factory.name().to_owned()),
522                    plugin_kind: Some(PluginCapabilityKind::SourceNormalizer),
523                    capability_summary: Some(PluginCapabilitySummary::SourceNormalizerPacket(
524                        capability_summary,
525                    )),
526                    message: Some(message),
527                }
528            }
529            Err(error) => Self {
530                path,
531                status: PluginDiagnosticStatus::SourceNormalizerUnsupported,
532                plugin_name: Some(plugin.plugin_name().to_owned()),
533                plugin_kind: Some(PluginCapabilityKind::SourceNormalizer),
534                capability_summary: None,
535                message: Some(format!(
536                    "instance `{}` is unavailable: {error}",
537                    reference.capability_instance_id().unwrap_or("unknown")
538                )),
539            },
540        }
541    }
542
543    pub(crate) fn from_native_source_resource_interface(
544        path: impl Into<PathBuf>,
545        plugin: &LoadedNativePlugin,
546        reference: &PluginReference,
547    ) -> Self {
548        let path = path.into();
549        match plugin.resolve_source_resource(reference) {
550            Ok(factory) => {
551                let capabilities = factory.resource_capabilities();
552                let capability_summary =
553                    SourceNormalizerResourcePluginCapabilitySummary::from(&capabilities);
554                let supported = !capabilities.supported_runtime_profiles.is_empty()
555                    && !capabilities.supported_output_routes.is_empty();
556                let status = if supported {
557                    PluginDiagnosticStatus::SourceNormalizerSupported
558                } else {
559                    PluginDiagnosticStatus::SourceNormalizerUnsupported
560                };
561                let message = if supported {
562                    format!(
563                        "{} source normalizer resource instance `{}` loaded",
564                        factory.name(),
565                        reference.capability_instance_id().unwrap_or("unknown")
566                    )
567                } else if capabilities.supported_runtime_profiles.is_empty() {
568                    format!(
569                        "{} resource instance `{}` does not advertise source normalizer runtime profiles",
570                        factory.name(),
571                        reference.capability_instance_id().unwrap_or("unknown")
572                    )
573                } else {
574                    format!(
575                        "{} resource instance `{}` does not advertise source normalizer output routes",
576                        factory.name(),
577                        reference.capability_instance_id().unwrap_or("unknown")
578                    )
579                };
580                Self {
581                    path,
582                    status,
583                    plugin_name: Some(factory.name().to_owned()),
584                    plugin_kind: Some(PluginCapabilityKind::SourceNormalizer),
585                    capability_summary: Some(PluginCapabilitySummary::SourceNormalizerResource(
586                        capability_summary,
587                    )),
588                    message: Some(message),
589                }
590            }
591            Err(error) => Self {
592                path,
593                status: PluginDiagnosticStatus::SourceNormalizerUnsupported,
594                plugin_name: Some(plugin.plugin_name().to_owned()),
595                plugin_kind: Some(PluginCapabilityKind::SourceNormalizer),
596                capability_summary: None,
597                message: Some(format!(
598                    "instance `{}` is unavailable: {error}",
599                    reference.capability_instance_id().unwrap_or("unknown")
600                )),
601            },
602        }
603    }
604
605    pub(crate) fn unsupported_native_interface(
606        path: impl Into<PathBuf>,
607        plugin: &LoadedNativePlugin,
608        interface: &'static str,
609    ) -> Self {
610        Self {
611            path: path.into(),
612            status: PluginDiagnosticStatus::UnsupportedKind,
613            plugin_name: Some(plugin.plugin_name().to_owned()),
614            plugin_kind: None,
615            capability_summary: None,
616            message: Some(format!(
617                "{} does not expose interface {interface}",
618                plugin.plugin_name()
619            )),
620        }
621    }
622
623    pub fn load_failed(path: impl Into<PathBuf>, error: PluginLoadError) -> Self {
624        Self::load_failed_message(path, error.to_string())
625    }
626
627    pub(crate) fn load_failed_message(
628        path: impl Into<PathBuf>,
629        message: impl Into<String>,
630    ) -> Self {
631        Self {
632            path: path.into(),
633            status: PluginDiagnosticStatus::LoadFailed,
634            plugin_name: None,
635            plugin_kind: None,
636            capability_summary: None,
637            message: Some(message.into()),
638        }
639    }
640
641    pub fn summary(&self) -> String {
642        self.message
643            .clone()
644            .or_else(|| self.plugin_name.clone())
645            .unwrap_or_else(|| self.path.display().to_string())
646    }
647}