diff --git a/zefie_wtvp_minisrv/includes/classes/WTVPNM.js b/zefie_wtvp_minisrv/includes/classes/WTVPNM.js index e09115b5..f3848401 100644 --- a/zefie_wtvp_minisrv/includes/classes/WTVPNM.js +++ b/zefie_wtvp_minisrv/includes/classes/WTVPNM.js @@ -3,7 +3,8 @@ // It does support seeking and pausing via the TCP control channel, but does not support bitrate switching or any of the // other advanced features of the RealServer protocol. It should be compatible with WebTV 2.5 and RP8 clients, but has only been tested with RP8. // RealAudio 3, RealAudio 5, RealAudio G2 and RealAudio 8 (not WebTV compatible) files. -// It is also not compatible with live streams at this time. +// It is also not compatible with live streams at this time. +// Also not tested with SureStream since they never worked with WebTV. (could we, as the server, make SureStream work with WebTV?) const net = require('net'); const fs = require('fs'); diff --git a/zefie_wtvp_minisrv/ra_win/prct3260.ocx b/zefie_wtvp_minisrv/ra_win/prct3260.ocx new file mode 100644 index 00000000..b539b8fd Binary files /dev/null and b/zefie_wtvp_minisrv/ra_win/prct3260.ocx differ diff --git a/zefie_wtvp_minisrv/ra_win/rpcli.cpp b/zefie_wtvp_minisrv/ra_win/rpcli.cpp new file mode 100644 index 00000000..7fc5b9b9 --- /dev/null +++ b/zefie_wtvp_minisrv/ra_win/rpcli.cpp @@ -0,0 +1,670 @@ +// rpcli.cpp - Minimal RealAudio 5/6 (G2) CLI encoder using RealProducer ActiveX +// Build (MSVC example): +// cl rpcli.cpp /EHsc /D_CRT_SECURE_NO_WARNINGS ole32.lib oleaut32.lib +// +// You must also #import the RealProducer control type library (prct3260.ocx). +// Adjust the path below to wherever the control is registered/installed. + +#ifndef __cplusplus +#error rpcli.cpp uses C++ COM features (#import, __uuidof). Compile with MSVC C++. +#endif + +#define COBJMACROS +#include +#include +#include +#include +#include +#include + +#import "prct3260.ocx" no_namespace named_guids raw_interfaces_only + +static void die(const char *msg); + +static BSTR ansi_to_bstr(const char *s) { + int wlen; + BSTR b; + if (!s) { + return NULL; + } + + wlen = MultiByteToWideChar(CP_ACP, 0, s, -1, NULL, 0); + if (wlen <= 0) { + return NULL; + } + + b = SysAllocStringLen(NULL, (UINT)(wlen - 1)); + if (!b) { + return NULL; + } + + if (MultiByteToWideChar(CP_ACP, 0, s, -1, b, wlen) <= 0) { + SysFreeString(b); + return NULL; + } + + return b; +} + +static void bstr_to_ansi(BSTR b, char *buf, size_t bufSize) { + if (!buf || bufSize == 0) { + return; + } + + buf[0] = '\0'; + if (!b) { + return; + } + + WideCharToMultiByte(CP_ACP, 0, b, -1, buf, (int)bufSize, NULL, NULL); +} + +static void pump_pending_messages(void) { + MSG msg; + + while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { + TranslateMessage(&msg); + DispatchMessage(&msg); + } +} + +static void die_last_error(IProducerControl *ctl, const char *fallback) { + long lastError = 0; + BSTR errorText = NULL; + char buf[512]; + + if (!ctl) { + die(fallback); + } + + if (FAILED(ctl->get_LastError(&lastError)) || lastError == 0) { + die(fallback); + } + + if (SUCCEEDED(ctl->GetErrorString(lastError, &errorText)) && errorText) { + bstr_to_ansi(errorText, buf, sizeof(buf)); + SysFreeString(errorText); + fprintf(stderr, "error: %s (code=%ld)\n", buf[0] ? buf : fallback, lastError); + } else { + fprintf(stderr, "error: %s (code=%ld)\n", fallback, lastError); + } + + ExitProcess(1); +} + +// Simple helper: print and exit +static void die(const char *msg) { + fprintf(stderr, "error: %s\n", msg); + ExitProcess(1); +} + +// Map rating string to something we can store as a property +static const char *rating_to_str(const char *r) { + if (_stricmp(r, "G") == 0) return "G"; + if (_stricmp(r, "PG") == 0) return "PG"; + if (_stricmp(r, "R") == 0) return "R"; + if (_stricmp(r, "X") == 0) return "X"; + return NULL; +} + +// Mode → AUDIO_CONTENT_* (from docs) +static AUDIO_CONTENT mode_to_audio_content(const char *m) { + // These constants come from the RealProducer type library. + // You’ll need to confirm exact values in the imported header: + // AUDIO_CONTENT_VOICE, AUDIO_CONTENT_VOICE_OVER_MUSIC, AUDIO_CONTENT_MUSIC, AUDIO_CONTENT_STEREO, etc. + if (_stricmp(m, "voice") == 0) return AUDIO_CONTENT_VOICE; + if (_stricmp(m, "voice-bgm") == 0) return AUDIO_CONTENT_VOICE_BACKGROUND; + if (_stricmp(m, "stereo") == 0) return AUDIO_CONTENT_MUSIC_STEREO; + // default: music + return AUDIO_CONTENT_MUSIC; +} + +// Target → TARGET_* + TargetXXModem property +typedef enum { + TGT_WEBTV, + TGT_56K, + TGT_ISDN, + TGT_CABLE, + TGT_PC +} target_t; + +typedef struct { + ICodecCookie *cookie; + AUDIO_CONTENT content; + long codecId; + long flavorId; + char name[512]; +} selected_codec_t; + +static target_t parse_target(const char *s) { + if (!s) return TGT_WEBTV; + if (_stricmp(s, "WebTV") == 0) return TGT_WEBTV; + if (_stricmp(s, "56k") == 0) return TGT_56K; + if (_stricmp(s, "ISDN") == 0) return TGT_ISDN; + if (_stricmp(s, "Cable") == 0) return TGT_CABLE; + if (_stricmp(s, "PC") == 0) return TGT_PC; + return TGT_WEBTV; +} + +static TARGET_AUDIENCE target_to_audience(target_t tgt) { + switch (tgt) { + case TGT_WEBTV: + return TARGET_28_MODEM; + case TGT_56K: + return TARGET_56_MODEM; + case TGT_ISDN: + return TARGET_SINGLE_ISDN; + case TGT_CABLE: + return TARGET_LAN_LOW; + case TGT_PC: + return TARGET_LAN_HIGH; + } + + return TARGET_28_MODEM; +} + +static AUDIO_CONTENT codec_name_to_audio_content(const char *name) { + if (!name) { + return AUDIO_CONTENT_MUSIC; + } + if (strstr(name, "Stereo") != NULL) { + return AUDIO_CONTENT_MUSIC_STEREO; + } + if (strstr(name, "Music") != NULL) { + return AUDIO_CONTENT_MUSIC; + } + return AUDIO_CONTENT_VOICE; +} + +static int parse_codec_index(const char *s) { + char *end = NULL; + long value; + + if (!s || !*s) { + return -1; + } + + value = strtol(s, &end, 10); + if (!end || *end != '\0' || value < 0 || value > 0x7fffffffL) { + return -1; + } + + return (int)value; +} + +static int get_audio_codec_by_index(IProducerControl *ctl, int codecIndex, selected_codec_t *selected) { + IEnumIDispatch *enumDisp = NULL; + HRESULT hr; + LONG count = 0; + IDispatch *pDisp = NULL; + LONG i; + + if (!selected) { + return 0; + } + + memset(selected, 0, sizeof(*selected)); + selected->codecId = -1; + selected->flavorId = -1; + selected->content = AUDIO_CONTENT_MUSIC; + + hr = ctl->get_AudioCodecEnum(&enumDisp); + if (FAILED(hr) || !enumDisp) { + return 0; + } + + enumDisp->GetCount(&count); + if (codecIndex < 0 || codecIndex >= count) { + enumDisp->Release(); + return 0; + } + + enumDisp->First(&pDisp); + for (i = 0; i < count && pDisp; ++i) { + if (i == codecIndex) { + IAudioCodecInfo *codec = NULL; + if (SUCCEEDED(pDisp->QueryInterface(__uuidof(IAudioCodecInfo), (void**)&codec))) { + BSTR name = NULL; + codec->get_CodecName(&name); + codec->get_CodecCookie(&selected->cookie); + if (selected->cookie) { + selected->cookie->get_codecId(&selected->codecId); + selected->cookie->get_flavorId(&selected->flavorId); + } + bstr_to_ansi(name, selected->name, sizeof(selected->name)); + selected->content = codec_name_to_audio_content(selected->name); + if (name) SysFreeString(name); + codec->Release(); + } + pDisp->Release(); + enumDisp->Release(); + return selected->cookie != NULL; + } + + { + IDispatch *next = NULL; + enumDisp->Next(&next); + pDisp->Release(); + pDisp = next; + } + } + + if (pDisp) pDisp->Release(); + enumDisp->Release(); + return 0; +} + +static int apply_audio_codec_to_target(ITargetAudienceInfo *info, + AUDIO_CONTENT content, + ICodecCookie *cookie) { + if (!info || !cookie) { + return 0; + } + + return SUCCEEDED(info->put_AudioCodec(TARGET_AUDIENCES_AUDIO, content, cookie)); +} + +static int apply_selected_audio_codec(IProducerControl *ctl, + target_t tgt, + const selected_codec_t *selected) { + ITargetAudienceInfo *info = NULL; + TARGET_AUDIENCE audienceList[] = { + TARGET_28_MODEM, + TARGET_56_MODEM, + TARGET_SINGLE_ISDN, + TARGET_DUAL_ISDN, + TARGET_LAN_LOW, + TARGET_LAN_HIGH + }; + int applied = 0; + int i; + + if (!ctl || !selected || !selected->cookie) { + return 0; + } + + for (i = 0; i < (int)(sizeof(audienceList) / sizeof(audienceList[0])); ++i) { + if (SUCCEEDED(ctl->get_TargetAudienceInfo(audienceList[i], &info)) && info) { + applied |= apply_audio_codec_to_target(info, selected->content, selected->cookie); + info->Release(); + info = NULL; + } + } + + if (SUCCEEDED(ctl->get_TargetAudienceInfo(target_to_audience(tgt), &info)) && info) { + applied |= apply_audio_codec_to_target(info, selected->content, selected->cookie); + info->Release(); + } + + return applied; +} + +// Enable only one target audience based on CLI +static void set_target_audience(IProducerControl *ctl, target_t tgt) { + // First, clear all target audiences + ctl->put_Target28KModem(VARIANT_FALSE); + ctl->put_Target56KModem(VARIANT_FALSE); + ctl->put_TargetSingleISDN(VARIANT_FALSE); + ctl->put_TargetDSLCableModem(VARIANT_FALSE); + ctl->put_TargetLAN(VARIANT_FALSE); + + switch (tgt) { + case TGT_WEBTV: + ctl->put_Target28KModem(VARIANT_TRUE); + break; + case TGT_56K: + ctl->put_Target56KModem(VARIANT_TRUE); + break; + case TGT_ISDN: + ctl->put_TargetSingleISDN(VARIANT_TRUE); + break; + case TGT_CABLE: + ctl->put_TargetDSLCableModem(VARIANT_TRUE); + break; + case TGT_PC: + ctl->put_TargetLAN(VARIANT_TRUE); + break; + } +} + +// List all audio codecs using AudioCodecEnum +static void list_codecs(IProducerControl *ctl) { + IEnumIDispatch *enumDisp = NULL; + HRESULT hr = ctl->get_AudioCodecEnum(&enumDisp); + if (FAILED(hr) || !enumDisp) { + die("failed to get AudioCodecEnum"); + } + + LONG count = 0; + enumDisp->GetCount(&count); + + printf("Available audio codecs:\n"); + + IDispatch *pDisp = NULL; + enumDisp->First(&pDisp); + for (LONG i = 0; i < count && pDisp; ++i) { + IAudioCodecInfo *codec = NULL; + if (SUCCEEDED(pDisp->QueryInterface(__uuidof(IAudioCodecInfo), (void**)&codec))) { + BSTR name = NULL; + ICodecCookie *cookie = NULL; + long codecId = -1; + long flavorId = -1; + codec->get_CodecName(&name); + codec->get_CodecCookie(&cookie); + if (cookie) { + cookie->get_codecId(&codecId); + cookie->get_flavorId(&flavorId); + } + + char buf[512]; + WideCharToMultiByte(CP_ACP, 0, name, -1, buf, sizeof(buf), NULL, NULL); + printf(" %ld: %s (codecId=%ld, flavorId=%ld)\n", i, buf, codecId, flavorId); + + SysFreeString(name); + if (cookie) cookie->Release(); + codec->Release(); + } + IDispatch *next = NULL; + enumDisp->Next(&next); + pDisp->Release(); + pDisp = next; + } + + if (pDisp) pDisp->Release(); + enumDisp->Release(); +} + +// Set basic clip properties (Title, Author, etc.) +static void set_clip_properties(IProducerControl *ctl, + const char *title, + const char *author, + const char *copyright, + const char *description, + const char *keywords, + const char *rating) { + if (title) { + BSTR b = ansi_to_bstr(title); + ctl->put_Title(b); + SysFreeString(b); + } + if (author) { + BSTR b = ansi_to_bstr(author); + ctl->put_Author(b); + SysFreeString(b); + } + if (copyright) { + BSTR b = ansi_to_bstr(copyright); + ctl->put_Copyright(b); + SysFreeString(b); + } + + // Extra metadata via SetStringProperty (from docs) + if (description) { + BSTR key = SysAllocString(L"Description"); + BSTR val = ansi_to_bstr(description); + ctl->SetStringProperty(key, val); + SysFreeString(key); + SysFreeString(val); + } + if (keywords) { + BSTR key = SysAllocString(L"Keywords"); + BSTR val = ansi_to_bstr(keywords); + ctl->SetStringProperty(key, val); + SysFreeString(key); + SysFreeString(val); + } + if (rating) { + const char *r = rating_to_str(rating); + if (r) { + BSTR key = SysAllocString(L"Rating"); + BSTR val = ansi_to_bstr(r); + ctl->SetStringProperty(key, val); + SysFreeString(key); + SysFreeString(val); + } + } + + // SelectiveRecord, MobilePlay, Indexable → false via custom numeric props + // Names are inferred; adjust to actual property names if they exist directly. + { + BSTR key = SysAllocString(L"SelectiveRecord"); + ctl->SetNumberProperty(key, 0); + SysFreeString(key); + } + { + BSTR key = SysAllocString(L"MobilePlay"); + ctl->SetNumberProperty(key, 0); + SysFreeString(key); + } + { + BSTR key = SysAllocString(L"Indexable"); + ctl->SetNumberProperty(key, 0); + SysFreeString(key); + } +} + +static void usage(void) { + fprintf(stderr, + "Usage: rpcli [options] input.wav [output.ra]\n" + "Options:\n" + " --ra5 | --g2 Set PlayerCompatibility (default: G2 / PLAYER_6)\n" + " --codec-list List available audio codecs and exit\n" + " --codec N Select codec list entry N; mutually exclusive with --target\n" + " --mode voice|voice-bgm|stereo|music (default: music)\n" + " --target WebTV|56k|ISDN|Cable|PC (default: WebTV)\n" + " --title TEXT\n" + " --author TEXT\n" + " --copyright TEXT\n" + " --description TEXT\n" + " --keywords TEXT\n" + " --rating G|PG|R|X\n" + ); +} + +int main(int argc, char **argv) { + const char *mode = "music"; + const char *target_str = "WebTV"; + const char *title = NULL; + const char *author = NULL; + const char *copyright = NULL; + const char *description = NULL; + const char *keywords = NULL; + const char *rating = NULL; + int player_is_ra5 = 0; + int do_codec_list = 0; + int codec_index = -1; + int target_was_explicit = 0; + selected_codec_t selected_codec; + + const char *infile = NULL; + char outbuf[MAX_PATH] = {0}; + const char *outfile = NULL; + + // Parse options + int i = 1; + for (; i < argc; ++i) { + if (strcmp(argv[i], "--ra5") == 0) { + player_is_ra5 = 1; + } else if (strcmp(argv[i], "--g2") == 0) { + player_is_ra5 = 0; + } else if (strcmp(argv[i], "--codec-list") == 0) { + do_codec_list = 1; + } else if (strcmp(argv[i], "--codec") == 0 && i+1 < argc) { + codec_index = parse_codec_index(argv[++i]); + if (codec_index < 0) { + die("--codec requires a non-negative integer"); + } + } else if (strcmp(argv[i], "--mode") == 0 && i+1 < argc) { + mode = argv[++i]; + } else if (strcmp(argv[i], "--target") == 0 && i+1 < argc) { + target_str = argv[++i]; + target_was_explicit = 1; + } else if (strcmp(argv[i], "--title") == 0 && i+1 < argc) { + title = argv[++i]; + } else if (strcmp(argv[i], "--author") == 0 && i+1 < argc) { + author = argv[++i]; + } else if (strcmp(argv[i], "--copyright") == 0 && i+1 < argc) { + copyright = argv[++i]; + } else if (strcmp(argv[i], "--description") == 0 && i+1 < argc) { + description = argv[++i]; + } else if (strcmp(argv[i], "--keywords") == 0 && i+1 < argc) { + keywords = argv[++i]; + } else if (strcmp(argv[i], "--rating") == 0 && i+1 < argc) { + rating = argv[++i]; + } else if (strncmp(argv[i], "--", 2) == 0) { + fprintf(stderr, "unknown option: %s\n", argv[i]); + usage(); + return 1; + } else { + // first non-option is input + infile = argv[i]; + if (i+1 < argc) { + outfile = argv[i+1]; + } + break; + } + } + + if (!infile && !do_codec_list) { + usage(); + return 1; + } + + if (codec_index >= 0 && target_was_explicit) { + die("--codec is mutually exclusive with --target"); + } + + if (!outfile && infile) { + // derive output: input + ".ra" + const char *p = strrchr(infile, '.'); + size_t len = p ? (size_t)(p - infile) : strlen(infile); + if (len >= sizeof(outbuf)-4) die("input filename too long"); + memcpy(outbuf, infile, len); + strcpy(outbuf + len, ".ra"); + outfile = outbuf; + } + + HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED); + if (FAILED(hr)) die("CoInitialize failed"); + + IProducerControl *ctl = NULL; + hr = CoCreateInstance(__uuidof(ProducerControl), + NULL, + CLSCTX_INPROC_SERVER, + __uuidof(IProducerControl), + (void**)&ctl); + if (FAILED(hr) || !ctl) { + CoUninitialize(); + die("failed to create ProducerControl (is prct3260.ocx installed/registered?)"); + } + + if (do_codec_list) { + list_codecs(ctl); + ctl->Release(); + CoUninitialize(); + return 0; + } + + memset(&selected_codec, 0, sizeof(selected_codec)); + selected_codec.codecId = -1; + selected_codec.flavorId = -1; + + if (codec_index >= 0 && !get_audio_codec_by_index(ctl, codec_index, &selected_codec)) { + ctl->Release(); + CoUninitialize(); + die("invalid codec index"); + } + + // Input properties + ctl->put_InputType(INPUT_SOURCE_FILE); + { + BSTR b = ansi_to_bstr(infile); + ctl->put_InputFilename(b); + SysFreeString(b); + } + ctl->put_InputDoAudio(VARIANT_TRUE); + ctl->put_InputDoVideo(VARIANT_FALSE); + ctl->put_InputDoEvents(VARIANT_FALSE); + + // Output properties + ctl->put_DoOutputFile(VARIANT_TRUE); + { + BSTR b = ansi_to_bstr(outfile); + ctl->put_OutputFilename(b); + SysFreeString(b); + } + + // PlayerCompatibility: RA5 vs G2 (PLAYER_5 / PLAYER_6) + ctl->put_PlayerCompatibility(player_is_ra5 ? PLAYER_5 : PLAYER_6); + + // SureStream: hard-coded false (single-rate) + ctl->put_SureStream(VARIANT_FALSE); + + // AudioContent (mode) + ctl->put_AudioContent(codec_index >= 0 ? selected_codec.content : mode_to_audio_content(mode)); + + // Target audience + set_target_audience(ctl, parse_target(target_str)); + + if (codec_index >= 0 && !apply_selected_audio_codec(ctl, parse_target(target_str), &selected_codec)) { + if (selected_codec.cookie) selected_codec.cookie->Release(); + ctl->Release(); + CoUninitialize(); + die("failed to apply selected codec"); + } + + // Clip properties + custom props + set_clip_properties(ctl, title, author, copyright, + description, keywords, rating); + + // Start encoding + hr = ctl->StartEncoding(); + if (FAILED(hr)) { + ctl->Release(); + CoUninitialize(); + die("StartEncoding failed"); + } + + // RealProducer uses STA COM callbacks/messages while encoding. + // Pump the queue so the control can make progress in a console app. + VARIANT_BOOL isEnc = VARIANT_FALSE; + DWORD startTick = GetTickCount(); + do { + long lastError = 0; + + pump_pending_messages(); + ctl->get_IsEncoding(&isEnc); + + if (SUCCEEDED(ctl->get_LastError(&lastError)) && lastError != 0) { + ctl->StopEncoding(); + if (selected_codec.cookie) selected_codec.cookie->Release(); + die_last_error(ctl, "encoding failed"); + } + + if (GetTickCount() - startTick > 300000) { + ctl->StopEncoding(); + if (selected_codec.cookie) selected_codec.cookie->Release(); + die_last_error(ctl, "encoding timed out"); + } + + MsgWaitForMultipleObjects(0, NULL, FALSE, 100, QS_ALLINPUT); + } while (isEnc == VARIANT_TRUE); + + { + long lastError = 0; + if (SUCCEEDED(ctl->get_LastError(&lastError)) && lastError != 0) { + if (selected_codec.cookie) selected_codec.cookie->Release(); + die_last_error(ctl, "encoding completed with an error"); + } + } + + // StopEncoding (in case) + ctl->StopEncoding(); + + if (selected_codec.cookie) selected_codec.cookie->Release(); + ctl->Release(); + CoUninitialize(); + + printf("Encoded %s -> %s\n", infile, outfile); + return 0; +} diff --git a/zefie_wtvp_minisrv/ra_win/rpcli.exe b/zefie_wtvp_minisrv/ra_win/rpcli.exe new file mode 100644 index 00000000..7839abcf Binary files /dev/null and b/zefie_wtvp_minisrv/ra_win/rpcli.exe differ