[vlc-devel] [PATCH 2/2] stream filter: Add rust based cuesheet module
Kartik Ohri
kartikohri13 at gmail.com
Sat Sep 19 23:09:34 CEST 2020
The module is written using Rust API and serves as
a proof of concept for the same.
---
configure.ac | 4 +-
modules/Cargo.toml | 2 +
modules/demux/Makefile.am | 28 +++
modules/demux/playlist/cuesheet.c | 160 ++++++++++++++++++
modules/demux/playlist/cuesheet/Cargo.toml | 14 ++
modules/demux/playlist/cuesheet/cbindgen.toml | 16 ++
modules/demux/playlist/cuesheet/src/capi.rs | 114 +++++++++++++
modules/demux/playlist/cuesheet/src/config.rs | 106 ++++++++++++
modules/demux/playlist/cuesheet/src/lib.rs | 5 +
modules/demux/playlist/cuesheet/src/parse.rs | 75 ++++++++
10 files changed, 521 insertions(+), 3 deletions(-)
create mode 100644 modules/Cargo.toml
create mode 100644 modules/demux/playlist/cuesheet.c
create mode 100644 modules/demux/playlist/cuesheet/Cargo.toml
create mode 100644 modules/demux/playlist/cuesheet/cbindgen.toml
create mode 100644 modules/demux/playlist/cuesheet/src/capi.rs
create mode 100644 modules/demux/playlist/cuesheet/src/config.rs
create mode 100644 modules/demux/playlist/cuesheet/src/lib.rs
create mode 100644 modules/demux/playlist/cuesheet/src/parse.rs
diff --git a/configure.ac b/configure.ac
index 53d56a725c..a9d25a505a 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1899,15 +1899,13 @@ AM_CONDITIONAL([ENABLE_SOUT], [test "${enable_sout}" != "no"])
dnl Rust Modules
AC_ARG_ENABLE([rust],
AS_HELP_STRING([--enable-rust], [enable building Rust modules (default disabled)]))
-AS_IF([test "${enable_rust}" = "yes"],
- [AC_DEFINE(ENABLE_RUST, 1, [Define to 1 for building rust modules.])])
-AM_CONDITIONAL([BUILD_RUST], [test "${enable_rust}" = "yes"])
if test "${enable_rust}" = "yes"
then
AC_CHECK_PROG(CARGO, [cargo], [yes], [no])
AS_IF([test "x$CARGO" = "xno"],
AC_MSG_ERROR([cargo not found. cargo is required to build rust modules]))
fi
+AM_CONDITIONAL([BUILD_RUST], [test "x$CARGO" = "xyes"])
dnl Lua modules
AC_ARG_ENABLE([lua],
diff --git a/modules/Cargo.toml b/modules/Cargo.toml
new file mode 100644
index 0000000000..0606311bda
--- /dev/null
+++ b/modules/Cargo.toml
@@ -0,0 +1,2 @@
+[workspace]
+members = ["demux/playlist/cuesheet"]
diff --git a/modules/demux/Makefile.am b/modules/demux/Makefile.am
index 675b2d10af..c808eee3a8 100644
--- a/modules/demux/Makefile.am
+++ b/modules/demux/Makefile.am
@@ -1,3 +1,4 @@
+
demuxdir = $(pluginsdir)/demux
demux_LTLIBRARIES =
@@ -256,6 +257,33 @@ libplaylist_plugin_la_SOURCES = \
demux/playlist/playlist.c demux/playlist/playlist.h
demux_LTLIBRARIES += libplaylist_plugin.la
+if BUILD_RUST
+CARGO_C = cargo capi install --release
+RUST_RUNTIME_LIBS = -ldl -lrt -lpthread -lgcc_s -lc -lm -lrt -lpthread -lutil
+libcuesheet_rust_plugin_la_SOURCES = \
+ demux/playlist/cuesheet/src/capi.rs \
+ demux/playlist/cuesheet/src/config.rs \
+ demux/playlist/cuesheet/src/lib.rs \
+ demux/playlist/cuesheet/src/parse.rs \
+ $(RUST_API)
+
+abs_src_dir = $(shell readlink -f $(srcdir))
+
+ at abs_top_builddir@/modules/demux/playlist/cuesheet/cuesheet.h: $(libcuesheet_rust_plugin_la_SOURCES)
+ mkdir -p demux/playlist/cuesheet && cd demux/playlist/cuesheet && \
+ $(CARGO_C) --library-type=staticlib --destdir=. --includedir=.. --libdir=. \
+ --manifest-path=$(abs_src_dir)/demux/playlist/cuesheet/Cargo.toml
+
+BUILT_SOURCES += @abs_top_builddir@/modules/demux/playlist/cuesheet/cuesheet.h
+CUESHEET_LIBS = @abs_top_builddir@/modules/demux/playlist/cuesheet/libcuesheet.a
+CUESHEET_LIBS += $(RUST_RUNTIME_LIBS)
+libcuesheet_plugin_la_CFLAGS = $(AM_CFLAGS) -I at abs_top_builddir@/modules/demux/playlist
+libcuesheet_plugin_la_SOURCES = demux/playlist/cuesheet.c @abs_top_builddir@/modules/demux/playlist/cuesheet/cuesheet.h
+libcuesheet_plugin_la_LIBADD = $(CUESHEET_LIBS)
+
+noinst_LTLIBRARIES += libcuesheet_plugin.la
+endif
+
libts_plugin_la_SOURCES = demux/mpeg/ts.c demux/mpeg/ts.h \
demux/mpeg/ts_pid.h demux/mpeg/ts_pid_fwd.h demux/mpeg/ts_pid.c \
demux/mpeg/ts_psi.h demux/mpeg/ts_psi.c \
diff --git a/modules/demux/playlist/cuesheet.c b/modules/demux/playlist/cuesheet.c
new file mode 100644
index 0000000000..83e0d5fc9c
--- /dev/null
+++ b/modules/demux/playlist/cuesheet.c
@@ -0,0 +1,160 @@
+/*****************************************************************************
+ * cuesheet.c : cue sheet playlist import format
+ *****************************************************************************
+ * Copyright (C) 2020 VLC authors and VideoLAN
+ *
+ * Authors: Kartik Ohri <kartikohri13 at gmail.com>
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+#ifdef HAVE_CONFIG_H
+# include "config.h"
+#endif
+
+#include <string.h>
+
+#include <vlc_common.h>
+#include <vlc_plugin.h>
+#include <vlc_access.h>
+#include <vlc_url.h>
+
+#include "cuesheet/cuesheet.h"
+#include "playlist.h"
+
+typedef struct {
+ struct CCuesheet *cuesheet;
+} cuesheet_sys_t;
+
+int Import_Cue_Sheet(vlc_object_t *);
+static int ReadDir(stream_t *, input_item_node_t *);
+
+int Import_Cue_Sheet( vlc_object_t *p_this )
+{
+ stream_t *p_demux = (stream_t *) p_this;
+ CHECK_FILE ( p_demux );
+
+ if ( !stream_IsMimeType ( p_demux->s, "application/x-cue" )
+ && !stream_HasExtension( p_demux->s, ".cue" ))
+ return VLC_EGENERIC;
+
+ p_demux->pf_control = access_vaDirectoryControlHelper;
+ p_demux->pf_readdir = ReadDir;
+ return VLC_SUCCESS;
+}
+
+static int ReadDir(stream_t* p_demux, input_item_node_t* p_subitems)
+{
+ int i_ret = VLC_EGENERIC;
+ CCuesheet *cuesheet = cuesheet_from_demux(p_demux->s);
+ if(!cuesheet)
+ return i_ret;
+
+ int tracks = cuesheet_get_tracks_number(cuesheet);
+ int idx = 0;
+ char *psz_val = NULL;
+ char *ppsz_option[2];
+
+ char *psz_album = cuesheet_get_property(cuesheet, "TITLE");
+ char *psz_album_artist = cuesheet_get_property(cuesheet, "PERFORMER");
+ char *psz_description = cuesheet_get_comment_value(cuesheet, "COMMENT");
+ char *psz_genre = cuesheet_get_comment_value(cuesheet, "GENRE");
+ char *psz_date = cuesheet_get_comment_value(cuesheet, "DATE");
+
+ char *psz_audio_file = cuesheet_get_property(cuesheet, "FILE_NAME");
+ if (!psz_audio_file)
+ goto end;
+
+ char *psz_audio_file_uri = vlc_uri_encode(psz_audio_file);
+ char *psz_url = vlc_uri_resolve(p_demux->psz_url, psz_audio_file_uri);
+ free(psz_audio_file);
+ free(psz_audio_file_uri);
+
+ while ( idx < tracks )
+ {
+ int i_options = 0;
+ input_item_t *p_item;
+
+ psz_val = cuesheet_get_track_property(cuesheet, idx, "TITLE");
+ if (psz_val)
+ {
+ p_item = input_item_New(psz_url, psz_val);
+ p_item->i_type = ITEM_TYPE_FILE;
+ free(psz_val);
+ }
+ else
+ goto end;
+
+ if (psz_album)
+ input_item_SetAlbum(p_item, psz_album);
+
+ if (psz_album_artist)
+ input_item_SetAlbumArtist(p_item, psz_album_artist);
+
+ if (psz_description)
+ input_item_SetDescription(p_item, psz_description);
+
+ if (psz_date)
+ input_item_SetDate(p_item, psz_date);
+
+ if (psz_genre)
+ input_item_SetGenre(p_item, psz_genre);
+
+ psz_val = cuesheet_get_track_property(cuesheet, idx, "PERFORMER");
+ if (psz_val)
+ {
+ input_item_SetArtist(p_item, psz_val);
+ free(psz_val);
+ }
+
+ int i_time = cuesheet_get_track_start(cuesheet, idx);
+ if (asprintf(ppsz_option, ":start-time=%d", i_time))
+ i_options++;
+
+ if (idx < tracks - 1)
+ {
+ i_time = cuesheet_get_track_start(cuesheet, idx + 1);
+ if (asprintf(ppsz_option + i_options, ":stop-time=%d", i_time))
+ i_options++;
+ }
+
+ input_item_AddOptions(p_item, i_options, (const char **)ppsz_option, VLC_INPUT_OPTION_TRUSTED);
+ input_item_node_AppendItem(p_subitems, p_item);
+ input_item_Release(p_item);
+ idx++;
+ }
+
+ i_ret = VLC_SUCCESS;
+
+ end:
+ free(psz_album);
+ free(psz_album_artist);
+ free(psz_description);
+ free(psz_genre);
+ free(psz_date);
+ unref_cuesheet(cuesheet);
+ return i_ret;
+}
+
+vlc_module_begin()
+ add_shortcut( "playlist" )
+ set_category( CAT_INPUT )
+ set_subcategory( SUBCAT_INPUT_DEMUX )
+ set_description ( N_("Cue Sheet importer") )
+ set_capability ( "stream_filter", 320 )
+ set_callback ( Import_Cue_Sheet )
+vlc_module_end()
+
+
diff --git a/modules/demux/playlist/cuesheet/Cargo.toml b/modules/demux/playlist/cuesheet/Cargo.toml
new file mode 100644
index 0000000000..fac05ec4f3
--- /dev/null
+++ b/modules/demux/playlist/cuesheet/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "cuesheet"
+version = "0.1.0"
+authors = ["Kartik Ohri <kartikohri13 at gmail.com>"]
+edition = "2018"
+license = "LGPL-2.1-or-later"
+
+[dependencies]
+vlccore-rs = {path = "../../../../src/vlccore-rs"}
+vlccore-sys = {path = "../../../../src/vlccore-sys"}
+libc = "0.2"
+
+[lib]
+name = "cuesheet"
\ No newline at end of file
diff --git a/modules/demux/playlist/cuesheet/cbindgen.toml b/modules/demux/playlist/cuesheet/cbindgen.toml
new file mode 100644
index 0000000000..cb6ba8e554
--- /dev/null
+++ b/modules/demux/playlist/cuesheet/cbindgen.toml
@@ -0,0 +1,16 @@
+header = "// SPDX-License-Identifier: LGPL-2.1-or-later"
+sys_includes = ["stddef.h", "stdint.h", "stdlib.h"]
+no_includes = true
+include_guard = "CUESHEET_H"
+tab_width = 4
+language = "C"
+documentation_style = "C"
+style = "type"
+cpp_compat = true
+
+[export]
+item_types = ["enums", "structs", "unions", "typedefs", "opaque", "functions"]
+
+[enum]
+rename_variants = "ScreamingSnakeCase"
+prefix_with_name = true
\ No newline at end of file
diff --git a/modules/demux/playlist/cuesheet/src/capi.rs b/modules/demux/playlist/cuesheet/src/capi.rs
new file mode 100644
index 0000000000..ad1dfef94f
--- /dev/null
+++ b/modules/demux/playlist/cuesheet/src/capi.rs
@@ -0,0 +1,114 @@
+use crate::config::Cuesheet;
+use libc::{c_char, c_int};
+use std::convert::TryFrom;
+use std::ffi::{CStr, CString};
+use std::io::{BufRead, BufReader};
+use std::mem::ManuallyDrop;
+use std::ptr::null_mut;
+
+use vlccore_rs::stream;
+use vlccore_rs::utils::convert_string_to_ptr;
+use vlccore_sys::stream::stream_t;
+
+pub struct CCuesheet {
+ cuesheet: Cuesheet,
+}
+
+#[no_mangle]
+pub extern "C" fn cuesheet_from_demux(s: *mut stream_t) -> *mut CCuesheet {
+ let mut stream = s.into();
+ let ptr = rust_cuesheet_from_demux(&mut stream)
+ .map(|cuesheet| Box::new(CCuesheet { cuesheet }))
+ .map(|c_cuesheet| Box::into_raw(c_cuesheet))
+ .unwrap_or(null_mut());
+ let _ = ManuallyDrop::new(stream);
+ ptr
+}
+
+fn rust_cuesheet_from_demux(stream: &mut stream::Stream) -> Option<Cuesheet> {
+ let mut cuesheet = Cuesheet::default();
+ let reader = BufReader::new(stream);
+ for line in reader.lines() {
+ match line {
+ Ok(l) => cuesheet.process_line(l.as_str())?,
+ Err(_) => return None,
+ }
+ }
+ Some(cuesheet)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn cuesheet_get_property(
+ c_cuesheet: *mut CCuesheet,
+ line: *const c_char,
+) -> *mut c_char {
+ let cuesheet = &(*c_cuesheet).cuesheet;
+
+ CStr::from_ptr(line)
+ .to_str()
+ .ok()
+ .and_then(|line| cuesheet.get_property(line))
+ .map(|property| convert_string_to_ptr(property.to_string()))
+ .unwrap_or(null_mut())
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn cuesheet_get_comment_value(
+ c_cuesheet: *mut CCuesheet,
+ line: *const c_char,
+) -> *mut c_char {
+ let cuesheet = &(*c_cuesheet).cuesheet;
+
+ CStr::from_ptr(line)
+ .to_str()
+ .ok()
+ .and_then(|line| cuesheet.comments.get(line))
+ .map(|comment| convert_string_to_ptr(comment.to_string()))
+ .unwrap_or(null_mut())
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn cuesheet_get_tracks_number(c_cuesheet: *mut CCuesheet) -> c_int {
+ (*c_cuesheet).cuesheet.tracks.len() as i32
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn cuesheet_get_track_property(
+ c_cuesheet: *mut CCuesheet,
+ index: c_int,
+ line: *const c_char,
+) -> *mut c_char {
+ let cuesheet = &(*c_cuesheet).cuesheet;
+
+ usize::try_from(index)
+ .ok()
+ .and_then(|index| cuesheet.get_track_at_index(index))
+ .zip(CStr::from_ptr(line).to_str().ok())
+ .and_then(|(track, property)| track.get_property(property))
+ .map(|value| convert_string_to_ptr(value.to_string()))
+ .unwrap_or(null_mut())
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn cuesheet_get_track_start(
+ c_cuesheet: *mut CCuesheet,
+ index: c_int,
+) -> c_int {
+ let cuesheet = &(*c_cuesheet).cuesheet;
+
+ usize::try_from(index)
+ .ok()
+ .and_then(|index| cuesheet.get_track_at_index(index))
+ .map(|track| track.begin_time_in_seconds())
+ .unwrap_or(-1)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn unref_cuesheet_string(text: *mut c_char) {
+ let _ = CString::from_raw(text);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn unref_cuesheet(cuesheet: *mut CCuesheet) {
+ let _ = Box::from_raw(cuesheet);
+}
diff --git a/modules/demux/playlist/cuesheet/src/config.rs b/modules/demux/playlist/cuesheet/src/config.rs
new file mode 100644
index 0000000000..24ebbbc7a4
--- /dev/null
+++ b/modules/demux/playlist/cuesheet/src/config.rs
@@ -0,0 +1,106 @@
+use std::borrow::Borrow;
+use std::collections::HashMap;
+
+#[derive(Clone, Debug)]
+pub struct CuesheetTrack {
+ pub item_type: String,
+ pub item_position: u8,
+ pub item_performer: String,
+ pub item_title: String,
+ pub item_begin_duration: String,
+}
+
+impl Default for CuesheetTrack {
+ fn default() -> Self {
+ CuesheetTrack {
+ item_type: "".to_owned(),
+ item_position: 0,
+ item_performer: "".to_string(),
+ item_title: "".to_string(),
+ item_begin_duration: "".to_string(),
+ }
+ }
+}
+
+fn add_to_start_time(
+ start_time: i32,
+ time_to_add: Option<&str>,
+ conversion_factor: i32,
+) -> Option<i32> {
+ let val = time_to_add?.parse::<u32>().ok()?;
+ Some(start_time + val as i32 * conversion_factor)
+}
+
+impl CuesheetTrack {
+ pub fn begin_time_in_seconds(&self) -> i32 {
+ let start: &str = &self.item_begin_duration;
+ let mut duration_parts: Vec<&str> = start.split(":").collect();
+
+ let frame_rate = 75;
+ let convert_min_to_s = 60;
+
+ duration_parts
+ .pop()
+ .and_then(|part| part.parse::<u32>().ok())
+ .map(|part| part / frame_rate)
+ .and_then(|start| add_to_start_time(start as i32, duration_parts.pop(), 1))
+ .and_then(|start| {
+ add_to_start_time(start as i32, duration_parts.pop(), convert_min_to_s)
+ })
+ .unwrap_or(-1)
+ }
+
+ pub fn get_property(&self, key: &str) -> Option<&str> {
+ match key.to_ascii_lowercase().as_str() {
+ "type" => Some(self.item_type.as_str()),
+ "begin_duration" => Some(self.item_begin_duration.as_str()),
+ "performer" => Some(self.item_performer.as_str()),
+ "title" => Some(self.item_title.as_str()),
+ _ => None,
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct Cuesheet {
+ pub file_name: String,
+ pub file_type: String,
+ pub tracks: Vec<CuesheetTrack>,
+ pub performer: String,
+ pub title: String,
+ pub comments: HashMap<String, String>,
+ pub is_processing_tracks: bool,
+}
+
+impl Default for Cuesheet {
+ fn default() -> Self {
+ Cuesheet {
+ file_name: "".to_string(),
+ file_type: "".to_string(),
+ tracks: Vec::new(),
+ performer: "".to_string(),
+ title: "".to_string(),
+ comments: HashMap::new(),
+ is_processing_tracks: false,
+ }
+ }
+}
+
+impl Cuesheet {
+ pub fn get_track_at_index(&self, index: usize) -> Option<&CuesheetTrack> {
+ if index >= self.tracks.len() {
+ return None;
+ }
+ Some(self.tracks[index].borrow())
+ }
+
+ pub fn get_property(&self, key: &str) -> Option<&str> {
+ match key.to_ascii_lowercase().as_str() {
+ "file_name" => Some(self.file_name.as_str()),
+ "file_type" => Some(self.file_type.as_str()),
+ "performer" => Some(self.performer.as_str()),
+ "title" => Some(self.title.as_str()),
+ _ => None,
+ }
+ }
+}
diff --git a/modules/demux/playlist/cuesheet/src/lib.rs b/modules/demux/playlist/cuesheet/src/lib.rs
new file mode 100644
index 0000000000..c43541e34f
--- /dev/null
+++ b/modules/demux/playlist/cuesheet/src/lib.rs
@@ -0,0 +1,5 @@
+mod config;
+mod parse;
+
+#[cfg(cargo_c)]
+mod capi;
diff --git a/modules/demux/playlist/cuesheet/src/parse.rs b/modules/demux/playlist/cuesheet/src/parse.rs
new file mode 100644
index 0000000000..b570638325
--- /dev/null
+++ b/modules/demux/playlist/cuesheet/src/parse.rs
@@ -0,0 +1,75 @@
+use crate::config::*;
+
+pub fn parse_line(line: &str) -> Vec<String> {
+ let characters = line.trim().chars();
+ let mut parts: Vec<String> = Vec::new();
+ let mut inside_quotes: bool = false;
+ let mut temp_str: String = String::new();
+ for char in characters {
+ if char == '"' {
+ inside_quotes = !inside_quotes;
+ } else if inside_quotes {
+ temp_str.push(char);
+ } else if char.is_whitespace() {
+ parts.push(temp_str.clone());
+ temp_str.clear();
+ } else {
+ temp_str.push(char);
+ }
+ }
+ parts.push(temp_str.clone());
+ parts
+}
+
+impl Cuesheet {
+ pub fn process_line(&mut self, line: &str) -> Option<()> {
+ let parts = parse_line(line);
+ let keyword = parts[0].as_str();
+ if !self.is_processing_tracks {
+ match keyword {
+ "FILE" => {
+ self.file_name = parts[1].to_owned();
+ self.file_type = parts[2].to_owned();
+ }
+ "TITLE" => self.title = parts[1].to_owned(),
+ "PERFORMER" => self.performer = parts[1].to_owned(),
+ "REM" => {
+ self.comments
+ .insert(parts[1].to_owned().to_lowercase(), parts[2].to_owned());
+ }
+ "TRACK" => self.is_processing_tracks = true,
+ _ => {}
+ };
+ }
+ if self.is_processing_tracks {
+ if keyword == "TRACK" {
+ let mut track = CuesheetTrack::default();
+ track.item_position = parts[1].to_owned().parse::<u8>().ok()?;
+ track.item_type = parts[2].to_owned();
+ self.tracks.push(track);
+ } else {
+ self.tracks.last_mut()?.process_track(line);
+ }
+ }
+ Some(())
+ }
+}
+
+impl CuesheetTrack {
+ fn process_track(&mut self, line: &str) -> Option<()> {
+ let parts = parse_line(line);
+ let keyword = parts[0].as_str();
+ match keyword {
+ "TITLE" => self.item_title = parts[1].to_owned(),
+ "PERFORMER" => self.item_performer = parts[1].to_owned(),
+ "INDEX" => {
+ let index_position = parts[1].to_owned().parse::<u8>().ok()?;
+ if index_position == 1 {
+ self.item_begin_duration = parts[2].to_owned();
+ }
+ }
+ _ => {}
+ };
+ Some(())
+ }
+}
--
2.25.1
More information about the vlc-devel
mailing list