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 exit
tclvlc.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/* fi
winmkvlc.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