Skip to content

Commit e693bc6

Browse files
committed
stability update
1 parent cc1f054 commit e693bc6

9 files changed

Lines changed: 1210 additions & 38 deletions

File tree

Cargo.lock

Lines changed: 1070 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
[package]
2-
name = "system_media"
2+
name = "system-media"
33
version = "0.1.0"
44
edition = "2024"
55

66
authors = ["William Gibbs <[email protected]>"]
77
description = "Rust crate for interacting with the NowPlaying API"
88
license = "MIT"
9-
repository = "https://github.com/wgibbs-rs/system_media"
9+
repository = "https://github.com/RustAudio/system-media"
1010
readme = "README.md"
1111
keywords = ["rust", "swift", "macos", "audio", "video"]
1212
categories = ["external-ffi-bindings", "multimedia::audio", "multimedia::video"]
13+
14+
[dependencies]
15+
image = "0.25.6"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
# system_media
1+
# system-media
22
Rust library for creating and interacting with system media sessions

build.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ fn main() {
1616
"MediaPlayer",
1717
"-framework",
1818
"AVFoundation",
19+
"-framework",
20+
"AppKit",
21+
"-framework",
22+
"Cocoa",
1923
"-o",
2024
&lib_path,
2125
swift_file,

examples/basic.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,23 @@
11
use system_media::MediaSession;
2+
use std::fs;
3+
use std::thread;
4+
use std::time::Duration;
25

36
fn main() {
47
println!("Now Playing session started");
58
println!("Keeping process alive... Press Ctrl+C to exit");
69
let mut session = MediaSession::new();
7-
session.set_title("Hello!");
10+
11+
// let manifest_dir = env!("CARGO_MANIFEST_DIR");
12+
// let path = format!("{}/examples/bug.png", manifest_dir);
13+
14+
session.set_playback_rate(1.0);
15+
session.set_playback_duration(300.0);
16+
session.set_elapsed_duration(100.0);
17+
18+
session.set_title("Help ME!");
19+
20+
// session.set_image(&path);
21+
822
session.start();
923
}

examples/bug.png

1.58 MB
Loading

src/lib.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ mod macos;
44
mod null;
55

66
use std::sync::{Arc, Mutex};
7+
use std::error::Error;
8+
use image::ImageReader;
79

810
pub trait MediaBackend {
911
fn set_title(&self, title: &str);
1012
fn set_artist(&self, artist: &str);
1113
fn set_album(&self, album: &str);
1214
fn set_genre(&self, genre: &str);
15+
fn set_image(&self, path: &str);
1316
fn set_media_type(&self, media_type: MediaType);
1417
fn set_playback_duration(&self, duration: f64);
1518
fn set_elapsed_duration(&self, duration: f64);
@@ -30,11 +33,19 @@ pub struct Metadata {
3033
artist: String,
3134
album: String,
3235
genre: String,
36+
image_path: String,
3337
media_type: MediaType,
3438
duration: f64,
3539
playback_rate: f64,
3640
}
3741

42+
fn decode_image(path: &str) -> Result<Vec<u8>, Box<dyn Error>> {
43+
let img = ImageReader::open(path)?
44+
.decode()?
45+
.to_rgb8();
46+
Ok(img.into_raw())
47+
}
48+
3849
pub struct MediaSession {
3950
backend: Box<dyn MediaBackend>,
4051
metadata: Arc<Mutex<Metadata>>,
@@ -55,6 +66,7 @@ impl MediaSession {
5566
artist: "".to_string(),
5667
album: "".to_string(),
5768
genre: "".to_string(),
69+
image_path: "".to_string(),
5870
media_type: MediaType::Audio,
5971
duration: 0.0,
6072
playback_rate: 0.0,
@@ -102,6 +114,17 @@ impl MediaSession {
102114
md.genre.clone()
103115
}
104116

117+
pub fn set_image(&mut self, path: &str) -> Result<(), Box<dyn Error>> {
118+
Arc::clone(&self.metadata).lock().unwrap().image_path = path.to_string();
119+
self.backend.set_image(path);
120+
Ok(())
121+
}
122+
123+
pub fn image(&self) -> String {
124+
let md = self.metadata.lock().unwrap();
125+
md.image_path.clone()
126+
}
127+
105128
pub fn set_media_type(&mut self, media_type: MediaType) {
106129
Arc::clone(&self.metadata).lock().unwrap().media_type = media_type;
107130
self.backend.set_media_type(media_type);

src/macos/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
use crate::{MediaBackend, MediaType};
22
use std::ffi::CString;
3+
use std::thread;
34

45
// Swift functions called by Rust
56
unsafe extern "C" {
67
fn swift_set_metadata_title(title: *mut i8);
78
fn swift_set_metadata_artist(artist: *mut i8);
89
fn swift_set_metadata_album(album: *mut i8);
910
fn swift_set_metadata_genre(genre: *mut i8);
11+
fn swift_set_metadata_image(bytes: *const u8, length: usize);
1012
fn swift_set_metadata_media_type(id: i64);
1113
fn swift_set_playback_duration(seconds: f64);
1214
fn swift_set_elapsed_duration(seconds: f64);
@@ -62,6 +64,13 @@ impl MediaBackend for NowPlayingBackend {
6264
swift_set_metadata_genre(str_to_raw(genre));
6365
}
6466
}
67+
68+
fn set_image(&self, path: &str) {
69+
let data = std::fs::read(path).unwrap();
70+
unsafe {
71+
swift_set_metadata_image(data.as_ptr(), data.len());
72+
}
73+
}
6574

6675
fn set_media_type(&self, media_type: MediaType) {
6776
unsafe {

src/macos/nowplaying.swift

Lines changed: 83 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import Foundation
22
import MediaPlayer
33
import AVFoundation
4+
import AppKit
5+
import Cocoa
46

57
@_silgen_name("rust_resume_playback_command")
68
public func rustStartPlaybackCommand()
@@ -16,73 +18,121 @@ public func rustPreviousTrackCommand()
1618

1719
@_cdecl("swift_set_metadata_title")
1820
public func setMetadataTitle(title : UnsafePointer<CChar>) {
19-
let s = String(cString: title)
20-
var nowPlayingInfo = [String: Any]()
21-
nowPlayingInfo[MPMediaItemPropertyTitle] = s
22-
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
21+
DispatchQueue.main.async {
22+
let s = String(cString: title)
23+
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
24+
nowPlayingInfo[MPMediaItemPropertyTitle] = s
25+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
26+
}
2327
}
2428

2529
@_cdecl("swift_set_metadata_artist")
2630
public func setMetadataArtist(artist : UnsafePointer<CChar>) {
27-
let s = String(cString: artist)
28-
var nowPlayingInfo = [String: Any]()
29-
nowPlayingInfo[MPMediaItemPropertyArtist] = s
30-
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
31+
DispatchQueue.main.async {
32+
let s = String(cString: artist)
33+
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
34+
nowPlayingInfo[MPMediaItemPropertyArtist] = s
35+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
36+
}
3137
}
3238

3339
@_cdecl("swift_set_metadata_album")
3440
public func setMetadataAlbumTitle(album : UnsafePointer<CChar>) {
35-
let s = String(cString: album)
36-
var nowPlayingInfo = [String: Any]()
37-
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = s
38-
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
41+
DispatchQueue.main.async {
42+
let s = String(cString: album)
43+
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
44+
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = s
45+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
46+
}
3947
}
4048

4149
@_cdecl("swift_set_metadata_genre")
4250
public func setMetadataGenre(genre : UnsafePointer<CChar>) {
43-
let s = String(cString: genre)
44-
var nowPlayingInfo = [String: Any]()
45-
nowPlayingInfo[MPMediaItemPropertyGenre] = s
46-
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
51+
DispatchQueue.main.async {
52+
let s = String(cString: genre)
53+
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
54+
nowPlayingInfo[MPMediaItemPropertyGenre] = s
55+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
56+
}
57+
}
58+
59+
func resize(image: NSImage, to newSize: NSSize) -> NSImage {
60+
let resizedImage = NSImage(size: newSize)
61+
resizedImage.lockFocus()
62+
defer { resizedImage.unlockFocus() }
63+
64+
image.draw(
65+
in: NSRect(origin: .zero, size: newSize),
66+
from: NSRect(origin: .zero, size: image.size),
67+
operation: .copy,
68+
fraction: 1.0
69+
)
70+
71+
return resizedImage
72+
}
73+
74+
@_cdecl("swift_set_metadata_image")
75+
public func setMetadataImage(bytes: UnsafePointer<UInt8>, length: Int) {
76+
DispatchQueue.main.async {
77+
let data = Data(bytes: bytes, count: length)
78+
guard let image = NSImage(data: data) else {
79+
print("Failed to convert data to NSImage")
80+
return
81+
}
82+
let scaledImg = resize(image: image, to: NSSize(width: 512, height: 512))
83+
let artwork = MPMediaItemArtwork(boundsSize: image.size) { _ in
84+
return scaledImg
85+
}
86+
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
87+
nowPlayingInfo[MPMediaItemPropertyArtwork] = artwork
88+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
89+
}
4790
}
4891

4992
@_cdecl("swift_set_metadata_media_type")
5093
public func setMetadataMediaType(id: Int) {
51-
var nowPlayingInfo = [String: Any]()
52-
if (id == 0) {
53-
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.audio.rawValue
54-
} else {
55-
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue
94+
DispatchQueue.main.async {
95+
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
96+
if (id == 0) {
97+
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.audio.rawValue
98+
} else {
99+
nowPlayingInfo[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue
100+
}
101+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
56102
}
57-
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
58103
}
59104

60105
@_cdecl("swift_set_playback_duration")
61106
public func setPlaybackDuration(seconds : Double) {
62-
var nowPlayingInfo = [String: Any]()
63-
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = seconds
64-
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
107+
DispatchQueue.main.async {
108+
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
109+
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = seconds
110+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
111+
}
65112
}
66113

67114
@_cdecl("swift_set_elapsed_duration")
68115
public func setElapsedPlaybackTime(seconds : Double) {
69-
var nowPlayingInfo = [String: Any]()
70-
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = seconds
71-
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
116+
DispatchQueue.main.async {
117+
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
118+
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = seconds
119+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
120+
}
72121
}
73122

74123
@_cdecl("swift_set_playback_rate")
75124
public func setPlaybackRate(rate : Double) {
76-
var nowPlayingInfo = [String: Any]()
77-
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate
78-
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
125+
DispatchQueue.main.async {
126+
var nowPlayingInfo = MPNowPlayingInfoCenter.default().nowPlayingInfo ?? [String: Any]()
127+
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = rate
128+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
129+
}
79130
}
80131

81132
@_cdecl("swift_start_session")
82133
public func startSession() {
83134
setupRemoteCommandTargets() // Create event hooks for media session.
84-
setPlaybackRate(rate: 0.0) // Ensures NowPlaying is properly displayed.
85-
RunLoop.main.run() // Loop the thread indefinitely; until killed.
135+
RunLoop.main.run() // Loop the thread indefinitely until killed.
86136
}
87137

88138
private func setupRemoteCommandTargets() {

0 commit comments

Comments
 (0)