1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
/* Copyright (C) 2023 Purism SPC
 * Copyright (C) 2024 DorotaC
 * SPDX-License-Identifier: LGPL-2.1-or-later OR MPL-2.0
 */

/*! Linux media device support */

use std::fs::File;
use std::io;
use std::io::Read;
use std::os::fd::AsRawFd;
use std::path::{Path, PathBuf};

use media_subsystem::{MediaEntF, MediaDeviceInfo, media_ioc_device_info, Zeroed, MediaV2Entity, MediaV2Interface, media_v2_interface, MediaV2Link, MediaIntfT,  media_ioc_g_topology, media_v2_topology};
use media_subsystem::sys::{media_v2_entity, media_v2_link};

use super::flock::{Lock, Locked, FileLike};

pub struct Device(pub File);

impl FileLike for Device {
    fn get_metadata(&self) -> io::Result<std::fs::Metadata> {
        self.0.get_metadata()
    }
    fn as_raw_fd(&self) -> std::os::unix::prelude::RawFd {
        AsRawFd::as_raw_fd(&self.0)
    }  
}

impl Device {
    pub fn new(p: impl AsRef<Path>) -> Result<Self, io::Error> {
        Ok(Self(File::open(p)?))
    }

    pub fn get_device_info(&self) -> Result<MediaDeviceInfo, nix::errno::Errno>{
        let device_pointer = AsRawFd::as_raw_fd(&self.0);
        let mut media_device_info = MediaDeviceInfo::zeroed();

        unsafe {
            media_ioc_device_info(
                device_pointer,
                &mut media_device_info,
            )
        }?;
        Ok(media_device_info)
    }
    
    /// Returns Ok when successfully locked, Err otherwise.
    /// TODO: A device is considered locked for libvidi when its main entity is locked?.
    /// FIXME: Does not attempt to retry when interrupted.
    pub fn try_lock(self) -> Result<Locked<Self>, Self> {
        Locked::new(self)
    }
    
    pub fn get_video_capture_path(&self) -> Result<PathBuf, io::Error> {
        let device_pointer = AsRawFd::as_raw_fd(&self.0);
        let mut topology = media_v2_topology::zeroed();
        unsafe {
            media_ioc_g_topology(device_pointer, &mut topology)
        }?;
        let mut links: Vec<_> = (0..topology.num_links)
            .map(|_| media_v2_link::zeroed())
            .collect();
        let mut entities: Vec<_> = (0..topology.num_entities)
            .map(|_| media_v2_entity::zeroed())
            .collect();
        let mut interfaces: Vec<_> = (0..topology.num_interfaces)
            .map(|_| media_v2_interface::zeroed())
            .collect();
        let mut topology = media_v2_topology::zeroed();
        topology.set_entities(&mut entities);
        topology.set_interfaces(&mut interfaces);
        topology.num_links = links.len() as u32;
        topology.ptr_links = links.as_mut_ptr() as u64;
        
        unsafe {
            media_ioc_g_topology(device_pointer, &mut topology)
        }.unwrap();

        let links: Vec<_> = links.into_iter()
            .map(MediaV2Link::from)
            .collect();
        let interfaces: Vec<_> = interfaces.iter()
            .map(MediaV2Interface::from)
            .collect();
        // Libcamera looks for interfaces with MediaIntfT::V4L_VIDEO,
        // but libmegapixels looks for entities with MediaEntF::IoV4L, which then get tracked to interfaces through links.
        // On my UVC camera, those entities are matched evenly to those interfaces. It's simpler to only parse the interfaces list.
        let video_interface = interfaces.iter()
            .filter(|i| i.intf_type == MediaIntfT::V4LVideo)
            .find(|i| find_path_by_devnode(i.devnode.major, i.devnode.minor)
                .and_then(v4l::Device::with_path)
                .and_then(|dev| dev.query_caps())
                .map(|caps| !(
                    caps.capabilities
                        & (v4l::capability::Flags::VIDEO_CAPTURE | v4l::capability::Flags::STREAMING)
                    ).is_empty())
                .unwrap_or(false) // This drops errors in opening devices silently.
            )
            .ok_or(io::Error::other("No V4lVideo interface"))?;
        find_path_by_devnode(video_interface.devnode.major, video_interface.devnode.minor)
    }
}


fn find_path_by_devnode(major:u32, minor: u32) -> Result<PathBuf, io::Error> {
    let mut data = String::new();
    File::open(format!("/sys/dev/char/{}:{}/uevent", major, minor))?
        .read_to_string(&mut data)?;
    let devline = data.lines()
        .find(|line| line.starts_with("DEVNAME="))
        .ok_or(io::Error::other("File does not contain a \"DEVNAME=\" line"))?;
    Ok(format!("/dev/{}", devline.split_at("DEVNAME=".len()).1).into())
}