MPV Tcl Extension edit
bll 2018-3-1 This is a Tcl interface to the MPV audio player, using libmpv. Linux users should be able to readily install libmpv. Windows users can get libmpv from: https://sourceforge.net/projects/mpv-player-windows/files/libmpv/ . I have not found a pre-compiled libmpv for Mac OS.Like the VLC Tcl Extension, I only need audio, and don't use playlists.This code has not been extensively tested, I don't yet know how robust it is. At this time, it has only been tested on Linux and Windows 7 (64-bit). Change Logbll 2018-4-16 Internal state on seek is still playing. Update state table. Reinitialize duration and time on media load.bll 2018-3-30 Update: change provided version number to 1.1 rather than using the MPV version number.
See Also: VLC Tcl Extensiontclmpv.c/* * Copyright 2018 Brad Lanam Walnut Creek CA US */ #define MPVDEBUG 0 #define USE_TCL_STUBS #include <stdio.h> #include <stdlib.h> #include <string.h> #include <memory.h> #include <unistd.h> #include <sys/stat.h> #include <sys/types.h> #include <tcl.h> #include <mpv/client.h> #define CHKTIMER 10 typedef struct { char *name; Tcl_ObjCmdProc *proc; } EnsembleData; typedef enum playstate { PS_NONE = 0, PS_IDLE = 1, PS_OPENING = 2, /* for MPV, mark the state as buffering until we receive the */ /* duration property change */ PS_BUFFERING = 3, PS_PLAYING = 4, PS_PAUSED = 5, PS_STOPPED = 6, PS_ERROR = 7 } playstate; typedef struct { playstate state; const char * name; } playStateMap_t; static const playStateMap_t playStateMap[] = { { PS_NONE, "none" }, { PS_IDLE, "idle" }, { PS_OPENING, "opening" }, { PS_BUFFERING, "buffering" }, { PS_PLAYING, "playing" }, { PS_PAUSED, "paused" }, { PS_STOPPED, "stopped" }, { PS_ERROR, "error" } }; #define playStateMapMax (sizeof(playStateMap)/sizeof(playStateMap_t)) typedef struct { mpv_event_id state; const char * name; playstate stateflag; } stateMap_t; static const stateMap_t stateMap[] = { { MPV_EVENT_NONE, "none", PS_NONE }, { MPV_EVENT_IDLE, "idle", PS_IDLE }, { MPV_EVENT_START_FILE, "opening", PS_OPENING }, { MPV_EVENT_FILE_LOADED, "playing", PS_BUFFERING }, { MPV_EVENT_PROPERTY_CHANGE, "property-chg", PS_NONE }, { MPV_EVENT_SEEK, "seeking", PS_NONE }, { MPV_EVENT_PLAYBACK_RESTART, "playafterseek", PS_NONE }, { MPV_EVENT_END_FILE, "stopped", PS_STOPPED }, { MPV_EVENT_SHUTDOWN, "ended", PS_STOPPED }, /* these next three are only for debugging */ { MPV_EVENT_TRACKS_CHANGED, "tracks-changed", PS_NONE }, { MPV_EVENT_AUDIO_RECONFIG, "audio-reconf", PS_NONE }, { MPV_EVENT_METADATA_UPDATE, "metadata-upd", PS_NONE } }; #define stateMapMax (sizeof(stateMap)/sizeof(stateMap_t)) #define stateMapIdxMax 40 /* mpv currently has 24 states coded */ typedef struct { Tcl_Interp *interp; mpv_handle *inst; char version [40]; mpv_event_id state; int argc; const char **argv; const char *device; double duration; double tm; int paused; int hasEvent; /* flag to process mpv event */ Tcl_TimerToken timerToken; int stateMapIdx [stateMapIdxMax]; FILE *debugfh; } mpvData_t; const char * mpvStateToStr ( mpv_event_id 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; } const char * stateToStr ( playstate state ) { int i; const char *tptr; tptr = ""; for (i = 0; i < playStateMapMax; ++i) { if (state == playStateMap[i].state) { tptr = playStateMap[i].name; break; } } return tptr; } /* executed in some arbitrary thread */ void mpvCallbackHandler ( void *cd ) { mpvData_t *mpvData = (mpvData_t *) cd; mpvData->hasEvent = 1; } void mpvEventHandler ( ClientData cd ) { mpvData_t *mpvData = (mpvData_t *) cd; playstate stateflag; if (mpvData->inst == NULL) { return; } if (mpvData->hasEvent == 0) { mpvData->timerToken = Tcl_CreateTimerHandler (CHKTIMER, &mpvEventHandler, mpvData); return; } mpvData->hasEvent = 0; mpv_event *event = mpv_wait_event (mpvData->inst, 0.0); stateflag = stateMap[(int) mpvData->stateMapIdx[event->event_id]].stateflag; while (event->event_id != MPV_EVENT_NONE) { #if MPVDEBUG if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "mpv: ev: %d %s\n", event->event_id, mpvStateToStr (event->event_id)); } #endif if (event->event_id == MPV_EVENT_PROPERTY_CHANGE) { mpv_event_property *prop = (mpv_event_property *) event->data; #if MPVDEBUG if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "mpv: ev: prop: %s\n", prop->name); } #endif if (strcmp (prop->name, "time-pos") == 0) { if (prop->format == MPV_FORMAT_DOUBLE) { mpvData->tm = * (double *) prop->data; } #if MPVDEBUG if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "mpv: ev: tm: %.2f\n", mpvData->tm); } #endif } else if (strcmp (prop->name, "duration") == 0) { if (mpvData->state == PS_BUFFERING) { mpvData->state = PS_PLAYING; #if MPVDEBUG if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "mpv: state: %s\n", stateToStr(mpvData->state)); } #endif } if (prop->format == MPV_FORMAT_DOUBLE) { mpvData->duration = * (double *) prop->data; } #if MPVDEBUG if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "mpv: ev: dur: %.2f\n", mpvData->duration); } #endif } } else if (stateflag != PS_NONE) { mpvData->state = stateflag; #if MPVDEBUG if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "mpv: state: %s\n", stateToStr(mpvData->state)); } #endif } mpv_event *event = mpv_wait_event (mpvData->inst, 0.0); stateflag = stateMap[(int) mpvData->stateMapIdx[event->event_id]].stateflag; } mpvData->timerToken = Tcl_CreateTimerHandler (CHKTIMER, &mpvEventHandler, mpvData); } int mpvDurationCmd ( ClientData cd, Tcl_Interp* interp, int objc, Tcl_Obj * const objv[] ) { int rc; double tm; double dtm; mpvData_t *mpvData = (mpvData_t *) cd; if (objc != 1) { Tcl_WrongNumArgs(interp, 1, objv, ""); return TCL_ERROR; } rc = TCL_OK; if (mpvData->inst == NULL) { rc = TCL_ERROR; } else { tm = mpvData->duration; Tcl_SetObjResult (interp, Tcl_NewDoubleObj (tm)); } return rc; } int mpvGetTimeCmd ( ClientData cd, Tcl_Interp* interp, int objc, Tcl_Obj * const objv[] ) { int rc; double tm; mpvData_t *mpvData = (mpvData_t *) cd; if (objc != 1) { Tcl_WrongNumArgs(interp, 1, objv, ""); return TCL_ERROR; } rc = TCL_OK; if (mpvData->inst == NULL) { rc = TCL_ERROR; } else { tm = mpvData->tm; Tcl_SetObjResult (interp, Tcl_NewDoubleObj (tm)); } return rc; } int mpvIsPlayCmd ( ClientData cd, Tcl_Interp* interp, int objc, Tcl_Obj * const objv[] ) { int rc; int rval; mpvData_t *mpvData = (mpvData_t *) cd; if (objc != 1) { Tcl_WrongNumArgs(interp, 1, objv, ""); return TCL_ERROR; } rc = TCL_OK; rval = 0; if (mpvData->inst == 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 the telnet VLC interface is ever dropped, this interface * could be enhanced. */ if (mpvData->state == PS_OPENING || mpvData->state == PS_PLAYING || mpvData->state == PS_PAUSED) { rval = 1; } Tcl_SetObjResult (interp, Tcl_NewIntObj (rval)); } return rc; } int mpvMediaCmd ( ClientData cd, Tcl_Interp* interp, int objc, Tcl_Obj * const objv[] ) { int rc; int status; mpvData_t *mpvData = (mpvData_t *) cd; char *fn; struct stat statinfo; double dval; if (objc != 2) { Tcl_WrongNumArgs(interp, 1, objv, "media"); return TCL_ERROR; } rc = TCL_OK; if (mpvData->inst == NULL) { rc = TCL_ERROR; } else { if (mpvData->device != NULL) { status = mpv_set_property (mpvData->inst, "audio-device", MPV_FORMAT_STRING, (void *) mpvData->device); #if MPVDEBUG if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "set-ad:status:%d %s\n", status, mpv_error_string(status)); } #endif } fn = Tcl_GetString(objv[1]); if (stat (fn, &statinfo) != 0) { rc = TCL_ERROR; return rc; } /* reset the duration and time */ mpvData->duration = 0.0; mpvData->tm = 0.0; /* like many players, mpv will start playing when the 'loadfile' * command is executed. */ const char *cmd[] = {"loadfile", fn, "replace", NULL}; status = mpv_command (mpvData->inst, cmd); #if MPVDEBUG if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "loadfile:status:%d %s\n", status, mpv_error_string(status)); } #endif dval = 1.0; status = mpv_set_property (mpvData->inst, "speed", MPV_FORMAT_DOUBLE, &dval); #if MPVDEBUG if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "speed-1:status:%d %s\n", status, mpv_error_string(status)); } #endif } return rc; } int mpvPauseCmd ( ClientData cd, Tcl_Interp* interp, int objc, Tcl_Obj * const objv[] ) { int rc; int status; mpvData_t *mpvData = (mpvData_t *) cd; if (objc != 1) { Tcl_WrongNumArgs(interp, 1, objv, ""); return TCL_ERROR; } rc = TCL_OK; if (mpvData->inst == NULL) { rc = TCL_ERROR; } else { if (mpvData->state != PS_PLAYING && mpvData->state != PS_PAUSED) { ; } else if (mpvData->state == PS_PLAYING && mpvData->paused == 0) { int val = 1; status = mpv_set_property (mpvData->inst, "pause", MPV_FORMAT_FLAG, &val); #if MPVDEBUG if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "pause-%d:status:%d %s\n", val, status, mpv_error_string(status)); } #endif mpvData->paused = 1; mpvData->state = PS_PAUSED; } else if (mpvData->state == PS_PAUSED && mpvData->paused == 1) { int val = 0; status = mpv_set_property (mpvData->inst, "pause", MPV_FORMAT_FLAG, &val); #if MPVDEBUG if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "pause-%d:status:%d %s\n", val, status, mpv_error_string(status)); } #endif mpvData->paused = 0; mpvData->state = PS_PLAYING; } } return rc; } int mpvPlayCmd ( ClientData cd, Tcl_Interp* interp, int objc, Tcl_Obj * const objv[] ) { int rc; int status; mpvData_t *mpvData = (mpvData_t *) cd; if (objc != 1) { Tcl_WrongNumArgs(interp, 1, objv, ""); return TCL_ERROR; } rc = TCL_OK; if (mpvData->inst == NULL) { rc = TCL_ERROR; } else { if (mpvData->state == PS_PAUSED && mpvData->paused == 1) { int val = 0; status = mpv_set_property (mpvData->inst, "pause", MPV_FORMAT_FLAG, &val); #if MPVDEBUG if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "play:status:%d %s\n", status, mpv_error_string(status)); } #endif mpvData->paused = 0; mpvData->state = PS_PLAYING; } } return rc; } int mpvRateCmd ( ClientData cd, Tcl_Interp* interp, int objc, Tcl_Obj * const objv[] ) { int rc; int status; mpvData_t *mpvData = (mpvData_t *) cd; double rate; double d; if (objc != 1 && objc != 2) { Tcl_WrongNumArgs(interp, 1, objv, "?rate?"); return TCL_ERROR; } rc = TCL_OK; if (mpvData->inst == NULL) { rc = TCL_ERROR; } else { if (objc == 2 && mpvData->state == PS_PLAYING) { rc = Tcl_GetDoubleFromObj (interp, objv[1], &d); if (rc == TCL_OK) { rate = d; status = mpv_set_property (mpvData->inst, "speed", MPV_FORMAT_DOUBLE, &rate); #if MPVDEBUG if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "speed-%.2f:status:%d %s\n", rate, status, mpv_error_string(status)); } #endif } } status = mpv_get_property (mpvData->inst, "speed", MPV_FORMAT_DOUBLE, &rate); #if MPVDEBUG if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "speed-get:status:%d %s\n", status, mpv_error_string(status)); } #endif Tcl_SetObjResult (interp, Tcl_NewDoubleObj ((double) rate)); } return rc; } int mpvSeekCmd ( ClientData cd, Tcl_Interp* interp, int objc, Tcl_Obj * const objv[] ) { int rc; int status; double tm; double dtm; mpvData_t *mpvData = (mpvData_t *) cd; double pos; double d; char spos [40]; if (objc != 1 && objc != 2) { Tcl_WrongNumArgs(interp, 1, objv, "?position?"); return TCL_ERROR; } rc = TCL_OK; if (mpvData->inst == NULL) { rc = TCL_ERROR; } else { if (objc == 2 && mpvData->state == PS_PLAYING) { rc = Tcl_GetDoubleFromObj (interp, objv[1], &d); if (rc == TCL_OK) { pos = (double) d; sprintf (spos, "%.1f", pos); const char *cmd[] = { "seek", spos, "absolute", NULL }; status = mpv_command (mpvData->inst, cmd); #if MPVDEBUG if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "seek-%s:status:%d %s\n", spos, status, mpv_error_string(status)); } #endif } } tm = mpvData->tm; Tcl_SetObjResult (interp, Tcl_NewDoubleObj ((double) tm)); } return rc; } int mpvStateCmd ( ClientData cd, Tcl_Interp* interp, int objc, Tcl_Obj * const objv[] ) { int rc; mpv_event_id plstate; mpvData_t *mpvData = (mpvData_t *) cd; rc = TCL_OK; if (mpvData->inst == NULL) { rc = TCL_ERROR; } else { plstate = mpvData->state; Tcl_SetObjResult (interp, Tcl_NewStringObj (stateToStr(plstate), -1)); } return rc; } int mpvStopCmd ( ClientData cd, Tcl_Interp* interp, int objc, Tcl_Obj * const objv[] ) { int rc; int status; mpvData_t *mpvData = (mpvData_t *) cd; rc = TCL_OK; if (mpvData->inst == NULL) { rc = TCL_ERROR; } else { /* stop: stops playback and clears playlist */ /* difference: vlc's stop command does not clear the playlist */ const char *cmd[] = {"stop", NULL}; status = mpv_command (mpvData->inst, cmd); #if MPVDEBUG if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "stop:status:%d %s\n", status, mpv_error_string(status)); } #endif } return rc; } int mpvHaveAudioDevListCmd ( ClientData cd, Tcl_Interp* interp, int objc, Tcl_Obj * const objv[] ) { int rc; rc = 1; Tcl_SetObjResult (interp, Tcl_NewBooleanObj (rc)); return TCL_OK; } int mpvVersionCmd ( ClientData cd, Tcl_Interp* interp, int objc, Tcl_Obj * const objv[] ) { mpvData_t *mpvData = (mpvData_t *) cd; Tcl_SetObjResult (interp, Tcl_NewStringObj (mpvData->version, -1)); return TCL_OK; } void mpvClose ( mpvData_t *mpvData ) { int i; if (mpvData->inst != NULL) { mpv_terminate_destroy (mpvData->inst); mpvData->inst = NULL; } if (mpvData->argv != NULL) { for (i = 0; i < mpvData->argc; ++i) { ckfree (mpvData->argv[i]); } ckfree (mpvData->argv); mpvData->argv = NULL; } if (mpvData->device != NULL) { free ((void *) mpvData->device); mpvData->device = NULL; } mpvData->state = PS_STOPPED; } void mpvExitHandler ( void *cd ) { mpvData_t *mpvData = (mpvData_t *) cd; Tcl_DeleteTimerHandler (mpvData->timerToken); mpvClose (mpvData); if (mpvData->debugfh != NULL) { fclose (mpvData->debugfh); mpvData->debugfh = NULL; } ckfree (cd); } int mpvReleaseCmd ( ClientData cd, Tcl_Interp* interp, int objc, Tcl_Obj * const objv[] ) { mpvData_t *mpvData = (mpvData_t *) cd; Tcl_DeleteTimerHandler (mpvData->timerToken); mpvClose (mpvData); if (mpvData->debugfh != NULL) { fclose (mpvData->debugfh); mpvData->debugfh = NULL; } return TCL_OK; } int mpvInitCmd ( ClientData cd, Tcl_Interp* interp, int objc, Tcl_Obj * const objv[] ) { char *tptr; char *nptr; int rc; int i; int len; int status; int gstatus; mpvData_t *mpvData = (mpvData_t *) cd; mpvData->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); mpvData->argv[i] = nptr; } mpvData->argc = objc; mpvData->argv[objc] = NULL; rc = TCL_ERROR; gstatus = 0; if (mpvData->inst == NULL) { mpvData->inst = mpv_create (); if (mpvData->inst != NULL) { status = mpv_initialize (mpvData->inst); if (status < 0) { gstatus = status; } double vol = 100.0; mpv_set_property (mpvData->inst, "volume", MPV_FORMAT_DOUBLE, &vol); mpv_observe_property(mpvData->inst, 0, "duration", MPV_FORMAT_DOUBLE); mpv_observe_property(mpvData->inst, 0, "time-pos", MPV_FORMAT_DOUBLE); mpv_set_wakeup_callback (mpvData->inst, &mpvCallbackHandler, mpvData); } } if (mpvData->inst != NULL && gstatus == 0) { rc = TCL_OK; } return rc; } int mpvAudioDevSetCmd ( ClientData cd, Tcl_Interp* interp, int objc, Tcl_Obj * const objv[] ) { mpvData_t *mpvData = (mpvData_t *) cd; int rc; char *dev; if (objc != 2) { Tcl_WrongNumArgs(interp, 1, objv, "deviceid"); return TCL_ERROR; } rc = TCL_OK; if (mpvData->inst == NULL) { rc = TCL_ERROR; } else { dev = Tcl_GetString(objv[1]); if (mpvData->device != NULL) { free ((void *) mpvData->device); } mpvData->device = NULL; if (strlen (dev) > 0) { mpvData->device = strdup (dev); } } return rc; } int mpvAudioDevListCmd ( ClientData cd, Tcl_Interp* interp, int objc, Tcl_Obj * const objv[] ) { mpvData_t *mpvData = (mpvData_t *) cd; Tcl_Obj *lobj; Tcl_Obj *sobj; mpv_node anodes; mpv_node_list *infolist; mpv_node_list *nodelist; char *nmptr; char *descptr; int status; if (mpvData->inst == NULL) { return TCL_ERROR; } lobj = Tcl_NewListObj (0, NULL); status = mpv_get_property (mpvData->inst, "audio-device-list", MPV_FORMAT_NODE, &anodes); if (anodes.format != MPV_FORMAT_NODE_ARRAY) { return TCL_ERROR; } nodelist = anodes.u.list; for (int i = 0; i < nodelist->num; ++i) { infolist = nodelist->values[i].u.list; for (int j = 0; j < infolist->num; ++j) { if (strcmp (infolist->keys[j], "name") == 0) { nmptr = infolist->values[j].u.string; } else if (strcmp (infolist->keys[j], "description") == 0) { descptr = infolist->values[j].u.string; } else { if (mpvData->debugfh != NULL) { fprintf (mpvData->debugfh, "dev: %s\n", infolist->keys[j]); } } } sobj = Tcl_NewStringObj (nmptr, -1); Tcl_ListObjAppendElement (interp, lobj, sobj); sobj = Tcl_NewStringObj (descptr, -1); Tcl_ListObjAppendElement (interp, lobj, sobj); } mpv_free_node_contents (&anodes); Tcl_SetObjResult (interp, lobj); return TCL_OK; } static const EnsembleData mpvCmdMap[] = { { "audiodevlist", mpvAudioDevListCmd }, { "audiodevset", mpvAudioDevSetCmd }, { "close", mpvReleaseCmd }, { "duration", mpvDurationCmd }, { "gettime", mpvGetTimeCmd }, { "init", mpvInitCmd }, { "haveaudiodevlist", mpvHaveAudioDevListCmd }, { "isplay", mpvIsPlayCmd }, { "media", mpvMediaCmd }, { "pause", mpvPauseCmd }, { "play", mpvPlayCmd }, { "rate", mpvRateCmd }, { "seek", mpvSeekCmd }, { "state", mpvStateCmd }, { "stop", mpvStopCmd }, { "version", mpvVersionCmd }, { NULL, NULL } }; int Tclmpv_Init (Tcl_Interp *interp) { Tcl_Namespace *nsPtr = NULL; Tcl_Command ensemble = NULL; Tcl_Obj *dictObj = NULL; Tcl_DString ds; mpvData_t *mpvData; unsigned int ivers; int i; int rc; int debug; const char *nsName = "::tcl::tclmpv"; const char *cmdName = nsName + 5; char tvers [20]; if (!Tcl_InitStubs (interp,"8.3",0)) { return TCL_ERROR; } debug = 0; #if MPVDEBUG debug = 1; #endif mpvData = (mpvData_t *) ckalloc (sizeof (mpvData_t)); mpvData->interp = interp; mpvData->inst = NULL; mpvData->argv = NULL; mpvData->state = PS_NONE; mpvData->device = NULL; mpvData->paused = 0; mpvData->duration = 0.0; mpvData->tm = 0.0; mpvData->hasEvent = 0; mpvData->timerToken = Tcl_CreateTimerHandler (CHKTIMER, &mpvEventHandler, mpvData); mpvData->debugfh = NULL; for (i = 0; i < stateMapIdxMax; ++i) { mpvData->stateMapIdx[i] = 0; } for (i = 0; i < stateMapMax; ++i) { mpvData->stateMapIdx[stateMap[i].state] = i; } if (debug) { mpvData->debugfh = fopen ("mpvdebug.txt", "w+"); } 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; mpvCmdMap[i].name != NULL; ++i) { Tcl_Obj *nameObj; Tcl_Obj *fqdnObj; nameObj = Tcl_NewStringObj (mpvCmdMap[i].name, -1); fqdnObj = Tcl_NewStringObj (Tcl_DStringValue(&ds), Tcl_DStringLength(&ds)); Tcl_AppendStringsToObj (fqdnObj, "::", mpvCmdMap[i].name, NULL); Tcl_DictObjPut (NULL, dictObj, nameObj, fqdnObj); if (mpvCmdMap[i].proc) { Tcl_CreateObjCommand (interp, Tcl_GetString (fqdnObj), mpvCmdMap[i].proc, (ClientData) mpvData, NULL); } } if (ensemble) { Tcl_SetEnsembleMappingDict (interp, ensemble, dictObj); } Tcl_DStringFree(&ds); ivers = mpv_client_api_version(); sprintf (tvers, "%d.%d", ivers >> 16, ivers & 0xFF); strcpy (mpvData->version, tvers); /* If the 'package ifneeded' and package provides do * not match, tcl fails. Can't really use the mpv * version number here. */ Tcl_PkgProvide (interp, cmdName+2, "1.1"); return TCL_OK; }