VLC Tcl Extension edit
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 Vista2017-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 ExtensionJJM 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 :)
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