Ely Office: six offline-first desktop apps in Rust and GPUI
ElyDoc, ElyTable, ElyShow (LibreOfficeKit), ElyPDF (MuPDF), ElyTree (Git), ElyMark (Milkdown and CodeMirror in CEF). ElyMail and ElyAgent follow.
This commit is contained in:
817 files changed
+143101
No files matched your search
@@ -0,0 +1,126 @@
|
||||
use std::path::Path;
|
||||
|
||||
use objc2::{AllocAnyThread, MainThreadMarker, rc::Retained, runtime::AnyObject};
|
||||
use objc2_av_foundation::{
|
||||
AVPlayer, AVPlayerItem, AVPlayerItemOutput, AVPlayerItemStatus, AVPlayerItemVideoOutput,
|
||||
};
|
||||
use objc2_core_media::{CMTime, CMTimeFlags};
|
||||
use objc2_core_video::kCVPixelBufferPixelFormatTypeKey;
|
||||
use objc2_foundation::{NSDictionary, NSNumber, NSString, NSURL};
|
||||
|
||||
use super::State;
|
||||
|
||||
/// Biplanar full-range YCbCr, as GPUI's surfaces draw.
|
||||
const PIXELS: u32 = u32::from_be_bytes(*b"420f");
|
||||
|
||||
pub struct Player {
|
||||
player: Retained<AVPlayer>,
|
||||
item: Retained<AVPlayerItem>,
|
||||
output: Retained<AVPlayerItemVideoOutput>,
|
||||
}
|
||||
|
||||
fn seconds(time: CMTime) -> Option<f64> {
|
||||
(time.flags.contains(CMTimeFlags::Valid) && !time.flags.contains(CMTimeFlags::Indefinite))
|
||||
.then(|| time.value as f64 / time.timescale as f64)
|
||||
}
|
||||
|
||||
impl Player {
|
||||
pub fn open(path: &Path) -> anyhow::Result<Self> {
|
||||
let mtm = MainThreadMarker::new()
|
||||
.ok_or_else(|| anyhow::anyhow!("media plays on the main thread"))?;
|
||||
let url = NSURL::fileURLWithPath(&NSString::from_str(&path.to_string_lossy()));
|
||||
let player = unsafe { AVPlayer::playerWithURL(&url, mtm) };
|
||||
let item = unsafe { player.currentItem() }
|
||||
.ok_or_else(|| anyhow::anyhow!("no player item for {}", path.display()))?;
|
||||
// The key is a CFString; NSString is its toll-free twin.
|
||||
let key: &NSString =
|
||||
unsafe { &*(kCVPixelBufferPixelFormatTypeKey as *const _ as *const NSString) };
|
||||
let format = NSNumber::new_u32(PIXELS);
|
||||
let value: &AnyObject = &format;
|
||||
let attributes = NSDictionary::<NSString, AnyObject>::from_slices(&[key], &[value]);
|
||||
let output = unsafe {
|
||||
AVPlayerItemVideoOutput::initWithPixelBufferAttributes(
|
||||
AVPlayerItemVideoOutput::alloc(),
|
||||
Some(&attributes),
|
||||
)
|
||||
};
|
||||
unsafe { item.addOutput(&output as &AVPlayerItemOutput) };
|
||||
Ok(Player {
|
||||
player,
|
||||
item,
|
||||
output,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn state(&self) -> State {
|
||||
match unsafe { self.item.status() } {
|
||||
AVPlayerItemStatus::ReadyToPlay => {
|
||||
let size = unsafe { self.item.presentationSize() };
|
||||
let video = (size.width > 0.0 && size.height > 0.0)
|
||||
.then(|| (size.width.round() as u32, size.height.round() as u32));
|
||||
State::Ready {
|
||||
seconds: seconds(unsafe { self.item.duration() }).unwrap_or(0.0),
|
||||
video,
|
||||
}
|
||||
}
|
||||
AVPlayerItemStatus::Failed => {
|
||||
let reason = unsafe { self.item.error() }
|
||||
.map(|error| error.localizedDescription().to_string())
|
||||
.unwrap_or_else(|| "the system player gave no reason".into());
|
||||
State::Failed(reason)
|
||||
}
|
||||
_ => State::Loading,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn play(&self) {
|
||||
unsafe { self.player.play() }
|
||||
}
|
||||
|
||||
pub fn pause(&self) {
|
||||
unsafe { self.player.pause() }
|
||||
}
|
||||
|
||||
pub fn playing(&self) -> bool {
|
||||
(unsafe { self.player.rate() }) > 0.0
|
||||
}
|
||||
|
||||
pub fn position(&self) -> f64 {
|
||||
seconds(unsafe { self.player.currentTime() }).unwrap_or(0.0)
|
||||
}
|
||||
|
||||
pub fn seek(&self, to: f64) {
|
||||
let time = CMTime {
|
||||
value: (to * 600.0).round() as i64,
|
||||
timescale: 600,
|
||||
flags: CMTimeFlags::Valid,
|
||||
epoch: 0,
|
||||
};
|
||||
let exact = CMTime {
|
||||
value: 0,
|
||||
timescale: 1,
|
||||
flags: CMTimeFlags::Valid,
|
||||
epoch: 0,
|
||||
};
|
||||
unsafe {
|
||||
self.player
|
||||
.seekToTime_toleranceBefore_toleranceAfter(time, exact, exact)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn frame(&self) -> Option<super::Frame> {
|
||||
let now = unsafe { self.item.currentTime() };
|
||||
if !unsafe { self.output.hasNewPixelBufferForItemTime(now) } {
|
||||
return None;
|
||||
}
|
||||
let buffer = unsafe {
|
||||
self.output
|
||||
.copyPixelBufferForItemTime_itemTimeForDisplay(now, std::ptr::null_mut())
|
||||
}?;
|
||||
let raw = Retained::into_raw(buffer);
|
||||
// The copy hands its one reference over.
|
||||
Some(unsafe {
|
||||
<core_video::pixel_buffer::CVPixelBuffer as core_foundation::base::TCFType>::wrap_under_create_rule(raw.cast())
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user