bll 2016-12-28 : One full day's work. This is a Tcl interface to libvlc, which comes with the VLC media player. The tighter interface gives better control over song position, time position and player state.
I only need audio, and don't use playlists. The media sub-command only handles file paths right now. There's quite a bit that could still be implemented. But most of that implementation would be fairly straightforward.
I have given up trying to get callbacks working. Calling Tcl code from the VLC event handler does not seem to work properly on Windows.
2017-1-3: Fix for XP and Vista
2017-6-12: Fixed exit callback; Added commands to get device list and set device.
I have the dynamic libraries built for VLC 2.1.x and 2.2.x for Windows 32/64, Linux 32/64 and Mac OS X if you want a pre-built copy.
See Also: MPV Tcl Extension
JJM 2017-01-03: It might be nice to put this in a Fossil repository someplace? It looks quite useful.
bll 2017-1-3: Feel free. It's a little basic at the moment as I only need audio, but adding the non-file media type, playlist management and video shouldn't be difficult. It is working well -- I have been using the telnet interface to VLC for the last five years, and this is rather better.
DG 17-03-10 Super SWEET! Just what I was looking for :)
chw 2019-01-16: Nice. Meanwhile I stole ideas from your extension and from https://github.com/ray2501/tkvlc and mixed it somewhat in https://www.androwish.org/index.html/dir?name=undroid/tkvlc mainly to have video playback. The most important new feature is to have the video frames rendered into a photo image. This allows to process them further, e.g. as texture on a 3D canvas. The stuff on https://www.androwish.org is still WIP but runs already on Linux, Windows, and MacOS.
test.tcl
#!/usr/bin/tclsh
set os $::tcl_platform(os)
if { [regexp -nocase {^win} $os] } {
set os Windows
}
load [pwd]/$os/tclvlc64[info sharedlibextension]
tclvlc init --intf dummy --no-video --ignore-config \
--no-media-library --no-playlist-autostart \
--no-loop --no-random --no-repeat --quiet --play-and-stop \
--audio-filter scaletempo
if { $os eq "Linux" } {
tclvlc media /home/bll/sources/ballroomdj/test.dir/test-files/counter.mp3
}
if { $os eq "Darwin" } {
tclvlc media /Users/bll/Desktop/BallroomDJ.app/Contents/MacOS/test.dir/test-files/counter.mp3
}
if { $os eq "Windows" } {
tclvlc media [file nativename {C:/Users/bll/Desktop/BallroomDJ/test.dir/test-files/counter.mp3}]
}
tclvlc play
set state [tclvlc state]
while { $state eq "opening" || $state eq "buffering" } {
after 100
set state [tclvlc state]
}
tclvlc seek 0.0387
set ::x 0
after 1000 set ::x 1
vwait ::x
tclvlc close
exittclvlc.c
/*
* Copyright 2016-2017 Brad Lanam Walnut Creek CA US
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <memory.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <tcl.h>
#include <vlc/vlc.h>
#include <vlc/libvlc_version.h>
#ifdef COMP_WINDOWS
# include <windows.h>
#endif
typedef struct { char *name; Tcl_ObjCmdProc *proc; } EnsembleData;
typedef struct {
libvlc_state_t state;
const char * name;
} stateMap_t;
static const stateMap_t stateMap[] = {
{ libvlc_NothingSpecial, "idle" },
{ libvlc_Opening, "opening" },
{ libvlc_Buffering, "buffering" },
{ libvlc_Playing, "playing" },
{ libvlc_Paused, "paused" },
{ libvlc_Stopped, "stopped" },
{ libvlc_Ended, "ended" },
{ libvlc_Error, "error" }
};
#define stateMapMax (sizeof(stateMap)/sizeof(stateMap_t))
typedef struct {
Tcl_Interp *interp;
libvlc_instance_t *inst;
char version [40];
libvlc_media_player_t *mp;
libvlc_state_t state;
int argc;
const char **argv;
const char *device;
} vlcData_t;
const char *
stateToStr (
libvlc_state_t state
)
{
int i;
const char *tptr;
tptr = "";
for (i = 0; i < stateMapMax; ++i) {
if (state == stateMap[i].state) {
tptr = stateMap[i].name;
break;
}
}
return tptr;
}
libvlc_state_t
stateToValue (
char *name
)
{
int i;
libvlc_state_t state;
state = libvlc_NothingSpecial;
for (i = 0; i < stateMapMax; ++i) {
if (strcmp (name, stateMap[i].name) == 0) {
state = stateMap[i].state;
break;
}
}
return state;
}
void
vlcEventHandler (
const struct libvlc_event_t *event,
void *cd
)
{
vlcData_t *vlcData = (vlcData_t *) cd;
int objc;
Tcl_Obj **objv;
Tcl_Obj *tobj;
if (event->type == libvlc_MediaStateChanged) {
vlcData->state = event->u.media_state_changed.new_state;
}
}
int
vlcDurationCmd (
ClientData cd,
Tcl_Interp* interp,
int objc,
Tcl_Obj * const objv[]
)
{
int rc;
libvlc_time_t tm;
double dtm;
vlcData_t *vlcData = (vlcData_t *) cd;
if (objc != 1) {
Tcl_WrongNumArgs(interp, 1, objv, "");
return TCL_ERROR;
}
rc = TCL_OK;
if (vlcData->inst == NULL || vlcData->mp == NULL) {
rc = TCL_ERROR;
} else {
tm = libvlc_media_player_get_length (vlcData->mp);
dtm = (double) tm / 1000.0;
Tcl_SetObjResult (interp, Tcl_NewDoubleObj (dtm));
}
return rc;
}
int
vlcGetTimeCmd (
ClientData cd,
Tcl_Interp* interp,
int objc,
Tcl_Obj * const objv[]
)
{
int rc;
libvlc_time_t tm;
double dtm;
vlcData_t *vlcData = (vlcData_t *) cd;
if (objc != 1) {
Tcl_WrongNumArgs(interp, 1, objv, "");
return TCL_ERROR;
}
rc = TCL_OK;
if (vlcData->inst == NULL || vlcData->mp == NULL) {
rc = TCL_ERROR;
} else {
tm = libvlc_media_player_get_time (vlcData->mp);
dtm = (double) tm / 1000.0;
Tcl_SetObjResult (interp, Tcl_NewDoubleObj (dtm));
}
return rc;
}
int
vlcIsPlayCmd (
ClientData cd,
Tcl_Interp* interp,
int objc,
Tcl_Obj * const objv[]
)
{
int rc;
int rval;
vlcData_t *vlcData = (vlcData_t *) cd;
if (objc != 1) {
Tcl_WrongNumArgs(interp, 1, objv, "");
return TCL_ERROR;
}
rc = TCL_OK;
rval = 0;
if (vlcData->inst == NULL || vlcData->mp == NULL) {
rc = TCL_ERROR;
} else {
/*
* In order to match the implementation of VLC's internal
* isplaying command, return true if the player is paused
*/
if (vlcData->state == libvlc_Opening ||
vlcData->state == libvlc_Buffering ||
vlcData->state == libvlc_Playing ||
vlcData->state == libvlc_Paused) {
rval = 1;
}
Tcl_SetObjResult (interp, Tcl_NewIntObj (rval));
}
return rc;
}
int
vlcMediaCmd (
ClientData cd,
Tcl_Interp* interp,
int objc,
Tcl_Obj * const objv[]
)
{
int rc;
libvlc_media_t *m;
libvlc_event_manager_t *em;
vlcData_t *vlcData = (vlcData_t *) cd;
char *fn;
struct stat statinfo;
if (objc != 2) {
Tcl_WrongNumArgs(interp, 1, objv, "mediapath");
return TCL_ERROR;
}
rc = TCL_OK;
if (vlcData->inst == NULL || vlcData->mp == NULL) {
rc = TCL_ERROR;
} else {
fn = Tcl_GetString(objv[1]);
if (stat (fn, &statinfo) != 0) {
rc = TCL_ERROR;
return rc;
}
m = libvlc_media_new_path (vlcData->inst, fn);
libvlc_media_player_set_rate (vlcData->mp, 1.0);
em = libvlc_media_event_manager (m);
libvlc_event_attach (em, libvlc_MediaStateChanged,
&vlcEventHandler, vlcData);
libvlc_media_player_set_media (vlcData->mp, m);
/* on mac os x, the device has to be set after the media is set */
if (vlcData->device != NULL) {
libvlc_audio_output_device_set (vlcData->mp, NULL, vlcData->device);
}
libvlc_media_release (m);
}
return rc;
}
int
vlcPauseCmd (
ClientData cd,
Tcl_Interp* interp,
int objc,
Tcl_Obj * const objv[]
)
{
int rc;
vlcData_t *vlcData = (vlcData_t *) cd;
if (objc != 1) {
Tcl_WrongNumArgs(interp, 1, objv, "");
return TCL_ERROR;
}
rc = TCL_OK;
if (vlcData->inst == NULL || vlcData->mp == NULL) {
rc = TCL_ERROR;
} else {
if (vlcData->state == libvlc_Opening ||
vlcData->state == libvlc_Buffering) {
;
} else if (vlcData->state == libvlc_Playing) {
libvlc_media_player_set_pause (vlcData->mp, 1);
} else if (vlcData->state == libvlc_Paused) {
libvlc_media_player_set_pause (vlcData->mp, 0);
}
}
return rc;
}
int
vlcPlayCmd (
ClientData cd,
Tcl_Interp* interp,
int objc,
Tcl_Obj * const objv[]
)
{
int rc;
vlcData_t *vlcData = (vlcData_t *) cd;
if (objc != 1) {
Tcl_WrongNumArgs(interp, 1, objv, "");
return TCL_ERROR;
}
rc = TCL_OK;
if (vlcData->inst == NULL || vlcData->mp == NULL) {
rc = TCL_ERROR;
} else {
libvlc_media_player_play (vlcData->mp);
}
return rc;
}
int
vlcRateCmd (
ClientData cd,
Tcl_Interp* interp,
int objc,
Tcl_Obj * const objv[]
)
{
int rc;
vlcData_t *vlcData = (vlcData_t *) cd;
float rate;
double d;
if (objc != 1 && objc != 2) {
Tcl_WrongNumArgs(interp, 1, objv, "?rate?");
return TCL_ERROR;
}
rc = TCL_OK;
if (vlcData->inst == NULL || vlcData->mp == NULL) {
rc = TCL_ERROR;
} else {
if (objc == 2 && vlcData->state == libvlc_Playing) {
rc = Tcl_GetDoubleFromObj (interp, objv[1], &d);
if (rc == TCL_OK) {
rate = (float) d;
libvlc_media_player_set_rate (vlcData->mp, rate);
}
}
rate = libvlc_media_player_get_rate (vlcData->mp);
Tcl_SetObjResult (interp, Tcl_NewDoubleObj ((double) rate));
}
return rc;
}
int
vlcSeekCmd (
ClientData cd,
Tcl_Interp* interp,
int objc,
Tcl_Obj * const objv[]
)
{
int rc;
libvlc_time_t tm;
double dtm;
vlcData_t *vlcData = (vlcData_t *) cd;
float pos;
double d;
if (objc != 1 && objc != 2) {
Tcl_WrongNumArgs(interp, 1, objv, "?position?");
return TCL_ERROR;
}
rc = TCL_OK;
if (vlcData->inst == NULL || vlcData->mp == NULL) {
rc = TCL_ERROR;
} else {
if (objc == 2 && vlcData->state == libvlc_Playing) {
tm = libvlc_media_player_get_length (vlcData->mp);
dtm = (double) tm / 1000.0;
rc = Tcl_GetDoubleFromObj (interp, objv[1], &d);
d = d / dtm;
if (rc == TCL_OK) {
pos = (float) d;
libvlc_media_player_set_position (vlcData->mp, pos);
}
}
pos = libvlc_media_player_get_position (vlcData->mp);
Tcl_SetObjResult (interp, Tcl_NewDoubleObj ((double) pos));
}
return rc;
}
int
vlcStateCmd (
ClientData cd,
Tcl_Interp* interp,
int objc,
Tcl_Obj * const objv[]
)
{
int rc;
libvlc_state_t plstate;
vlcData_t *vlcData = (vlcData_t *) cd;
rc = TCL_OK;
if (vlcData->inst == NULL || vlcData->mp == NULL) {
rc = TCL_ERROR;
} else {
plstate = vlcData->state;
Tcl_SetObjResult (interp, Tcl_NewStringObj (stateToStr(plstate), -1));
}
return rc;
}
int
vlcStopCmd (
ClientData cd,
Tcl_Interp* interp,
int objc,
Tcl_Obj * const objv[]
)
{
int rc;
vlcData_t *vlcData = (vlcData_t *) cd;
rc = TCL_OK;
if (vlcData->inst == NULL || vlcData->mp == NULL) {
rc = TCL_ERROR;
} else {
libvlc_media_player_stop (vlcData->mp);
}
return rc;
}
int
vlcHaveAudioDevListCmd (
ClientData cd,
Tcl_Interp* interp,
int objc,
Tcl_Obj * const objv[]
)
{
int rc;
rc = 0;
#if LIBVLC_VERSION_INT >= LIBVLC_VERSION(2,2,0,0)
rc = 1;
#endif
Tcl_SetObjResult (interp, Tcl_NewBooleanObj (rc));
return TCL_OK;
}
#if LIBVLC_VERSION_INT >= LIBVLC_VERSION(2,2,0,0)
int
vlcAudioDevSetCmd (
ClientData cd,
Tcl_Interp* interp,
int objc,
Tcl_Obj * const objv[]
)
{
vlcData_t *vlcData = (vlcData_t *) cd;
int rc;
char *dev;
if (objc != 2) {
Tcl_WrongNumArgs(interp, 1, objv, "deviceid");
return TCL_ERROR;
}
rc = TCL_OK;
if (vlcData->inst == NULL || vlcData->mp == NULL) {
rc = TCL_ERROR;
} else {
dev = Tcl_GetString(objv[1]);
if (vlcData->device != NULL) {
free ((void *) vlcData->device);
}
vlcData->device = NULL;
if (strlen (dev) > 0) {
vlcData->device = strdup (dev);
}
}
return rc;
}
int
vlcAudioDevListCmd (
ClientData cd,
Tcl_Interp* interp,
int objc,
Tcl_Obj * const objv[]
)
{
vlcData_t *vlcData = (vlcData_t *) cd;
Tcl_Obj *lobj;
Tcl_Obj *sobj;
libvlc_audio_output_device_t *adevlist;
libvlc_audio_output_device_t *adevlistptr;
if (vlcData->inst == NULL || vlcData->mp == NULL ||
strcmp (vlcData->version, "2.2.0") < 0) {
return TCL_ERROR;
}
lobj = Tcl_NewListObj (0, NULL);
adevlist = libvlc_audio_output_device_enum (vlcData->mp);
adevlistptr = adevlist;
while (adevlistptr != (libvlc_audio_output_device_t *) NULL) {
sobj = Tcl_NewStringObj (adevlistptr->psz_device, -1);
Tcl_ListObjAppendElement (interp, lobj, sobj);
sobj = Tcl_NewStringObj (adevlistptr->psz_description, -1);
Tcl_ListObjAppendElement (interp, lobj, sobj);
adevlistptr = adevlistptr->p_next;
}
libvlc_audio_output_device_list_release (adevlist);
Tcl_SetObjResult (interp, lobj);
return TCL_OK;
}
#endif
int
vlcVersionCmd (
ClientData cd,
Tcl_Interp* interp,
int objc,
Tcl_Obj * const objv[]
)
{
vlcData_t *vlcData = (vlcData_t *) cd;
Tcl_SetObjResult (interp, Tcl_NewStringObj (vlcData->version, -1));
return TCL_OK;
}
void
vlcClose (
vlcData_t *vlcData
)
{
int i;
if (vlcData->mp != NULL) {
libvlc_media_player_stop (vlcData->mp);
libvlc_media_player_release (vlcData->mp);
vlcData->mp = NULL;
}
if (vlcData->inst != NULL) {
libvlc_release (vlcData->inst);
vlcData->inst = NULL;
}
if (vlcData->argv != NULL) {
for (i = 0; i < vlcData->argc; ++i) {
ckfree (vlcData->argv[i]);
}
ckfree (vlcData->argv);
vlcData->argv = NULL;
}
if (vlcData->device != NULL) {
free ((void *) vlcData->device);
vlcData->device = NULL;
}
vlcData->state = libvlc_NothingSpecial;
}
void
vlcExitHandler (
void *cd
)
{
vlcData_t *vlcData = (vlcData_t *) cd;
vlcClose (vlcData);
ckfree (cd);
}
int
vlcReleaseCmd (
ClientData cd,
Tcl_Interp* interp,
int objc,
Tcl_Obj * const objv[]
)
{
vlcData_t *vlcData = (vlcData_t *) cd;
vlcClose (vlcData);
return TCL_OK;
}
int
vlcInitCmd (
ClientData cd,
Tcl_Interp* interp,
int objc,
Tcl_Obj * const objv[]
)
{
char *tptr;
char *nptr;
int rc;
int i;
int len;
vlcData_t *vlcData = (vlcData_t *) cd;
vlcData->argv = (const char **) ckalloc (sizeof(const char *) * (size_t) (objc + 1));
for (i = 0; i < objc; ++i) {
tptr = Tcl_GetStringFromObj (objv[i], &len);
nptr = (char *) ckalloc (len+1);
strcpy (nptr, tptr);
vlcData->argv[i] = nptr;
}
vlcData->argc = objc;
vlcData->argv[objc] = NULL;
rc = TCL_ERROR;
strcpy (vlcData->version, libvlc_get_version ());
tptr = strchr (vlcData->version, ' ');
if (tptr != NULL) {
*tptr = '\0';
}
if (vlcData->inst == NULL) {
vlcData->inst = libvlc_new (objc, vlcData->argv);
}
if (vlcData->inst != NULL && vlcData->mp == NULL) {
vlcData->mp = libvlc_media_player_new (vlcData->inst);
}
if (vlcData->inst != NULL && vlcData->mp != NULL) {
rc = TCL_OK;
}
return rc;
}
static const EnsembleData vlcCmdMap[] = {
#if LIBVLC_VERSION_INT >= LIBVLC_VERSION(2,2,0,0)
{ "audiodevlist", vlcAudioDevListCmd },
{ "audiodevset", vlcAudioDevSetCmd },
#endif
{ "close", vlcReleaseCmd },
{ "duration", vlcDurationCmd },
{ "gettime", vlcGetTimeCmd },
{ "init", vlcInitCmd },
{ "haveaudiodevlist", vlcHaveAudioDevListCmd },
{ "isplay", vlcIsPlayCmd },
{ "media", vlcMediaCmd },
{ "pause", vlcPauseCmd },
{ "play", vlcPlayCmd },
{ "rate", vlcRateCmd },
{ "seek", vlcSeekCmd },
{ "state", vlcStateCmd },
{ "stop", vlcStopCmd },
{ "version", vlcVersionCmd },
{ NULL, NULL }
};
int
Tclvlc_Init (Tcl_Interp *interp)
{
Tcl_Namespace *nsPtr = NULL;
Tcl_Command ensemble = NULL;
Tcl_Obj *dictObj = NULL;
Tcl_DString ds;
vlcData_t *vlcData;
int i;
int rc;
const char *nsName = "::tcl::tclvlc";
const char *cmdName = nsName + 5;
#ifdef USE_TCL_STUBS
if (!Tcl_InitStubs (interp,"8.3",0)) {
return TCL_ERROR;
}
#else
if (!Tcl_PkgRequire (interp,"Tcl","8.3",0)) {
return TCL_ERROR;
}
#endif
#ifdef COMP_WINDOWS
/* https://forum.videolan.org/viewtopic.php?t=135009 */
{
OSVERSIONINFO osvi;
memset (&osvi, 0, sizeof(OSVERSIONINFO));
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);
GetVersionEx (&osvi);
if ((osvi.dwMajorVersion < 6) ||
(osvi.dwMajorVersion == 6) && (osvi.dwMinorVersion == 0)) {
SetErrorMode(SEM_FAILCRITICALERRORS);
}
}
#endif
vlcData = (vlcData_t *) ckalloc (sizeof (vlcData_t));
vlcData->interp = interp;
vlcData->inst = NULL;
vlcData->mp = NULL;
vlcData->argv = NULL;
vlcData->state = libvlc_NothingSpecial;
vlcData->device = NULL;
Tcl_CreateExitHandler (vlcExitHandler, vlcData);
nsPtr = Tcl_FindNamespace(interp, nsName, NULL, 0);
if (nsPtr == NULL) {
nsPtr = Tcl_CreateNamespace(interp, nsName, NULL, 0);
if (nsPtr == NULL) {
Tcl_Panic ("failed to create namespace: %s\n", nsName);
}
}
ensemble = Tcl_CreateEnsemble(interp, cmdName, nsPtr, TCL_ENSEMBLE_PREFIX);
if (ensemble == NULL) {
Tcl_Panic ("failed to create ensemble: %s\n", cmdName);
}
Tcl_DStringInit (&ds);
Tcl_DStringAppend (&ds, nsName, -1);
dictObj = Tcl_NewObj();
for (i = 0; vlcCmdMap[i].name != NULL; ++i) {
Tcl_Obj *nameObj;
Tcl_Obj *fqdnObj;
nameObj = Tcl_NewStringObj (vlcCmdMap[i].name, -1);
fqdnObj = Tcl_NewStringObj (Tcl_DStringValue(&ds), Tcl_DStringLength(&ds));
Tcl_AppendStringsToObj (fqdnObj, "::", vlcCmdMap[i].name, NULL);
Tcl_DictObjPut (NULL, dictObj, nameObj, fqdnObj);
if (vlcCmdMap[i].proc) {
Tcl_CreateObjCommand (interp, Tcl_GetString (fqdnObj),
vlcCmdMap[i].proc, (ClientData) vlcData, NULL);
}
}
if (ensemble) {
Tcl_SetEnsembleMappingDict (interp, ensemble, dictObj);
}
Tcl_DStringFree(&ds);
Tcl_PkgProvide (interp, cmdName+2, "0.1");
return TCL_OK;
}
mkvlc.sh
#!/bin/sh
tv=8.6
slext=.so
os=`uname -s`
arch=`uname -m`
bits=64
case $arch in
i?86*)
bits=32
;;
esac
inc=-I/usr/include/tcl${tv}
lib=
lv=$tv
if [ $os = "Darwin" ]; then
slext=.dylib
# for MacPorts tcl
inc=-I/opt/local/include
lib=-L/opt/local/lib
inc+=" -I/Applications/VLC.app/Contents/MacOS/include"
lib+=" -L/Applications/VLC.app/Contents/MacOS/lib"
fi
${CC:-cc} -O -shared -fPIC -o tclvlc${slext} $inc -DUSE_TCL_STUBS tclvlc.c $lib -ltclstub${lv} -lvlc
test -d $os || mkdir $os
if [ -f tclvlc${slext} ]; then
echo "tclvlc success"
mv -f tclvlc${slext} $os/tclvlc${bits}${slext}
fi
if [ $os = "Darwin" ]; then
install_name_tool -change "@loader_path/lib/libvlc.5.dylib" "/Applications/VLC.app/Contents/MacOS/lib/libvlc.5.dylib" Darwin/*
fiwinmkvlc.sh
#!/bin/bash
test -d Windows || mkdir Windows
case $MSYSTEM in
*32)
gcc -m32 -shared -static-libgcc -DCOMP_WINDOWS -o Windows/tclvlc32.dll \
-I'/home/bll/vlc/vlc-2.2.4/include' \
'-Wl,-rpath=/c/Program Files (x86)/VideoLAN/vlc' \
-I$HOME/local-32/include -DUSE_TCL_STUBS tclvlc.c \
-L$HOME/local-32/lib -ltclstub86 \
-L'/c/Program Files (x86)/VideoLAN/vlc' -lvlc
;;
*64)
gcc -m64 -shared -static-libgcc -DCOMP_WINDOWS -o Windows/tclvlc64.dll \
-I'/home/bll/vlc/vlc-2.2.4/include' \
'-Wl,-rpath=/c/Program Files/VideoLAN/vlc' \
-I$HOME/local-64/include -DUSE_TCL_STUBS tclvlc.c \
-L$HOME/local-64/lib -ltclstub86 \
-L'/c/Program Files/VideoLAN/vlc' -lvlc
;;
esac