mirror of
https://github.com/ApfelTeeSaft/CTR-WebCam.git
synced 2026-08-26 19:23:33 +00:00
Release
This commit is contained in:
+35
@@ -0,0 +1,35 @@
|
||||
# Build artifacts
|
||||
dist/
|
||||
*.3dsx
|
||||
*.elf
|
||||
*.smdh
|
||||
*.cia
|
||||
|
||||
# 3DS build
|
||||
3ds-app/build/
|
||||
*.o
|
||||
*.d
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Config (user-specific)
|
||||
config.local.json
|
||||
@@ -0,0 +1,260 @@
|
||||
#---------------------------------------------------------------------------------
|
||||
.SUFFIXES:
|
||||
#---------------------------------------------------------------------------------
|
||||
|
||||
ifeq ($(strip $(DEVKITARM)),)
|
||||
$(error "Please set DEVKITARM in your environment. export DEVKITARM=<path to>devkitARM")
|
||||
endif
|
||||
|
||||
TOPDIR ?= $(CURDIR)
|
||||
include $(DEVKITARM)/3ds_rules
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# TARGET is the name of the output
|
||||
# BUILD is the directory where object files & intermediate files will be placed
|
||||
# SOURCES is a list of directories containing source code
|
||||
# DATA is a list of directories containing data files
|
||||
# INCLUDES is a list of directories containing header files
|
||||
# GRAPHICS is a list of directories containing graphics files
|
||||
# GFXBUILD is the directory where converted graphics files will be placed
|
||||
# If set to $(BUILD), it will statically link in the converted
|
||||
# files as if they were data files.
|
||||
#
|
||||
# NO_SMDH: if set to anything, no SMDH file is generated.
|
||||
# ROMFS is the directory which contains the RomFS, relative to the Makefile (Optional)
|
||||
# APP_TITLE is the name of the app stored in the SMDH file (Optional)
|
||||
# APP_DESCRIPTION is the description of the app stored in the SMDH file (Optional)
|
||||
# APP_AUTHOR is the author of the app stored in the SMDH file (Optional)
|
||||
# ICON is the filename of the icon (.png), relative to the project folder.
|
||||
# If not set, it attempts to use one of the following (in this order):
|
||||
# - <Project name>.png
|
||||
# - icon.png
|
||||
# - <libctru folder>/default_icon.png
|
||||
#---------------------------------------------------------------------------------
|
||||
TARGET := 3ds-webcam-bridge
|
||||
BUILD := build
|
||||
SOURCES := source
|
||||
DATA := data
|
||||
INCLUDES := source source/third_party
|
||||
GRAPHICS := gfx
|
||||
#ROMFS := romfs
|
||||
|
||||
APP_TITLE := 3DS Webcam Bridge
|
||||
APP_DESCRIPTION := Stream 3DS cameras to PC as virtual webcam
|
||||
APP_AUTHOR := CTR-WebCam Contributors
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# options for code generation
|
||||
#---------------------------------------------------------------------------------
|
||||
ARCH := -march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft
|
||||
|
||||
CFLAGS := -g -Wall -O2 -mword-relocations \
|
||||
-ffunction-sections \
|
||||
$(ARCH)
|
||||
|
||||
CFLAGS += $(INCLUDE) -D__3DS__
|
||||
|
||||
CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++11
|
||||
|
||||
ASFLAGS := -g $(ARCH)
|
||||
LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
|
||||
|
||||
LIBS := -lctru -lm
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# list of directories containing libraries, this must be the top level containing
|
||||
# include and lib
|
||||
#---------------------------------------------------------------------------------
|
||||
LIBDIRS := $(CTRULIB)
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# no real need to edit anything past this point unless you need to add additional
|
||||
# rules for different file extensions
|
||||
#---------------------------------------------------------------------------------
|
||||
ifneq ($(BUILD),$(notdir $(CURDIR)))
|
||||
#---------------------------------------------------------------------------------
|
||||
|
||||
export OUTPUT := $(CURDIR)/../dist/$(TARGET)
|
||||
export TOPDIR := $(CURDIR)
|
||||
|
||||
export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \
|
||||
$(foreach dir,$(GRAPHICS),$(CURDIR)/$(dir)) \
|
||||
$(foreach dir,$(DATA),$(CURDIR)/$(dir))
|
||||
|
||||
export DEPSDIR := $(CURDIR)/$(BUILD)
|
||||
|
||||
CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c)))
|
||||
CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp)))
|
||||
SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s)))
|
||||
PICAFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.v.pica)))
|
||||
SHLISTFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.shlist)))
|
||||
GFXFILES := $(foreach dir,$(GRAPHICS),$(notdir $(wildcard $(dir)/*.t3s)))
|
||||
BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*)))
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# use CXX for linking C++ projects, CC for standard C
|
||||
#---------------------------------------------------------------------------------
|
||||
ifeq ($(strip $(CPPFILES)),)
|
||||
#---------------------------------------------------------------------------------
|
||||
export LD := $(CC)
|
||||
#---------------------------------------------------------------------------------
|
||||
else
|
||||
#---------------------------------------------------------------------------------
|
||||
export LD := $(CXX)
|
||||
#---------------------------------------------------------------------------------
|
||||
endif
|
||||
#---------------------------------------------------------------------------------
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
ifeq ($(GFXBUILD),$(BUILD))
|
||||
#---------------------------------------------------------------------------------
|
||||
export T3XFILES := $(GFXFILES:.t3s=.t3x)
|
||||
#---------------------------------------------------------------------------------
|
||||
else
|
||||
#---------------------------------------------------------------------------------
|
||||
export ROMFS_T3XFILES := $(patsubst %.t3s, $(GFXBUILD)/%.t3x, $(GFXFILES))
|
||||
export T3XHFILES := $(patsubst %.t3s, $(BUILD)/%.h, $(GFXFILES))
|
||||
#---------------------------------------------------------------------------------
|
||||
endif
|
||||
#---------------------------------------------------------------------------------
|
||||
|
||||
export OFILES_SOURCES := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o)
|
||||
|
||||
export OFILES_BIN := $(addsuffix .o,$(BINFILES)) \
|
||||
$(PICAFILES:.v.pica=.shbin.o) $(SHLISTFILES:.shlist=.shbin.o) \
|
||||
$(addsuffix .o,$(T3XFILES))
|
||||
|
||||
export OFILES := $(OFILES_BIN) $(OFILES_SOURCES)
|
||||
|
||||
export HFILES := $(PICAFILES:.v.pica=_shbin.h) $(SHLISTFILES:.shlist=_shbin.h) \
|
||||
$(addsuffix .h,$(subst .,_,$(BINFILES))) \
|
||||
$(GFXFILES:.t3s=.h)
|
||||
|
||||
export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \
|
||||
$(foreach dir,$(LIBDIRS),-I$(dir)/include) \
|
||||
-I$(CURDIR)/$(BUILD)
|
||||
|
||||
export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib)
|
||||
|
||||
export _3DSXDEPS := $(if $(NO_SMDH),,$(OUTPUT).smdh)
|
||||
|
||||
ifeq ($(strip $(ICON)),)
|
||||
icons := $(wildcard *.png)
|
||||
ifneq (,$(findstring $(TARGET).png,$(icons)))
|
||||
export APP_ICON := $(TOPDIR)/$(TARGET).png
|
||||
else
|
||||
ifneq (,$(findstring icon.png,$(icons)))
|
||||
export APP_ICON := $(TOPDIR)/icon.png
|
||||
endif
|
||||
endif
|
||||
else
|
||||
export APP_ICON := $(TOPDIR)/$(ICON)
|
||||
endif
|
||||
|
||||
ifeq ($(strip $(NO_SMDH)),)
|
||||
export _3DSXFLAGS += --smdh=$(CURDIR)/../dist/$(TARGET).smdh
|
||||
endif
|
||||
|
||||
ifneq ($(ROMFS),)
|
||||
export _3DSXFLAGS += --romfs=$(CURDIR)/$(ROMFS)
|
||||
endif
|
||||
|
||||
.PHONY: all clean
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
all: $(BUILD) $(GFXBUILD) $(DEPSDIR) $(ROMFS_T3XFILES) $(T3XHFILES)
|
||||
@mkdir -p $(CURDIR)/../dist
|
||||
@$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile
|
||||
|
||||
$(BUILD):
|
||||
@mkdir -p $@
|
||||
|
||||
ifneq ($(GFXBUILD),$(BUILD))
|
||||
$(GFXBUILD):
|
||||
@mkdir -p $@
|
||||
endif
|
||||
|
||||
ifneq ($(DEPSDIR),$(BUILD))
|
||||
$(DEPSDIR):
|
||||
@mkdir -p $@
|
||||
endif
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
clean:
|
||||
@echo clean ...
|
||||
@rm -fr $(BUILD) $(GFXBUILD) $(TARGET).3dsx $(OUTPUT).smdh $(TARGET).elf
|
||||
@rm -fr $(CURDIR)/../dist/$(TARGET).*
|
||||
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
$(GFXBUILD)/%.t3x $(BUILD)/%.h : %.t3s
|
||||
#---------------------------------------------------------------------------------
|
||||
@echo $(notdir $<)
|
||||
@tex3ds -i $< -H $(BUILD)/$*.h -d $(DEPSDIR)/$*.d -o $(GFXBUILD)/$*.t3x
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
else
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# main targets
|
||||
#---------------------------------------------------------------------------------
|
||||
$(OUTPUT).3dsx : $(OUTPUT).elf $(_3DSXDEPS)
|
||||
|
||||
$(OFILES_SOURCES) : $(HFILES)
|
||||
|
||||
$(OUTPUT).elf : $(OFILES)
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# you need a rule like this for each extension you use as binary data
|
||||
#---------------------------------------------------------------------------------
|
||||
%.bin.o %_bin.h : %.bin
|
||||
#---------------------------------------------------------------------------------
|
||||
@echo $(notdir $<)
|
||||
@$(bin2o)
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
.PRECIOUS : %.t3x
|
||||
#---------------------------------------------------------------------------------
|
||||
%.t3x.o %_t3x.h : %.t3x
|
||||
#---------------------------------------------------------------------------------
|
||||
@echo $(notdir $<)
|
||||
@$(bin2o)
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
# rules for assembling GPU shaders
|
||||
#---------------------------------------------------------------------------------
|
||||
define shader-as
|
||||
$(eval CURBIN := $*.shbin)
|
||||
$(eval DEPSFILE := $(DEPSDIR)/$*.shbin.d)
|
||||
echo "$(CURBIN).o: $< $1" > $(DEPSFILE)
|
||||
echo "extern const u8" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"_end[];" > `(echo $(CURBIN) | tr . _)`.h
|
||||
echo "extern const u8" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"[];" >> `(echo $(CURBIN) | tr . _)`.h
|
||||
echo "extern const u32" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`_size";" >> `(echo $(CURBIN) | tr . _)`.h
|
||||
picasso -o $(CURBIN) $1
|
||||
bin2s $(CURBIN) | $(AS) -o $*.shbin.o
|
||||
endef
|
||||
|
||||
%.shbin.o %_shbin.h : %.v.pica %.g.pica
|
||||
@echo $(notdir $^)
|
||||
@$(call shader-as,$^)
|
||||
|
||||
%.shbin.o %_shbin.h : %.v.pica
|
||||
@echo $(notdir $<)
|
||||
@$(call shader-as,$<)
|
||||
|
||||
%.shbin.o %_shbin.h : %.shlist
|
||||
@echo $(notdir $<)
|
||||
@$(call shader-as,$(foreach file,$(shell cat $<),$(dir $<)$(file)))
|
||||
|
||||
#---------------------------------------------------------------------------------
|
||||
%.t3x %.h : %.t3s
|
||||
#---------------------------------------------------------------------------------
|
||||
@echo $(notdir $<)
|
||||
@tex3ds -i $< -H $*.h -d $*.d -o $*.t3x
|
||||
|
||||
-include $(DEPSDIR)/*.d
|
||||
|
||||
#---------------------------------------------------------------------------------------
|
||||
endif
|
||||
#---------------------------------------------------------------------------------------
|
||||
@@ -0,0 +1,369 @@
|
||||
#include "camera_stream.h"
|
||||
#include <malloc.h>
|
||||
#include <string.h>
|
||||
#include <arpa/inet.h>
|
||||
|
||||
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
||||
#include "third_party/stb_image_write.h"
|
||||
|
||||
// Global buffers
|
||||
static uint16_t* cam_buffer_left = NULL;
|
||||
static uint16_t* cam_buffer_right = NULL;
|
||||
static uint8_t* rgb_buffer = NULL;
|
||||
|
||||
// Camera handles
|
||||
static u32 cam_left = 0;
|
||||
static u32 cam_right = 0;
|
||||
static bool cameras_active = false;
|
||||
|
||||
// STB callback for JPEG encoding
|
||||
static void stb_write_callback(void* context, void* data, int size) {
|
||||
JpegBuffer* buf = (JpegBuffer*)context;
|
||||
|
||||
// Resize if needed
|
||||
if (buf->size + size > buf->capacity) {
|
||||
size_t new_capacity = buf->capacity * 2;
|
||||
if (new_capacity < buf->size + size) {
|
||||
new_capacity = buf->size + size;
|
||||
}
|
||||
uint8_t* new_data = realloc(buf->data, new_capacity);
|
||||
if (!new_data) return;
|
||||
buf->data = new_data;
|
||||
buf->capacity = new_capacity;
|
||||
}
|
||||
|
||||
memcpy(buf->data + buf->size, data, size);
|
||||
buf->size += size;
|
||||
}
|
||||
|
||||
JpegBuffer* jpeg_buffer_create(size_t initial_capacity) {
|
||||
JpegBuffer* buf = malloc(sizeof(JpegBuffer));
|
||||
if (!buf) return NULL;
|
||||
|
||||
buf->data = malloc(initial_capacity);
|
||||
if (!buf->data) {
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
buf->size = 0;
|
||||
buf->capacity = initial_capacity;
|
||||
return buf;
|
||||
}
|
||||
|
||||
void jpeg_buffer_free(JpegBuffer* buf) {
|
||||
if (buf) {
|
||||
if (buf->data) free(buf->data);
|
||||
free(buf);
|
||||
}
|
||||
}
|
||||
|
||||
Result camera_init(void) {
|
||||
Result ret = camInit();
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
// Allocate buffers
|
||||
cam_buffer_left = memalign(0x80, CAM_BUFFER_SIZE);
|
||||
cam_buffer_right = memalign(0x80, CAM_BUFFER_SIZE);
|
||||
rgb_buffer = malloc(RGB_BUFFER_SIZE);
|
||||
|
||||
if (!cam_buffer_left || !cam_buffer_right || !rgb_buffer) {
|
||||
camera_shutdown();
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void camera_shutdown(void) {
|
||||
if (cameras_active) {
|
||||
camera_stop();
|
||||
}
|
||||
|
||||
if (cam_buffer_left) {
|
||||
free(cam_buffer_left);
|
||||
cam_buffer_left = NULL;
|
||||
}
|
||||
if (cam_buffer_right) {
|
||||
free(cam_buffer_right);
|
||||
cam_buffer_right = NULL;
|
||||
}
|
||||
if (rgb_buffer) {
|
||||
free(rgb_buffer);
|
||||
rgb_buffer = NULL;
|
||||
}
|
||||
|
||||
camExit();
|
||||
}
|
||||
|
||||
Result camera_start(CameraMode mode) {
|
||||
Result ret;
|
||||
|
||||
if (cameras_active) {
|
||||
camera_stop();
|
||||
}
|
||||
|
||||
// Select camera(s)
|
||||
u32 select;
|
||||
switch (mode) {
|
||||
case CAM_MODE_INNER:
|
||||
select = SELECT_IN1;
|
||||
break;
|
||||
case CAM_MODE_OUTER_MONO:
|
||||
case CAM_MODE_OUTER_STEREO:
|
||||
select = SELECT_OUT1 | SELECT_OUT2;
|
||||
break;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Set camera parameters
|
||||
ret = CAMU_SetSize(select, SIZE_CTR_TOP_LCD, CONTEXT_A);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
ret = CAMU_SetOutputFormat(select, OUTPUT_YUV_422, CONTEXT_A);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
ret = CAMU_SetFrameRate(select, FRAME_RATE_15);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
ret = CAMU_SetNoiseFilter(select, true);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
ret = CAMU_SetAutoExposure(select, true);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
ret = CAMU_SetAutoWhiteBalance(select, true);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
ret = CAMU_SetTrimming(select, false);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
// Activate cameras
|
||||
ret = CAMU_Activate(select);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
cameras_active = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void camera_stop(void) {
|
||||
if (cameras_active) {
|
||||
CAMU_Activate(SELECT_NONE);
|
||||
cameras_active = false;
|
||||
}
|
||||
}
|
||||
|
||||
void yuv422_to_rgb24(const uint16_t* yuv, uint8_t* rgb, int width, int height) {
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x += 2) {
|
||||
int idx = y * width + x;
|
||||
uint16_t px1 = yuv[idx];
|
||||
uint16_t px2 = yuv[idx + 1];
|
||||
|
||||
// YUV422: [Y0 U Y1 V] packed in 16-bit values
|
||||
int y0 = (px1 >> 8) & 0xFF;
|
||||
int u = px1 & 0xFF;
|
||||
int y1 = (px2 >> 8) & 0xFF;
|
||||
int v = px2 & 0xFF;
|
||||
|
||||
// YUV to RGB conversion
|
||||
int c0 = y0 - 16;
|
||||
int c1 = y1 - 16;
|
||||
int d = u - 128;
|
||||
int e = v - 128;
|
||||
|
||||
// Pixel 0
|
||||
int r0 = (298 * c0 + 409 * e + 128) >> 8;
|
||||
int g0 = (298 * c0 - 100 * d - 208 * e + 128) >> 8;
|
||||
int b0 = (298 * c0 + 516 * d + 128) >> 8;
|
||||
|
||||
// Pixel 1
|
||||
int r1 = (298 * c1 + 409 * e + 128) >> 8;
|
||||
int g1 = (298 * c1 - 100 * d - 208 * e + 128) >> 8;
|
||||
int b1 = (298 * c1 + 516 * d + 128) >> 8;
|
||||
|
||||
// Clamp
|
||||
r0 = r0 < 0 ? 0 : (r0 > 255 ? 255 : r0);
|
||||
g0 = g0 < 0 ? 0 : (g0 > 255 ? 255 : g0);
|
||||
b0 = b0 < 0 ? 0 : (b0 > 255 ? 255 : b0);
|
||||
r1 = r1 < 0 ? 0 : (r1 > 255 ? 255 : r1);
|
||||
g1 = g1 < 0 ? 0 : (g1 > 255 ? 255 : g1);
|
||||
b1 = b1 < 0 ? 0 : (b1 > 255 ? 255 : b1);
|
||||
|
||||
// Write RGB
|
||||
int out_idx = (y * width + x) * 3;
|
||||
rgb[out_idx + 0] = r0;
|
||||
rgb[out_idx + 1] = g0;
|
||||
rgb[out_idx + 2] = b0;
|
||||
rgb[out_idx + 3] = r1;
|
||||
rgb[out_idx + 4] = g1;
|
||||
rgb[out_idx + 5] = b1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Result camera_capture_frame(CameraMode mode, JpegBuffer* left, JpegBuffer* right) {
|
||||
Result ret;
|
||||
u32 transfer_size = 0;
|
||||
|
||||
// Reset JPEG buffer sizes
|
||||
if (left) left->size = 0;
|
||||
if (right) right->size = 0;
|
||||
|
||||
switch (mode) {
|
||||
case CAM_MODE_INNER:
|
||||
case CAM_MODE_OUTER_MONO: {
|
||||
// Capture left/main camera only
|
||||
u32 cam_select = (mode == CAM_MODE_INNER) ? SELECT_IN1 : SELECT_OUT1;
|
||||
|
||||
ret = CAMU_GetMaxBytes(&transfer_size, CAM_WIDTH, CAM_HEIGHT);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
ret = CAMU_SetTransferBytes(cam_select, transfer_size, CAM_WIDTH, CAM_HEIGHT);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
ret = CAMU_ClearBuffer(PORT_CAM1);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
ret = CAMU_StartCapture(cam_select);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
ret = CAMU_SetReceiving(&cam_left, cam_buffer_left, cam_select, transfer_size, 0);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
// Wait for transfer
|
||||
svcWaitSynchronization(cam_left, U64_MAX);
|
||||
svcCloseHandle(cam_left);
|
||||
|
||||
ret = CAMU_StopCapture(cam_select);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
// Convert to RGB
|
||||
yuv422_to_rgb24(cam_buffer_left, rgb_buffer, CAM_WIDTH, CAM_HEIGHT);
|
||||
|
||||
// Encode JPEG
|
||||
if (left) {
|
||||
stbi_write_jpg_to_func(stb_write_callback, left, CAM_WIDTH, CAM_HEIGHT, 3,
|
||||
rgb_buffer, JPEG_QUALITY);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case CAM_MODE_OUTER_STEREO: {
|
||||
// Capture both outer cameras
|
||||
ret = CAMU_GetMaxBytes(&transfer_size, CAM_WIDTH, CAM_HEIGHT);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
// Left camera (OUT1)
|
||||
ret = CAMU_SetTransferBytes(SELECT_OUT1, transfer_size, CAM_WIDTH, CAM_HEIGHT);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
ret = CAMU_ClearBuffer(PORT_CAM1);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
ret = CAMU_StartCapture(SELECT_OUT1);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
ret = CAMU_SetReceiving(&cam_left, cam_buffer_left, SELECT_OUT1, transfer_size, 0);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
// Right camera (OUT2)
|
||||
ret = CAMU_SetTransferBytes(SELECT_OUT2, transfer_size, CAM_WIDTH, CAM_HEIGHT);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
ret = CAMU_ClearBuffer(PORT_CAM2);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
ret = CAMU_StartCapture(SELECT_OUT2);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
ret = CAMU_SetReceiving(&cam_right, cam_buffer_right, SELECT_OUT2, transfer_size, 0);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
// Wait for both transfers
|
||||
svcWaitSynchronization(cam_left, U64_MAX);
|
||||
svcWaitSynchronization(cam_right, U64_MAX);
|
||||
svcCloseHandle(cam_left);
|
||||
svcCloseHandle(cam_right);
|
||||
|
||||
ret = CAMU_StopCapture(SELECT_OUT1 | SELECT_OUT2);
|
||||
if (R_FAILED(ret)) return ret;
|
||||
|
||||
// Convert left to RGB
|
||||
yuv422_to_rgb24(cam_buffer_left, rgb_buffer, CAM_WIDTH, CAM_HEIGHT);
|
||||
if (left) {
|
||||
stbi_write_jpg_to_func(stb_write_callback, left, CAM_WIDTH, CAM_HEIGHT, 3,
|
||||
rgb_buffer, JPEG_QUALITY);
|
||||
}
|
||||
|
||||
// Convert right to RGB
|
||||
yuv422_to_rgb24(cam_buffer_right, rgb_buffer, CAM_WIDTH, CAM_HEIGHT);
|
||||
if (right) {
|
||||
stbi_write_jpg_to_func(stb_write_callback, right, CAM_WIDTH, CAM_HEIGHT, 3,
|
||||
rgb_buffer, JPEG_QUALITY);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sendall(int sock, const void* buf, size_t len) {
|
||||
const uint8_t* ptr = buf;
|
||||
size_t sent = 0;
|
||||
|
||||
while (sent < len) {
|
||||
int n = send(sock, ptr + sent, len - sent, 0);
|
||||
if (n < 0) return -1;
|
||||
if (n == 0) return -2; // Connection closed
|
||||
sent += n;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Result send_frame_mono(int sock, JpegBuffer* jpeg) {
|
||||
uint8_t header[6];
|
||||
|
||||
// Header: [flags][version][length]
|
||||
header[0] = 0x00; // No stereo flag
|
||||
header[1] = PROTO_VERSION;
|
||||
uint32_t len = htonl(jpeg->size);
|
||||
memcpy(&header[2], &len, 4);
|
||||
|
||||
if (sendall(sock, header, 6) < 0) return -1;
|
||||
if (sendall(sock, jpeg->data, jpeg->size) < 0) return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
Result send_frame_stereo(int sock, JpegBuffer* left, JpegBuffer* right,
|
||||
uint16_t wL, uint16_t hL, uint16_t wR, uint16_t hR) {
|
||||
uint8_t header[18];
|
||||
|
||||
// Header: [flags][version][wL][hL][wR][hR][left_len][right_len]
|
||||
header[0] = PROTO_FLAG_STEREO;
|
||||
header[1] = PROTO_VERSION;
|
||||
|
||||
uint16_t wL_be = htons(wL);
|
||||
uint16_t hL_be = htons(hL);
|
||||
uint16_t wR_be = htons(wR);
|
||||
uint16_t hR_be = htons(hR);
|
||||
memcpy(&header[2], &wL_be, 2);
|
||||
memcpy(&header[4], &hL_be, 2);
|
||||
memcpy(&header[6], &wR_be, 2);
|
||||
memcpy(&header[8], &hR_be, 2);
|
||||
|
||||
uint32_t left_len = htonl(left->size);
|
||||
uint32_t right_len = htonl(right->size);
|
||||
memcpy(&header[10], &left_len, 4);
|
||||
memcpy(&header[14], &right_len, 4);
|
||||
|
||||
if (sendall(sock, header, 18) < 0) return -1;
|
||||
if (sendall(sock, left->data, left->size) < 0) return -1;
|
||||
if (sendall(sock, right->data, right->size) < 0) return -1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#ifndef CAMERA_STREAM_H
|
||||
#define CAMERA_STREAM_H
|
||||
|
||||
#include <3ds.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
// Camera configuration
|
||||
#define CAM_WIDTH 400
|
||||
#define CAM_HEIGHT 240
|
||||
#define CAM_BUFFER_SIZE (CAM_WIDTH * CAM_HEIGHT * 2)
|
||||
#define RGB_BUFFER_SIZE (CAM_WIDTH * CAM_HEIGHT * 3)
|
||||
|
||||
// JPEG quality (0-100)
|
||||
#define JPEG_QUALITY 65
|
||||
|
||||
// Protocol constants
|
||||
#define PROTO_VERSION 0x01
|
||||
#define PROTO_FLAG_STEREO 0x01
|
||||
|
||||
// Camera mode
|
||||
typedef enum {
|
||||
CAM_MODE_INNER,
|
||||
CAM_MODE_OUTER_MONO,
|
||||
CAM_MODE_OUTER_STEREO
|
||||
} CameraMode;
|
||||
|
||||
// JPEG encoded buffer
|
||||
typedef struct {
|
||||
uint8_t* data;
|
||||
size_t size;
|
||||
size_t capacity;
|
||||
} JpegBuffer;
|
||||
|
||||
// Initialize camera subsystem
|
||||
Result camera_init(void);
|
||||
|
||||
// Shutdown camera subsystem
|
||||
void camera_shutdown(void);
|
||||
|
||||
// Start camera capture
|
||||
Result camera_start(CameraMode mode);
|
||||
|
||||
// Stop camera capture
|
||||
void camera_stop(void);
|
||||
|
||||
// Capture and encode frame(s)
|
||||
Result camera_capture_frame(CameraMode mode, JpegBuffer* left, JpegBuffer* right);
|
||||
|
||||
// Send mono frame over socket
|
||||
Result send_frame_mono(int sock, JpegBuffer* jpeg);
|
||||
|
||||
// Send stereo frame over socket
|
||||
Result send_frame_stereo(int sock, JpegBuffer* left, JpegBuffer* right,
|
||||
uint16_t wL, uint16_t hL, uint16_t wR, uint16_t hR);
|
||||
|
||||
// Helper: convert YUV422 to RGB24
|
||||
void yuv422_to_rgb24(const uint16_t* yuv, uint8_t* rgb, int width, int height);
|
||||
|
||||
// Helper: send all data
|
||||
int sendall(int sock, const void* buf, size_t len);
|
||||
|
||||
// Helper: allocate JPEG buffer
|
||||
JpegBuffer* jpeg_buffer_create(size_t initial_capacity);
|
||||
|
||||
// Helper: free JPEG buffer
|
||||
void jpeg_buffer_free(JpegBuffer* buf);
|
||||
|
||||
#endif // CAMERA_STREAM_H
|
||||
@@ -0,0 +1,560 @@
|
||||
#include <3ds.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <malloc.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/stat.h>
|
||||
#include <netinet/in.h>
|
||||
#include <netinet/tcp.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <errno.h>
|
||||
#include <time.h>
|
||||
|
||||
#include "camera_stream.h"
|
||||
|
||||
#define SOC_BUFFER_SIZE 0x100000
|
||||
#define SERVER_PORT 9000
|
||||
#define RECONNECT_DELAY_MS 3000
|
||||
#define CONFIG_PATH "/3ds/3ds-webcam-bridge/config.txt"
|
||||
|
||||
// Application state
|
||||
typedef struct {
|
||||
char server_ip[32];
|
||||
int server_port;
|
||||
CameraMode camera_mode;
|
||||
bool stereo_enabled;
|
||||
bool streaming;
|
||||
bool should_exit;
|
||||
|
||||
// Stats
|
||||
u64 frames_sent;
|
||||
u64 bytes_sent;
|
||||
float fps;
|
||||
bool connected;
|
||||
|
||||
// Threading
|
||||
Thread stream_thread;
|
||||
LightLock state_lock;
|
||||
} AppState;
|
||||
|
||||
static AppState g_state;
|
||||
static u32* soc_buffer = NULL;
|
||||
static PrintConsole topScreen, bottomScreen;
|
||||
|
||||
// Forward declarations
|
||||
static void stream_thread_func(void* arg);
|
||||
static void draw_ui(void);
|
||||
static void handle_input(void);
|
||||
static void load_config(void);
|
||||
static void save_config(void);
|
||||
static bool input_ip_address(void);
|
||||
static bool input_port(void);
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
Result ret;
|
||||
|
||||
// Initialize services
|
||||
gfxInitDefault();
|
||||
consoleInit(GFX_TOP, &topScreen);
|
||||
consoleInit(GFX_BOTTOM, &bottomScreen);
|
||||
|
||||
// Initialize SOC (sockets)
|
||||
soc_buffer = memalign(0x1000, SOC_BUFFER_SIZE);
|
||||
if (!soc_buffer) {
|
||||
printf("Failed to allocate SOC buffer\n");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
ret = socInit(soc_buffer, SOC_BUFFER_SIZE);
|
||||
if (R_FAILED(ret)) {
|
||||
printf("socInit failed: 0x%08lX\n", ret);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// Initialize camera
|
||||
ret = camera_init();
|
||||
if (R_FAILED(ret)) {
|
||||
printf("camera_init failed: 0x%08lX\n", ret);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
// Initialize application state
|
||||
memset(&g_state, 0, sizeof(AppState));
|
||||
strcpy(g_state.server_ip, "192.168.1.100"); // Default IP
|
||||
g_state.server_port = SERVER_PORT;
|
||||
g_state.camera_mode = CAM_MODE_OUTER_STEREO;
|
||||
g_state.stereo_enabled = true;
|
||||
g_state.streaming = false;
|
||||
g_state.should_exit = false;
|
||||
LightLock_Init(&g_state.state_lock);
|
||||
|
||||
// Load config from SD card
|
||||
load_config();
|
||||
|
||||
printf("3DS Webcam Bridge v1.0\n");
|
||||
printf("======================\n\n");
|
||||
printf("Press START to exit\n");
|
||||
printf("Press A to toggle streaming\n");
|
||||
printf("Press SELECT to change camera\n");
|
||||
printf("Press Y to edit IP/Port\n");
|
||||
printf("Press X to toggle 3D\n\n");
|
||||
|
||||
// Main loop
|
||||
while (aptMainLoop() && !g_state.should_exit) {
|
||||
handle_input();
|
||||
draw_ui();
|
||||
|
||||
gspWaitForVBlank();
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (g_state.streaming) {
|
||||
g_state.should_exit = true;
|
||||
threadJoin(g_state.stream_thread, U64_MAX);
|
||||
threadFree(g_state.stream_thread);
|
||||
}
|
||||
|
||||
cleanup:
|
||||
camera_shutdown();
|
||||
socExit();
|
||||
|
||||
if (soc_buffer) {
|
||||
free(soc_buffer);
|
||||
}
|
||||
|
||||
gfxExit();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void handle_input(void) {
|
||||
hidScanInput();
|
||||
u32 kDown = hidKeysDown();
|
||||
|
||||
// Exit
|
||||
if (kDown & KEY_START) {
|
||||
g_state.should_exit = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Toggle streaming
|
||||
if (kDown & KEY_A) {
|
||||
LightLock_Lock(&g_state.state_lock);
|
||||
if (g_state.streaming) {
|
||||
g_state.should_exit = true;
|
||||
LightLock_Unlock(&g_state.state_lock);
|
||||
threadJoin(g_state.stream_thread, U64_MAX);
|
||||
threadFree(g_state.stream_thread);
|
||||
LightLock_Lock(&g_state.state_lock);
|
||||
g_state.streaming = false;
|
||||
g_state.should_exit = false;
|
||||
g_state.connected = false;
|
||||
} else {
|
||||
// Determine camera mode
|
||||
if (g_state.camera_mode == CAM_MODE_INNER) {
|
||||
// Inner camera - always mono
|
||||
g_state.camera_mode = CAM_MODE_INNER;
|
||||
} else {
|
||||
// Outer cameras - check stereo flag
|
||||
if (g_state.stereo_enabled) {
|
||||
g_state.camera_mode = CAM_MODE_OUTER_STEREO;
|
||||
} else {
|
||||
g_state.camera_mode = CAM_MODE_OUTER_MONO;
|
||||
}
|
||||
}
|
||||
|
||||
g_state.streaming = true;
|
||||
g_state.frames_sent = 0;
|
||||
g_state.bytes_sent = 0;
|
||||
|
||||
s32 prio = 0;
|
||||
svcGetThreadPriority(&prio, CUR_THREAD_HANDLE);
|
||||
g_state.stream_thread = threadCreate(stream_thread_func, NULL, 8*1024, prio-1, -2, false);
|
||||
}
|
||||
LightLock_Unlock(&g_state.state_lock);
|
||||
}
|
||||
|
||||
// Change camera mode
|
||||
if (kDown & KEY_SELECT) {
|
||||
LightLock_Lock(&g_state.state_lock);
|
||||
if (!g_state.streaming) {
|
||||
if (g_state.camera_mode == CAM_MODE_INNER ||
|
||||
g_state.camera_mode == CAM_MODE_OUTER_MONO ||
|
||||
g_state.camera_mode == CAM_MODE_OUTER_STEREO) {
|
||||
// Toggle between inner and outer
|
||||
if (g_state.camera_mode == CAM_MODE_INNER) {
|
||||
g_state.camera_mode = g_state.stereo_enabled ?
|
||||
CAM_MODE_OUTER_STEREO : CAM_MODE_OUTER_MONO;
|
||||
} else {
|
||||
g_state.camera_mode = CAM_MODE_INNER;
|
||||
g_state.stereo_enabled = false; // Inner doesn't support stereo
|
||||
}
|
||||
}
|
||||
}
|
||||
LightLock_Unlock(&g_state.state_lock);
|
||||
}
|
||||
|
||||
// Toggle stereo (only for outer cameras)
|
||||
if (kDown & KEY_X) {
|
||||
LightLock_Lock(&g_state.state_lock);
|
||||
if (!g_state.streaming &&
|
||||
(g_state.camera_mode == CAM_MODE_OUTER_MONO ||
|
||||
g_state.camera_mode == CAM_MODE_OUTER_STEREO)) {
|
||||
g_state.stereo_enabled = !g_state.stereo_enabled;
|
||||
}
|
||||
LightLock_Unlock(&g_state.state_lock);
|
||||
}
|
||||
|
||||
// Edit IP/Port (only when not streaming)
|
||||
if (kDown & KEY_Y) {
|
||||
LightLock_Lock(&g_state.state_lock);
|
||||
if (!g_state.streaming) {
|
||||
LightLock_Unlock(&g_state.state_lock);
|
||||
|
||||
// Show menu on bottom screen
|
||||
consoleSelect(&bottomScreen);
|
||||
printf("\x1b[2J");
|
||||
printf("\x1b[0;0H");
|
||||
printf("=== Configuration ===\n\n");
|
||||
printf("A: Edit IP Address\n");
|
||||
printf("B: Edit Port\n");
|
||||
printf("X: Cancel\n\n");
|
||||
printf("Current:\n");
|
||||
printf("IP: %s\n", g_state.server_ip);
|
||||
printf("Port: %d\n", g_state.server_port);
|
||||
gfxFlushBuffers();
|
||||
gfxSwapBuffers();
|
||||
|
||||
// Wait for choice
|
||||
while (aptMainLoop()) {
|
||||
hidScanInput();
|
||||
u32 kPressed = hidKeysDown();
|
||||
|
||||
if (kPressed & KEY_A) {
|
||||
if (input_ip_address()) {
|
||||
save_config();
|
||||
}
|
||||
break;
|
||||
} else if (kPressed & KEY_B) {
|
||||
if (input_port()) {
|
||||
save_config();
|
||||
}
|
||||
break;
|
||||
} else if (kPressed & KEY_X) {
|
||||
break;
|
||||
}
|
||||
gspWaitForVBlank();
|
||||
}
|
||||
} else {
|
||||
LightLock_Unlock(&g_state.state_lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void draw_ui(void) {
|
||||
LightLock_Lock(&g_state.state_lock);
|
||||
|
||||
// Top screen - Status
|
||||
consoleSelect(&topScreen);
|
||||
printf("\x1b[2J"); // Clear screen
|
||||
printf("\x1b[0;0H"); // Home cursor
|
||||
|
||||
printf("=== 3DS Webcam Bridge ===\n\n");
|
||||
|
||||
printf("Server: %s:%d\n", g_state.server_ip, g_state.server_port);
|
||||
printf("Status: %s\n", g_state.streaming ?
|
||||
(g_state.connected ? "STREAMING" : "CONNECTING...") : "STOPPED");
|
||||
|
||||
const char* cam_str = "Unknown";
|
||||
switch (g_state.camera_mode) {
|
||||
case CAM_MODE_INNER:
|
||||
cam_str = "Inner (Mono)";
|
||||
break;
|
||||
case CAM_MODE_OUTER_MONO:
|
||||
cam_str = "Outer (Mono)";
|
||||
break;
|
||||
case CAM_MODE_OUTER_STEREO:
|
||||
cam_str = "Outer (Stereo)";
|
||||
break;
|
||||
}
|
||||
printf("Camera: %s\n", cam_str);
|
||||
printf("3D Mode: %s\n", g_state.stereo_enabled ? "Enabled" : "Disabled");
|
||||
|
||||
printf("\n--- Statistics ---\n");
|
||||
printf("Frames: %llu\n", g_state.frames_sent);
|
||||
printf("Data: %.2f MB\n", g_state.bytes_sent / (1024.0 * 1024.0));
|
||||
printf("FPS: %.1f\n", g_state.fps);
|
||||
|
||||
printf("\n--- Controls ---\n");
|
||||
printf("A: Start/Stop\n");
|
||||
printf("SELECT: Change Camera\n");
|
||||
printf("X: Toggle 3D%s\n",
|
||||
(g_state.camera_mode == CAM_MODE_INNER) ? " (Outer only!)" : "");
|
||||
printf("Y: Edit IP/Port\n");
|
||||
printf("START: Exit\n");
|
||||
|
||||
// Bottom screen - Settings
|
||||
consoleSelect(&bottomScreen);
|
||||
printf("\x1b[2J");
|
||||
printf("\x1b[0;0H");
|
||||
|
||||
printf("=== Settings ===\n\n");
|
||||
|
||||
printf("Network Configuration:\n");
|
||||
printf("IP: %s\n", g_state.server_ip);
|
||||
printf("Port: %d\n", g_state.server_port);
|
||||
printf("-> Press Y to edit\n");
|
||||
|
||||
printf("\nCamera Selection:\n");
|
||||
printf("%s Inner (single lens)\n",
|
||||
(g_state.camera_mode == CAM_MODE_INNER) ? ">" : " ");
|
||||
printf("%s Outer (dual lens)\n",
|
||||
(g_state.camera_mode != CAM_MODE_INNER) ? ">" : " ");
|
||||
|
||||
printf("\n3D Stereo Mode:\n");
|
||||
if (g_state.camera_mode == CAM_MODE_INNER) {
|
||||
printf(" (Requires outer cameras)\n");
|
||||
} else {
|
||||
printf("[%c] Enable 3D (stereo)\n", g_state.stereo_enabled ? 'X' : ' ');
|
||||
printf("Press X to toggle\n");
|
||||
}
|
||||
|
||||
printf("\nResolution: %dx%d\n", CAM_WIDTH, CAM_HEIGHT);
|
||||
printf("JPEG Quality: %d\n", JPEG_QUALITY);
|
||||
|
||||
LightLock_Unlock(&g_state.state_lock);
|
||||
gfxFlushBuffers();
|
||||
gfxSwapBuffers();
|
||||
}
|
||||
|
||||
static void stream_thread_func(void* arg) {
|
||||
(void)arg;
|
||||
|
||||
int sock = -1;
|
||||
JpegBuffer* jpeg_left = NULL;
|
||||
JpegBuffer* jpeg_right = NULL;
|
||||
u64 last_time = 0;
|
||||
u32 frame_count = 0;
|
||||
|
||||
jpeg_left = jpeg_buffer_create(CAM_WIDTH * CAM_HEIGHT);
|
||||
jpeg_right = jpeg_buffer_create(CAM_WIDTH * CAM_HEIGHT);
|
||||
|
||||
if (!jpeg_left || !jpeg_right) {
|
||||
goto thread_cleanup;
|
||||
}
|
||||
|
||||
while (!g_state.should_exit) {
|
||||
// Connect to server
|
||||
sock = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sock < 0) {
|
||||
svcSleepThread(RECONNECT_DELAY_MS * 1000000ULL);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Set socket options
|
||||
int flag = 1;
|
||||
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag));
|
||||
|
||||
struct sockaddr_in server_addr;
|
||||
memset(&server_addr, 0, sizeof(server_addr));
|
||||
server_addr.sin_family = AF_INET;
|
||||
server_addr.sin_port = htons(g_state.server_port);
|
||||
inet_aton(g_state.server_ip, &server_addr.sin_addr);
|
||||
|
||||
if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
|
||||
close(sock);
|
||||
sock = -1;
|
||||
svcSleepThread(RECONNECT_DELAY_MS * 1000000ULL);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Connected
|
||||
LightLock_Lock(&g_state.state_lock);
|
||||
g_state.connected = true;
|
||||
CameraMode mode = g_state.camera_mode;
|
||||
LightLock_Unlock(&g_state.state_lock);
|
||||
|
||||
// Start camera
|
||||
if (R_FAILED(camera_start(mode))) {
|
||||
close(sock);
|
||||
sock = -1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Streaming loop
|
||||
last_time = svcGetSystemTick();
|
||||
frame_count = 0;
|
||||
|
||||
while (!g_state.should_exit) {
|
||||
// Capture frame
|
||||
Result ret = camera_capture_frame(mode, jpeg_left,
|
||||
(mode == CAM_MODE_OUTER_STEREO) ? jpeg_right : NULL);
|
||||
|
||||
if (R_FAILED(ret)) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Send frame
|
||||
if (mode == CAM_MODE_OUTER_STEREO) {
|
||||
ret = send_frame_stereo(sock, jpeg_left, jpeg_right,
|
||||
CAM_WIDTH, CAM_HEIGHT, CAM_WIDTH, CAM_HEIGHT);
|
||||
} else {
|
||||
ret = send_frame_mono(sock, jpeg_left);
|
||||
}
|
||||
|
||||
if (R_FAILED(ret)) {
|
||||
break; // Connection lost
|
||||
}
|
||||
|
||||
// Update stats
|
||||
LightLock_Lock(&g_state.state_lock);
|
||||
g_state.frames_sent++;
|
||||
g_state.bytes_sent += jpeg_left->size;
|
||||
if (mode == CAM_MODE_OUTER_STEREO) {
|
||||
g_state.bytes_sent += jpeg_right->size;
|
||||
}
|
||||
|
||||
frame_count++;
|
||||
u64 current_time = svcGetSystemTick();
|
||||
u64 elapsed_ms = (current_time - last_time) * 1000 / CPU_TICKS_PER_MSEC;
|
||||
if (elapsed_ms >= 1000) {
|
||||
g_state.fps = (float)frame_count * 1000.0f / elapsed_ms;
|
||||
frame_count = 0;
|
||||
last_time = current_time;
|
||||
}
|
||||
LightLock_Unlock(&g_state.state_lock);
|
||||
|
||||
// Frame rate limiting (target ~15 FPS)
|
||||
svcSleepThread(33 * 1000000ULL); // ~30ms
|
||||
}
|
||||
|
||||
// Cleanup connection
|
||||
camera_stop();
|
||||
close(sock);
|
||||
sock = -1;
|
||||
|
||||
LightLock_Lock(&g_state.state_lock);
|
||||
g_state.connected = false;
|
||||
LightLock_Unlock(&g_state.state_lock);
|
||||
|
||||
if (g_state.should_exit) break;
|
||||
|
||||
// Wait before reconnecting
|
||||
svcSleepThread(RECONNECT_DELAY_MS * 1000000ULL);
|
||||
}
|
||||
|
||||
thread_cleanup:
|
||||
if (sock >= 0) {
|
||||
close(sock);
|
||||
}
|
||||
if (jpeg_left) {
|
||||
jpeg_buffer_free(jpeg_left);
|
||||
}
|
||||
if (jpeg_right) {
|
||||
jpeg_buffer_free(jpeg_right);
|
||||
}
|
||||
}
|
||||
|
||||
// Configuration file loading
|
||||
static void load_config(void) {
|
||||
FILE* f = fopen(CONFIG_PATH, "r");
|
||||
if (!f) {
|
||||
return; // Use defaults if file doesn't exist
|
||||
}
|
||||
|
||||
char line[128];
|
||||
while (fgets(line, sizeof(line), f)) {
|
||||
// Remove newline
|
||||
line[strcspn(line, "\r\n")] = 0;
|
||||
|
||||
// Parse key=value
|
||||
char* equals = strchr(line, '=');
|
||||
if (!equals) continue;
|
||||
|
||||
*equals = '\0';
|
||||
char* key = line;
|
||||
char* value = equals + 1;
|
||||
|
||||
if (strcmp(key, "ip") == 0) {
|
||||
strncpy(g_state.server_ip, value, sizeof(g_state.server_ip) - 1);
|
||||
g_state.server_ip[sizeof(g_state.server_ip) - 1] = '\0';
|
||||
} else if (strcmp(key, "port") == 0) {
|
||||
int port = atoi(value);
|
||||
if (port > 0 && port < 65536) {
|
||||
g_state.server_port = port;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
// Configuration file saving
|
||||
static void save_config(void) {
|
||||
// Create directory if it doesn't exist
|
||||
mkdir("/3ds", 0777);
|
||||
mkdir("/3ds/3ds-webcam-bridge", 0777);
|
||||
|
||||
FILE* f = fopen(CONFIG_PATH, "w");
|
||||
if (!f) {
|
||||
return;
|
||||
}
|
||||
|
||||
fprintf(f, "ip=%s\n", g_state.server_ip);
|
||||
fprintf(f, "port=%d\n", g_state.server_port);
|
||||
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
// Input IP address using software keyboard
|
||||
static bool input_ip_address(void) {
|
||||
SwkbdState swkbd;
|
||||
char mybuf[32];
|
||||
SwkbdButton button = SWKBD_BUTTON_NONE;
|
||||
|
||||
swkbdInit(&swkbd, SWKBD_TYPE_NORMAL, 2, -1);
|
||||
swkbdSetHintText(&swkbd, "Enter IP address");
|
||||
swkbdSetInitialText(&swkbd, g_state.server_ip);
|
||||
swkbdSetFeatures(&swkbd, SWKBD_MULTILINE);
|
||||
|
||||
button = swkbdInputText(&swkbd, mybuf, sizeof(mybuf));
|
||||
|
||||
if (button == SWKBD_BUTTON_CONFIRM) {
|
||||
// Basic validation
|
||||
if (strlen(mybuf) > 0) {
|
||||
strncpy(g_state.server_ip, mybuf, sizeof(g_state.server_ip) - 1);
|
||||
g_state.server_ip[sizeof(g_state.server_ip) - 1] = '\0';
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Input port using software keyboard
|
||||
static bool input_port(void) {
|
||||
SwkbdState swkbd;
|
||||
char mybuf[16];
|
||||
char portstr[16];
|
||||
SwkbdButton button = SWKBD_BUTTON_NONE;
|
||||
|
||||
snprintf(portstr, sizeof(portstr), "%d", g_state.server_port);
|
||||
|
||||
swkbdInit(&swkbd, SWKBD_TYPE_NUMPAD, 2, -1);
|
||||
swkbdSetHintText(&swkbd, "Enter port (1-65535)");
|
||||
swkbdSetInitialText(&swkbd, portstr);
|
||||
swkbdSetFeatures(&swkbd, SWKBD_FIXED_WIDTH);
|
||||
|
||||
button = swkbdInputText(&swkbd, mybuf, sizeof(mybuf));
|
||||
|
||||
if (button == SWKBD_BUTTON_CONFIRM) {
|
||||
int port = atoi(mybuf);
|
||||
if (port > 0 && port < 65536) {
|
||||
g_state.server_port = port;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
+1724
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,508 @@
|
||||
# 3DS Webcam Bridge
|
||||
|
||||
Stream your Nintendo 3DS cameras to your PC as a virtual webcam! Support for both **mono** and **stereo (3D)** modes using the outer dual cameras.
|
||||
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
- **📷 Multiple Camera Modes**
|
||||
- Inner camera (single lens, mono)
|
||||
- Outer cameras (dual lens, mono or stereo)
|
||||
- True 3D support with synchronized stereo capture
|
||||
|
||||
- **🎥 Stereo Output Modes**
|
||||
- **Off**: Left eye only
|
||||
- **Side-by-Side**: Horizontal stereo for 3D displays/VR
|
||||
- **Anaglyph**: Red-cyan 3D glasses
|
||||
- **Dual**: Separate left/right virtual cameras
|
||||
|
||||
- **💻 Cross-Platform PC Support**
|
||||
- **Windows 10/11**: Via OBS Virtual Camera
|
||||
- **Linux**: Via v4l2loopback + pyvirtualcam/pyfakewebcam/ffmpeg
|
||||
|
||||
- **⚡ Real-time Streaming**
|
||||
- ~15 FPS @ 400×240 per camera
|
||||
- JPEG compression for efficient Wi-Fi streaming
|
||||
- Auto-reconnect on connection loss
|
||||
|
||||
---
|
||||
|
||||
## 📋 Table of Contents
|
||||
|
||||
- [Security & Ethics Notice](#security--ethics-notice)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Linux (Ubuntu/Debian)](#linux-ubuntudebian)
|
||||
- [Windows 10/11](#windows-1011)
|
||||
- [Building for 3DS](#building-for-3ds)
|
||||
- [Usage](#usage)
|
||||
- [3DS Application](#3ds-application)
|
||||
- [PC Server Options](#pc-server-options)
|
||||
- [Project Structure](#project-structure)
|
||||
- [Requirements](#requirements)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
- [Documentation](#documentation)
|
||||
- [Contributing](#contributing)
|
||||
- [License](#license)
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Security & Ethics Notice
|
||||
|
||||
**IMPORTANT**: This software is intended for personal, educational, and creative use only.
|
||||
|
||||
- **Privacy**: Streaming camera feeds creates privacy risks. Always obtain consent from anyone who may be captured by the cameras.
|
||||
- **Network Security**: The stream is **not encrypted** by default. Only use on trusted networks.
|
||||
- **Responsible Use**: Do not use this software for surveillance, harassment, or any illegal activities.
|
||||
- **Children**: If used by or around minors, ensure appropriate adult supervision and consent.
|
||||
|
||||
By using this software, you accept full responsibility for its use and agree to comply with all applicable laws and regulations.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
### Linux (Ubuntu/Debian)
|
||||
|
||||
#### 1. Install PC Server Dependencies
|
||||
|
||||
```bash
|
||||
# Clone or navigate to the repository
|
||||
cd 3ds-webcam-bridge
|
||||
|
||||
# Install dependencies (Python, v4l2loopback, ffmpeg, etc.)
|
||||
./scripts/pc-install.sh
|
||||
|
||||
# Create virtual camera device
|
||||
./scripts/create-v4l2.sh
|
||||
|
||||
# For dual devices (stereo mode):
|
||||
./scripts/create-v4l2.sh --dual
|
||||
```
|
||||
|
||||
#### 2. Run the Server
|
||||
|
||||
```bash
|
||||
# Mono mode (default)
|
||||
./pc-server/run_server.sh --device /dev/video0
|
||||
|
||||
# Side-by-side stereo
|
||||
./pc-server/run_server.sh --stereo-mode sbs --width 400 --height 240 --device /dev/video0
|
||||
|
||||
# Dual device stereo
|
||||
./pc-server/run_server.sh --stereo-mode dual --dual-devices /dev/video0,/dev/video1
|
||||
```
|
||||
|
||||
#### 3. Use in Applications
|
||||
|
||||
Open any application that supports webcams (Zoom, OBS, Chrome, etc.) and select:
|
||||
- **Single mode**: `/dev/video0` (or "3DS Webcam")
|
||||
- **Dual mode**: `/dev/video0` (Left) and `/dev/video1` (Right)
|
||||
|
||||
---
|
||||
|
||||
### Windows 10/11
|
||||
|
||||
#### 1. Install PC Server Dependencies
|
||||
|
||||
**Run PowerShell as Administrator:**
|
||||
|
||||
```powershell
|
||||
# Allow script execution for this session
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
|
||||
# Navigate to repository
|
||||
cd 3ds-webcam-bridge
|
||||
|
||||
# Run installer (installs Python, OBS Studio, dependencies)
|
||||
.\scripts\windows\pc-install.ps1 -Port 9000
|
||||
```
|
||||
|
||||
The installer will:
|
||||
- ✅ Install Python 3.11 (if needed)
|
||||
- ✅ Install OBS Studio with Virtual Camera
|
||||
- ✅ Create Python virtual environment
|
||||
- ✅ Install Python packages
|
||||
- ✅ Add firewall rule for TCP port 9000
|
||||
|
||||
#### 2. Initialize OBS Virtual Camera
|
||||
|
||||
**Important first-time step:**
|
||||
|
||||
1. Launch **OBS Studio**
|
||||
2. Go to **Tools → VirtualCam → Start**
|
||||
3. Close OBS (the virtual camera will remain available)
|
||||
|
||||
#### 3. Run the Server
|
||||
|
||||
```powershell
|
||||
# Mono mode
|
||||
.\pc-server\run_server.ps1 -Port 9000 -Device "OBS Virtual Camera"
|
||||
|
||||
# Side-by-side stereo
|
||||
.\pc-server\run_server.ps1 -StereoMode sbs -Width 400 -Height 240 -Device "OBS Virtual Camera"
|
||||
|
||||
# Dual device stereo (requires multiple OBS virtual cameras)
|
||||
.\pc-server\run_server.ps1 -StereoMode dual -DualDevices "OBS Virtual Camera,OBS Virtual Camera 2"
|
||||
```
|
||||
|
||||
#### 4. Use in Applications
|
||||
|
||||
Open any application (Zoom, Discord, Chrome, etc.) and select:
|
||||
- **"OBS Virtual Camera"** as your camera device
|
||||
|
||||
---
|
||||
|
||||
### Building for 3DS
|
||||
|
||||
#### 1. Install devkitPro (Linux)
|
||||
|
||||
```bash
|
||||
# Install devkitPro toolchain
|
||||
./scripts/install-devkitpro.sh
|
||||
|
||||
# Set environment variables (add to ~/.bashrc for persistence)
|
||||
export DEVKITPRO=/opt/devkitpro
|
||||
export DEVKITARM=$DEVKITPRO/devkitARM
|
||||
export PATH=$DEVKITARM/bin:$PATH
|
||||
|
||||
# Reload environment
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
#### 2. Build the 3DS App
|
||||
|
||||
```bash
|
||||
# Build (downloads stb_image_write.h if needed)
|
||||
./scripts/build-3ds.sh
|
||||
|
||||
# Output: dist/3ds-webcam-bridge.3dsx
|
||||
```
|
||||
|
||||
#### 3. Install on 3DS
|
||||
|
||||
1. Copy `dist/3ds-webcam-bridge.3dsx` to your SD card:
|
||||
```
|
||||
SD:/3ds/3ds-webcam-bridge/3ds-webcam-bridge.3dsx
|
||||
```
|
||||
|
||||
2. Insert SD card into 3DS
|
||||
3. Launch **Homebrew Launcher**
|
||||
4. Launch **3DS Webcam Bridge**
|
||||
|
||||
---
|
||||
|
||||
## 📖 Usage
|
||||
|
||||
### 3DS Application
|
||||
|
||||
#### Controls
|
||||
|
||||
| Button | Action |
|
||||
|--------|--------|
|
||||
| **A** | Start/Stop streaming |
|
||||
| **SELECT** | Toggle between Inner/Outer cameras |
|
||||
| **X** | Toggle 3D mode (outer cameras only) |
|
||||
| **Y** | Edit IP address and port |
|
||||
| **START** | Exit application |
|
||||
|
||||
#### UI Screens
|
||||
|
||||
**Top Screen**: Status display
|
||||
- Server IP and port
|
||||
- Connection status
|
||||
- Camera mode
|
||||
- Frame count, data sent, FPS
|
||||
|
||||
**Bottom Screen**: Settings
|
||||
- **Network configuration** (IP/Port) - Press Y to edit
|
||||
- Camera selection (Inner/Outer)
|
||||
- 3D checkbox (enabled only for Outer cameras)
|
||||
- Resolution and quality info
|
||||
|
||||
#### Camera Modes
|
||||
|
||||
| Mode | Description | 3D Support |
|
||||
|------|-------------|------------|
|
||||
| **Inner** | Front-facing camera (single lens) | ❌ No |
|
||||
| **Outer** | Rear cameras (dual lens) | ✅ Yes |
|
||||
|
||||
**Important**: The **3D checkbox** is only enabled when **Outer** cameras are selected. The inner camera has only one lens and cannot provide stereo 3D.
|
||||
|
||||
#### Configuration
|
||||
|
||||
**Method 1: Using Software Keyboard (Recommended)**
|
||||
|
||||
1. Press **Y** button in the app
|
||||
2. Select **A** to edit IP address or **B** to edit port
|
||||
3. Use the 3DS keyboard to enter your PC's IP address
|
||||
4. Configuration is automatically saved to SD card
|
||||
|
||||
**Method 2: Manual Config File**
|
||||
|
||||
Create a config file at **SD:/3ds/3ds-webcam-bridge/config.txt**:
|
||||
|
||||
```
|
||||
# Your PC's IP address
|
||||
ip=192.168.1.100
|
||||
|
||||
# Server port
|
||||
port=9000
|
||||
```
|
||||
|
||||
The app will load this config on startup. Any changes made via the Y button will update this file.
|
||||
|
||||
---
|
||||
|
||||
### PC Server Options
|
||||
|
||||
#### Full Command-Line Reference
|
||||
|
||||
```bash
|
||||
server.py [options]
|
||||
|
||||
Options:
|
||||
--listen ADDRESS Listen address (default: 0.0.0.0)
|
||||
--port PORT TCP port (default: 9000)
|
||||
--mode MODE Force mono or auto-detect (default: auto)
|
||||
--stereo-mode MODE Stereo output: off, sbs, anaglyph, dual (default: off)
|
||||
--device DEVICE Virtual camera device
|
||||
Linux: /dev/videoX
|
||||
Windows: "OBS Virtual Camera" or index
|
||||
--dual-devices DEVICES Two devices for dual mode (comma-separated)
|
||||
--width WIDTH Output width per eye (default: 640)
|
||||
--height HEIGHT Output height (default: 480)
|
||||
--fps FPS Output frame rate (default: 15)
|
||||
--prefer-backend BACKEND Backend: pyvirtualcam, pyfakewebcam, ffmpeg
|
||||
--verbose Verbose logging
|
||||
```
|
||||
|
||||
#### Examples
|
||||
|
||||
**Linux**:
|
||||
```bash
|
||||
# Basic mono
|
||||
./run_server.sh --device /dev/video0
|
||||
|
||||
# Side-by-side stereo (1280x480 output)
|
||||
./run_server.sh --stereo-mode sbs --width 640 --height 480
|
||||
|
||||
# Anaglyph 3D (red-cyan)
|
||||
./run_server.sh --stereo-mode anaglyph --width 640 --height 480
|
||||
|
||||
# Dual outputs
|
||||
./run_server.sh --stereo-mode dual --dual-devices /dev/video0,/dev/video1
|
||||
|
||||
# Use pyfakewebcam backend
|
||||
./run_server.sh --prefer-backend pyfakewebcam --device /dev/video0
|
||||
```
|
||||
|
||||
**Windows**:
|
||||
```powershell
|
||||
# Basic mono
|
||||
.\run_server.ps1 -Device "OBS Virtual Camera"
|
||||
|
||||
# Side-by-side stereo
|
||||
.\run_server.ps1 -StereoMode sbs -Width 640 -Height 480
|
||||
|
||||
# Dual outputs (requires configuring multiple OBS virtual cameras)
|
||||
.\run_server.ps1 -StereoMode dual -DualDevices "OBS Virtual Camera,OBS Virtual Camera 2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
3ds-webcam-bridge/
|
||||
├── README.md # This file
|
||||
├── LICENSE # MIT license
|
||||
├── .gitignore
|
||||
├── examples/
|
||||
│ └── sample-config.json # Example configuration
|
||||
├── 3ds-app/ # 3DS homebrew application
|
||||
│ ├── Makefile
|
||||
│ └── source/
|
||||
│ ├── main.c # Main app with UI
|
||||
│ ├── camera_stream.c # Camera capture & streaming
|
||||
│ ├── camera_stream.h
|
||||
│ └── third_party/
|
||||
│ └── stb_image_write.h # JPEG encoder
|
||||
├── pc-server/ # PC server (Python)
|
||||
│ ├── server.py # Main server application
|
||||
│ ├── requirements.txt
|
||||
│ ├── run_server.sh # Linux launcher
|
||||
│ └── run_server.ps1 # Windows launcher
|
||||
├── scripts/ # Installation & build scripts
|
||||
│ ├── install-devkitpro.sh # Install 3DS toolchain (Linux)
|
||||
│ ├── build-3ds.sh # Build 3DS app
|
||||
│ ├── pc-install.sh # Install PC dependencies (Linux)
|
||||
│ ├── create-v4l2.sh # Create v4l2loopback devices (Linux)
|
||||
│ └── windows/
|
||||
│ ├── pc-install.ps1 # Install PC dependencies (Windows)
|
||||
│ └── helpers.psm1 # PowerShell helper functions
|
||||
├── docs/ # Documentation
|
||||
│ ├── design.md # Technical design details
|
||||
│ └── troubleshooting.md # Common issues and solutions
|
||||
└── dist/ # Build output
|
||||
└── 3ds-webcam-bridge.3dsx
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📦 Requirements
|
||||
|
||||
### 3DS
|
||||
|
||||
- **Hardware**: Nintendo 3DS/2DS (Old or New) with Homebrew Launcher
|
||||
- **Firmware**: Any version with Homebrew access
|
||||
- **SD Card**: For installing the `.3dsx` file
|
||||
- **Wi-Fi**: 2.4 GHz network (3DS doesn't support 5 GHz)
|
||||
|
||||
### PC - Linux
|
||||
|
||||
- **OS**: Ubuntu 20.04+, Debian 11+, or equivalent
|
||||
- **Kernel**: 4.4+ with v4l2loopback support
|
||||
- **Python**: 3.8+
|
||||
- **Packages**:
|
||||
- `v4l2loopback-dkms`
|
||||
- `v4l2loopback-utils`
|
||||
- `ffmpeg`
|
||||
- `python3-venv`
|
||||
|
||||
### PC - Windows
|
||||
|
||||
- **OS**: Windows 10 (1809+) or Windows 11
|
||||
- **Python**: 3.8+ (auto-installed by script)
|
||||
- **OBS Studio**: 26.0+ (auto-installed by script)
|
||||
- **winget**: For automated installation
|
||||
|
||||
### Network
|
||||
|
||||
- **Both devices on same network** (LAN/Wi-Fi)
|
||||
- **TCP port 9000** open (configurable)
|
||||
- **Recommended**: 5 GHz Wi-Fi for PC (3DS uses 2.4 GHz)
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
#### "No virtual camera found" (Windows)
|
||||
- **Solution**: Start OBS Studio, go to **Tools → VirtualCam → Start**, then try again
|
||||
- Ensure OBS Studio 26.0+ is installed (check via `winget list OBSProject.OBSStudio`)
|
||||
|
||||
#### "/dev/video0: Device or resource busy" (Linux)
|
||||
- **Solution**: Close all applications using the device (browser, Zoom, etc.)
|
||||
- Check: `sudo fuser /dev/video0`
|
||||
- Kill processes: `sudo fuser -k /dev/video0`
|
||||
|
||||
#### "Connection refused" / "Timeout"
|
||||
- Check that both devices are on the same network
|
||||
- Verify the server IP address on 3DS matches your PC
|
||||
- Find your PC IP:
|
||||
- **Linux**: `ip addr show` or `hostname -I`
|
||||
- **Windows**: `ipconfig` (look for IPv4)
|
||||
- Check firewall:
|
||||
- **Linux**: `sudo ufw allow 9000/tcp`
|
||||
- **Windows**: Run installer as Administrator to add rule
|
||||
|
||||
#### Low FPS / Laggy stream
|
||||
- Reduce resolution: `--width 320 --height 240`
|
||||
- Lower JPEG quality (edit `3ds-app/source/camera_stream.h`, change `JPEG_QUALITY`)
|
||||
- Use 5 GHz Wi-Fi for PC (3DS is limited to 2.4 GHz)
|
||||
- Ensure 3DS is close to Wi-Fi router
|
||||
|
||||
#### "v4l2loopback not loaded" (Linux)
|
||||
- **Solution**: Run `./scripts/create-v4l2.sh`
|
||||
- Manual: `sudo modprobe v4l2loopback`
|
||||
- Check: `lsmod | grep v4l2loopback`
|
||||
|
||||
#### Stereo mode not working
|
||||
- Ensure 3DS is set to **Outer** cameras with **3D enabled**
|
||||
- Verify server is using `--stereo-mode sbs` (or anaglyph/dual)
|
||||
- Check logs with `--verbose` flag
|
||||
|
||||
For more detailed troubleshooting, see [`docs/troubleshooting.md`](docs/troubleshooting.md).
|
||||
|
||||
---
|
||||
|
||||
## 📚 Documentation
|
||||
|
||||
- **[design.md](docs/design.md)**: Protocol specification, architecture, performance tuning
|
||||
- **[troubleshooting.md](docs/troubleshooting.md)**: Detailed problem-solving guide
|
||||
- **[sample-config.json](examples/sample-config.json)**: Example configuration
|
||||
|
||||
### External Resources
|
||||
|
||||
- **devkitPro**: https://devkitpro.org/wiki/Getting_Started
|
||||
- **libctru Camera API**: https://libctru.devkitpro.org/cam_8h.html
|
||||
- **pyvirtualcam**: https://github.com/letmaik/pyvirtualcam
|
||||
- **v4l2loopback**: https://github.com/umlaeute/v4l2loopback
|
||||
- **stb_image_write**: https://github.com/nothings/stb
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! Please:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Test on both platforms (if possible)
|
||||
5. Submit a pull request
|
||||
|
||||
### Ideas for Contributions
|
||||
|
||||
- macOS support
|
||||
- Config UI for IP entry on 3DS
|
||||
- Recording to file option
|
||||
- H.264 encoding for lower bandwidth
|
||||
- Portrait/landscape rotation
|
||||
- Multiple 3DS clients simultaneously
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
This project is licensed under the **MIT License** - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
### Third-Party Libraries
|
||||
|
||||
- **libctru**: https://github.com/devkitPro/libctru (Zlib license)
|
||||
- **stb_image_write**: https://github.com/nothings/stb (Public domain / MIT)
|
||||
- **pyvirtualcam**: https://github.com/letmaik/pyvirtualcam (GPL-2.0)
|
||||
|
||||
---
|
||||
|
||||
## 🙏 Acknowledgments
|
||||
|
||||
- devkitPro team for the amazing 3DS toolchain
|
||||
- stb libraries by Sean Barrett
|
||||
- v4l2loopback developers
|
||||
- pyvirtualcam and OBS Studio teams
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Performance Tips
|
||||
|
||||
### 3DS Side
|
||||
- Keep 3DS close to Wi-Fi router
|
||||
- Close other applications/homebrew
|
||||
- Use lower resolution if needed (edit source)
|
||||
|
||||
### PC Side
|
||||
- Use wired Ethernet for PC (3DS will be on Wi-Fi)
|
||||
- Close unnecessary applications
|
||||
- For dual mode, ensure both v4l2 devices are created
|
||||
|
||||
### Network
|
||||
- Use QoS to prioritize 3DS traffic if possible
|
||||
- Avoid network congestion (large downloads, streaming)
|
||||
|
||||
---
|
||||
|
||||
**Made with ❤️ for the 3DS homebrew community**
|
||||
+479
@@ -0,0 +1,479 @@
|
||||
# Technical Design Document
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
The 3DS Webcam Bridge consists of two main components:
|
||||
|
||||
1. **3DS Homebrew Application**: Captures camera frames, encodes to JPEG, and streams over TCP
|
||||
2. **PC Server**: Receives frames, decodes, and exposes as virtual webcam(s)
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────┐
|
||||
│ Nintendo 3DS │ │ PC (Server) │
|
||||
│ │ │ │
|
||||
│ ┌───────────┐ │ │ ┌────────────┐ │
|
||||
│ │ Camera │ │ TCP/IP (Wi-Fi) │ │ Server │ │
|
||||
│ │ Capture │ │───────────────────► │ │ (Python) │ │
|
||||
│ └─────┬─────┘ │ Port 9000 │ └──────┬─────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ ┌─────▼─────┐ │ │ ┌──────▼─────┐ │
|
||||
│ │ JPEG │ │ │ │ JPEG │ │
|
||||
│ │ Encoder │ │ │ │ Decoder │ │
|
||||
│ └─────┬─────┘ │ │ └──────┬─────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ ┌─────▼─────┐ │ │ ┌──────▼─────┐ │
|
||||
│ │ Network │ │ │ │ Virtual │ │
|
||||
│ │ Protocol │ │ │ │ Webcam │ │
|
||||
│ └───────────┘ │ │ └────────────┘ │
|
||||
└─────────────────┘ └──────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Protocol Specification
|
||||
|
||||
### Wire Protocol
|
||||
|
||||
The protocol is designed to be backward-compatible and support both mono and stereo streams.
|
||||
|
||||
#### Header Format
|
||||
|
||||
All frames begin with a 2-byte header:
|
||||
|
||||
```
|
||||
[FLAGS (1 byte)][VERSION (1 byte)]
|
||||
```
|
||||
|
||||
**FLAGS**:
|
||||
- Bit 0: Stereo flag (1 = stereo, 0 = mono)
|
||||
- Bits 1-7: Reserved (must be 0)
|
||||
|
||||
**VERSION**: Protocol version (currently 0x01)
|
||||
|
||||
#### Mono Frame Format
|
||||
|
||||
```
|
||||
┌─────────┬─────────┬────────────┬──────────────┐
|
||||
│ FLAGS │ VERSION │ LENGTH │ JPEG DATA │
|
||||
│ (1 byte)│ (1 byte)│ (4 bytes) │ (LENGTH) │
|
||||
│ 0x00 │ 0x01 │ Big-endian │ │
|
||||
└─────────┴─────────┴────────────┴──────────────┘
|
||||
```
|
||||
|
||||
**Total header**: 6 bytes
|
||||
- FLAGS: 0x00 (no stereo)
|
||||
- VERSION: 0x01
|
||||
- LENGTH: 32-bit unsigned big-endian integer (JPEG size)
|
||||
|
||||
#### Stereo Frame Format
|
||||
|
||||
```
|
||||
┌─────────┬─────────┬────────────────────────────────┬─────────────┬──────────────┬─────────────┬──────────────┐
|
||||
│ FLAGS │ VERSION │ DIMENSIONS │ LEFT_LEN │ RIGHT_LEN │ LEFT_JPEG │ RIGHT_JPEG │
|
||||
│ (1 byte)│ (1 byte)│ (8 bytes) │ (4 bytes) │ (4 bytes) │ (LEFT_LEN) │ (RIGHT_LEN) │
|
||||
│ 0x01 │ 0x01 │ wL,hL,wR,hR (16-bit BE each) │ Big-endian │ Big-endian │ │ │
|
||||
└─────────┴─────────┴────────────────────────────────┴─────────────┴──────────────┴─────────────┴──────────────┘
|
||||
```
|
||||
|
||||
**Total header**: 18 bytes
|
||||
- FLAGS: 0x01 (stereo enabled)
|
||||
- VERSION: 0x01
|
||||
- DIMENSIONS:
|
||||
- wL: Left width (16-bit BE)
|
||||
- hL: Left height (16-bit BE)
|
||||
- wR: Right width (16-bit BE)
|
||||
- hR: Right height (16-bit BE)
|
||||
- LEFT_LEN: Left JPEG size (32-bit BE)
|
||||
- RIGHT_LEN: Right JPEG size (32-bit BE)
|
||||
|
||||
### Network Transport
|
||||
|
||||
- **Protocol**: TCP
|
||||
- **Port**: 9000 (default, configurable)
|
||||
- **Connection**: Client (3DS) → Server (PC)
|
||||
- **Reconnection**: Automatic with exponential backoff (3s delay)
|
||||
- **Socket Options**: `TCP_NODELAY` enabled for lower latency
|
||||
|
||||
---
|
||||
|
||||
## 3DS Implementation Details
|
||||
|
||||
### Camera Subsystem
|
||||
|
||||
The 3DS has multiple camera configurations:
|
||||
|
||||
| Camera | Hardware | Purpose | Stereo? |
|
||||
|--------|----------|---------|---------|
|
||||
| **Inner (SELECT_IN1)** | Single lens | Front-facing selfie camera | ❌ No |
|
||||
| **Outer (SELECT_OUT1, SELECT_OUT2)** | Dual lens | Rear stereo cameras | ✅ Yes |
|
||||
|
||||
#### Camera Configuration
|
||||
|
||||
```c
|
||||
CAMU_SetSize(select, SIZE_CTR_TOP_LCD, CONTEXT_A); // 400×240
|
||||
CAMU_SetOutputFormat(select, OUTPUT_YUV_422, CONTEXT_A); // YUV422
|
||||
CAMU_SetFrameRate(select, FRAME_RATE_15); // 15 FPS target
|
||||
CAMU_SetNoiseFilter(select, true); // Reduce noise
|
||||
CAMU_SetAutoExposure(select, true); // Auto exposure
|
||||
CAMU_SetAutoWhiteBalance(select, true); // Auto WB
|
||||
```
|
||||
|
||||
#### Capture Process
|
||||
|
||||
1. **Select camera(s)**: `SELECT_IN1` or `SELECT_OUT1|SELECT_OUT2`
|
||||
2. **Set transfer size**: `CAMU_GetMaxBytes()` → `CAMU_SetTransferBytes()`
|
||||
3. **Clear buffer**: `CAMU_ClearBuffer()`
|
||||
4. **Start capture**: `CAMU_StartCapture()`
|
||||
5. **Set receiving**: `CAMU_SetReceiving()` → returns handle
|
||||
6. **Wait for completion**: `svcWaitSynchronization(handle)`
|
||||
7. **Stop capture**: `CAMU_StopCapture()`
|
||||
|
||||
For stereo, both cameras are captured sequentially but as quickly as possible to minimize desynchronization.
|
||||
|
||||
### Color Space Conversion
|
||||
|
||||
3DS outputs **YUV422** format. We convert to **RGB24** for JPEG encoding:
|
||||
|
||||
```c
|
||||
// YUV422 format: [Y0 U Y1 V] packed in 16-bit values
|
||||
// Conversion matrix:
|
||||
R = 1.164*(Y - 16) + 1.596*(V - 128)
|
||||
G = 1.164*(Y - 16) - 0.391*(U - 128) - 0.813*(V - 128)
|
||||
B = 1.164*(Y - 16) + 2.018*(U - 128)
|
||||
```
|
||||
|
||||
Fixed-point integer math is used for performance:
|
||||
```c
|
||||
R = (298*c + 409*e + 128) >> 8
|
||||
G = (298*c - 100*d - 208*e + 128) >> 8
|
||||
B = (298*c + 516*d + 128) >> 8
|
||||
```
|
||||
|
||||
Where: `c = Y-16`, `d = U-128`, `e = V-128`
|
||||
|
||||
### JPEG Encoding
|
||||
|
||||
We use **stb_image_write.h** (public domain, single-header library):
|
||||
|
||||
```c
|
||||
stbi_write_jpg_to_func(callback, context, width, height, 3, rgb_data, quality);
|
||||
```
|
||||
|
||||
- **Quality**: 65 (configurable via `JPEG_QUALITY` define)
|
||||
- **Typical size**: 10-20 KB per 400×240 frame
|
||||
- **Encoding time**: ~30-50ms on 3DS hardware
|
||||
|
||||
### Threading Model
|
||||
|
||||
```
|
||||
Main Thread:
|
||||
- UI rendering (top/bottom screens)
|
||||
- Input handling
|
||||
- State management
|
||||
|
||||
Stream Thread:
|
||||
- Socket connection/reconnection
|
||||
- Camera capture loop
|
||||
- JPEG encoding
|
||||
- Network transmission
|
||||
- Statistics tracking
|
||||
```
|
||||
|
||||
Synchronization via `LightLock` for shared state access.
|
||||
|
||||
### Performance Characteristics
|
||||
|
||||
| Metric | Mono | Stereo |
|
||||
|--------|------|--------|
|
||||
| **Capture Time** | ~30ms | ~60ms |
|
||||
| **Encode Time** | ~40ms | ~80ms |
|
||||
| **Network Time** | ~20ms | ~40ms |
|
||||
| **Total Latency** | ~90ms | ~180ms |
|
||||
| **Target FPS** | 12-15 | 10-12 |
|
||||
| **Bandwidth** | ~150 KB/s | ~300 KB/s |
|
||||
|
||||
*Note: Actual performance varies based on scene complexity, network conditions, and JPEG quality settings.*
|
||||
|
||||
---
|
||||
|
||||
## PC Server Implementation
|
||||
|
||||
### Backend Architecture
|
||||
|
||||
The server supports multiple virtual camera backends:
|
||||
|
||||
```
|
||||
┌────────────────────────────────────┐
|
||||
│ Server Core (server.py) │
|
||||
│ - Protocol parsing │
|
||||
│ - Frame decoding (JPEG → RGB) │
|
||||
│ - Stereo composition │
|
||||
└──────────────┬─────────────────────┘
|
||||
│
|
||||
┌───────┴────────┐
|
||||
│ │
|
||||
┌───▼───┐ ┌────▼────┐ ┌────────┐
|
||||
│ pyvir │ │ pyfake │ │ ffmpeg │
|
||||
│tualcam│ │ webcam │ │ pipe │
|
||||
└───┬───┘ └────┬────┘ └────┬───┘
|
||||
│ │ │
|
||||
┌───▼────┐ ┌────▼────┐ ┌────▼───┐
|
||||
│Windows │ │ Linux │ │ Linux │
|
||||
│OBS Virt│ │v4l2loop │ │v4l2loop│
|
||||
└────────┘ └─────────┘ └────────┘
|
||||
```
|
||||
|
||||
### Stereo Modes
|
||||
|
||||
#### 1. Off (Mono)
|
||||
- Send left eye only
|
||||
- Resolution: `W × H`
|
||||
- Use case: Standard webcam applications
|
||||
|
||||
#### 2. Side-by-Side (SBS)
|
||||
- Compose horizontally: `[LEFT | RIGHT]`
|
||||
- Resolution: `2W × H`
|
||||
- Use case: 3D displays, VR headsets
|
||||
- Implementation:
|
||||
```python
|
||||
sbs = np.concatenate([left_arr, right_arr], axis=1)
|
||||
```
|
||||
|
||||
#### 3. Anaglyph (Red-Cyan)
|
||||
- Red channel from left, green+blue from right
|
||||
- Resolution: `W × H`
|
||||
- Use case: Red-cyan 3D glasses
|
||||
- Implementation:
|
||||
```python
|
||||
anaglyph[:,:,0] = left[:,:,0] # Red from left
|
||||
anaglyph[:,:,1] = right[:,:,1] # Green from right
|
||||
anaglyph[:,:,2] = right[:,:,2] # Blue from right
|
||||
```
|
||||
|
||||
#### 4. Dual Devices
|
||||
- Two separate virtual cameras
|
||||
- Resolution: `W × H` each
|
||||
- Use case: Advanced VR setups, custom stereo applications
|
||||
|
||||
### Backend Selection Logic
|
||||
|
||||
```
|
||||
User preference: --prefer-backend
|
||||
│
|
||||
├─ pyvirtualcam?
|
||||
│ ├─ Available? → Use it
|
||||
│ └─ Not available → Fallback
|
||||
│
|
||||
├─ pyfakewebcam? (Linux only)
|
||||
│ ├─ Available? → Use it
|
||||
│ └─ Not available → Fallback
|
||||
│
|
||||
└─ ffmpeg? (Linux only)
|
||||
├─ Available? → Use it
|
||||
└─ Not available → Error
|
||||
```
|
||||
|
||||
### Frame Processing Pipeline
|
||||
|
||||
```
|
||||
1. TCP Receive
|
||||
├─ Read 2-byte header
|
||||
├─ Parse FLAGS and VERSION
|
||||
└─ Determine mono/stereo
|
||||
|
||||
2. Decode
|
||||
├─ Mono: Read 4-byte length → JPEG data
|
||||
└─ Stereo: Read 8-byte dims → 8-byte lengths → 2× JPEG data
|
||||
|
||||
3. PIL Decoding
|
||||
├─ JPEG bytes → PIL Image
|
||||
└─ Convert to RGB mode
|
||||
|
||||
4. Composition (if stereo)
|
||||
├─ Off: left only
|
||||
├─ SBS: horizontal concat
|
||||
├─ Anaglyph: color channel merge
|
||||
└─ Dual: process separately
|
||||
|
||||
5. Resize
|
||||
├─ Target resolution (--width, --height)
|
||||
└─ BILINEAR interpolation
|
||||
|
||||
6. NumPy Conversion
|
||||
└─ RGB24 format (uint8, HxWx3)
|
||||
|
||||
7. Backend Send
|
||||
├─ pyvirtualcam: camera.send(frame)
|
||||
├─ pyfakewebcam: camera.schedule_frame(frame)
|
||||
└─ ffmpeg: stdin.write(frame.tobytes())
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Platform-Specific Details
|
||||
|
||||
### Windows: OBS Virtual Camera
|
||||
|
||||
- **Provider**: OBS Studio 26.0+
|
||||
- **Driver**: OBS VirtualCam plugin (bundled)
|
||||
- **Location**: Installed with OBS, typically `C:\Program Files\obs-studio\`
|
||||
- **Access**: Via pyvirtualcam library
|
||||
- **Multiple devices**: User must configure in OBS settings
|
||||
|
||||
**First-time setup**:
|
||||
1. Launch OBS Studio
|
||||
2. Tools → VirtualCam → Start
|
||||
3. This initializes the driver
|
||||
4. Close OBS (driver remains active)
|
||||
|
||||
**Device naming**:
|
||||
- Default: `"OBS Virtual Camera"`
|
||||
- Can be referenced by index: `0`, `1`, etc.
|
||||
|
||||
### Linux: v4l2loopback
|
||||
|
||||
- **Provider**: Kernel module `v4l2loopback`
|
||||
- **Installation**: `sudo apt install v4l2loopback-dkms v4l2loopback-utils`
|
||||
- **Device creation**:
|
||||
```bash
|
||||
sudo modprobe v4l2loopback devices=1 video_nr=0 card_label="3DS Webcam" exclusive_caps=1
|
||||
```
|
||||
|
||||
**Persistent setup** (`/etc/modprobe.d/v4l2loopback.conf`):
|
||||
```
|
||||
options v4l2loopback devices=1 video_nr=0 card_label="3DS Webcam" exclusive_caps=1
|
||||
```
|
||||
|
||||
**Multiple devices**:
|
||||
```bash
|
||||
sudo modprobe v4l2loopback devices=2 video_nr=0,1 \
|
||||
card_label="3DS Left","3DS Right" exclusive_caps=1,1
|
||||
```
|
||||
|
||||
**Verification**:
|
||||
```bash
|
||||
v4l2-ctl --list-devices
|
||||
ls -l /dev/video*
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Tuning
|
||||
|
||||
### Bandwidth Optimization
|
||||
|
||||
**JPEG Quality vs Size**:
|
||||
| Quality | Avg Size (400×240) | Bandwidth @ 15 FPS |
|
||||
|---------|--------------------|--------------------|
|
||||
| 50 | 8 KB | 120 KB/s |
|
||||
| 65 | 12 KB | 180 KB/s |
|
||||
| 80 | 18 KB | 270 KB/s |
|
||||
| 95 | 30 KB | 450 KB/s |
|
||||
|
||||
**Recommendation**: Quality 60-70 for good balance.
|
||||
|
||||
### Resolution Options
|
||||
|
||||
| Resolution | Capture Time | Bandwidth @ Q65 |
|
||||
|------------|--------------|-----------------|
|
||||
| 320×240 | ~25ms | ~140 KB/s |
|
||||
| 400×240 | ~30ms | ~180 KB/s |
|
||||
| 512×384* | N/A | (Unsupported) |
|
||||
|
||||
*Note: 3DS hardware maximum is 640×480, but we use 400×240 for performance.*
|
||||
|
||||
### Network Recommendations
|
||||
|
||||
- **Topology**: Wired PC + wireless 3DS ideal
|
||||
- **Wi-Fi**: 2.4 GHz (3DS limitation), keep 3DS close to AP
|
||||
- **QoS**: Prioritize port 9000 if router supports it
|
||||
- **Interference**: Minimize other 2.4 GHz devices
|
||||
|
||||
### CPU Usage
|
||||
|
||||
**3DS**:
|
||||
- Mono: ~40% CPU (one ARM11 core)
|
||||
- Stereo: ~70% CPU
|
||||
- Leaves headroom for system processes
|
||||
|
||||
**PC**:
|
||||
- Negligible (<5% on modern CPU)
|
||||
- JPEG decode + resize are lightweight
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
### Threat Model
|
||||
|
||||
**In Scope**:
|
||||
- Local network (trusted environment)
|
||||
- Personal/educational use
|
||||
|
||||
**Out of Scope**:
|
||||
- Internet-facing deployment
|
||||
- Untrusted network actors
|
||||
|
||||
### Current Security Posture
|
||||
|
||||
⚠️ **No Encryption**: Traffic is plaintext over TCP
|
||||
⚠️ **No Authentication**: Server accepts any connection
|
||||
⚠️ **No Integrity Checks**: Corrupted data may cause crashes
|
||||
|
||||
### Recommendations for Production Use
|
||||
|
||||
If deploying in a less-trusted environment:
|
||||
|
||||
1. **VPN Tunnel**: Use Wireguard/OpenVPN between 3DS and PC
|
||||
2. **SSH Tunnel**: `ssh -L 9000:localhost:9000 user@server`
|
||||
3. **Firewall Rules**: Restrict to known 3DS IP only
|
||||
4. **Protocol Extension**: Add HMAC for integrity checking
|
||||
|
||||
Example SSH tunnel:
|
||||
```bash
|
||||
# On PC
|
||||
ssh -L 9000:localhost:9000 user@localhost -N
|
||||
|
||||
# On 3DS
|
||||
# Connect to 127.0.0.1:9000 (tunneled)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
### Potential Improvements
|
||||
|
||||
1. **H.264 Encoding**: Lower bandwidth, but requires hardware encoder (limited on 3DS)
|
||||
2. **Adaptive Quality**: Adjust JPEG quality based on network conditions
|
||||
3. **Frame Skipping**: Drop frames under high load to maintain real-time
|
||||
4. **Audio Capture**: Add microphone support (separate audio stream)
|
||||
5. **Recording**: Save to file on PC side
|
||||
6. **Multi-Client**: Support multiple 3DS devices simultaneously
|
||||
7. **WebRTC**: Browser-based viewing without virtual camera
|
||||
|
||||
### Known Limitations
|
||||
|
||||
- **3DS Hardware**: Max resolution 640×480, limited CPU/RAM
|
||||
- **Network Latency**: ~100-200ms typical, inherent in Wi-Fi
|
||||
- **Synchronization**: Stereo frames not hardware-synced (~10ms difference)
|
||||
- **Frame Rate**: Limited by capture/encode pipeline, not true 30 FPS
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
### 3DS Development
|
||||
- libctru documentation: https://libctru.devkitpro.org/
|
||||
- devkitPro: https://devkitpro.org/
|
||||
- 3DS Homebrew: https://www.3dbrew.org/
|
||||
|
||||
### Libraries
|
||||
- stb_image_write: https://github.com/nothings/stb
|
||||
- pyvirtualcam: https://github.com/letmaik/pyvirtualcam
|
||||
- v4l2loopback: https://github.com/umlaeute/v4l2loopback
|
||||
|
||||
### Standards
|
||||
- YUV color space: ITU-R BT.601
|
||||
- JPEG: ITU-T T.81 / ISO/IEC 10918-1
|
||||
- TCP: RFC 793
|
||||
@@ -0,0 +1,733 @@
|
||||
# Troubleshooting Guide
|
||||
|
||||
This guide covers common issues and solutions for the 3DS Webcam Bridge.
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Connection Issues](#connection-issues)
|
||||
- [Virtual Camera Issues](#virtual-camera-issues)
|
||||
- [3DS Application Issues](#3ds-application-issues)
|
||||
- [Performance Issues](#performance-issues)
|
||||
- [Build Issues](#build-issues)
|
||||
- [Platform-Specific Issues](#platform-specific-issues)
|
||||
|
||||
---
|
||||
|
||||
## Connection Issues
|
||||
|
||||
### 3DS Shows "CONNECTING..." But Never Connects
|
||||
|
||||
**Symptoms**: 3DS app shows "CONNECTING..." indefinitely, never reaches "STREAMING" status.
|
||||
|
||||
**Possible Causes**:
|
||||
|
||||
1. **Wrong IP Address**
|
||||
- **Check**: Verify the server IP on your 3DS matches your PC's IP
|
||||
- **Linux**: Run `ip addr show` or `hostname -I`
|
||||
- **Windows**: Run `ipconfig` in Command Prompt, look for IPv4 Address
|
||||
- **Solution**: Update the IP in the 3DS app or config file
|
||||
|
||||
2. **PC Server Not Running**
|
||||
- **Check**: Is `server.py` running on your PC?
|
||||
- **Solution**: Start the server first, then start streaming on 3DS
|
||||
|
||||
3. **Firewall Blocking Connection**
|
||||
- **Linux**:
|
||||
```bash
|
||||
# Check firewall status
|
||||
sudo ufw status
|
||||
|
||||
# Allow port 9000
|
||||
sudo ufw allow 9000/tcp
|
||||
```
|
||||
- **Windows**:
|
||||
```powershell
|
||||
# Run installer as Admin to add firewall rule automatically
|
||||
.\scripts\windows\pc-install.ps1 -Port 9000
|
||||
|
||||
# Or manually add rule in Windows Defender Firewall
|
||||
```
|
||||
|
||||
4. **Different Networks**
|
||||
- **Check**: Ensure 3DS and PC are on the same network
|
||||
- **Solution**: Connect both devices to the same Wi-Fi network
|
||||
|
||||
5. **Wrong Port**
|
||||
- **Check**: Server port matches (default: 9000)
|
||||
- **Solution**: Use same port on both sides, or specify with `--port` flag
|
||||
|
||||
**Verification Steps**:
|
||||
|
||||
```bash
|
||||
# On PC (Linux/Mac)
|
||||
nc -l 9000 # Listen on port 9000
|
||||
|
||||
# On PC (Windows PowerShell)
|
||||
Test-NetConnection -ComputerName localhost -Port 9000
|
||||
|
||||
# Try connecting from 3DS
|
||||
# If connection fails, it's a network/firewall issue
|
||||
```
|
||||
|
||||
### Connection Drops Frequently
|
||||
|
||||
**Symptoms**: Stream starts but disconnects after a few seconds/minutes.
|
||||
|
||||
**Possible Causes**:
|
||||
|
||||
1. **Weak Wi-Fi Signal**
|
||||
- **Solution**: Move 3DS closer to Wi-Fi router
|
||||
- **Solution**: Reduce interference (microwave, cordless phones, etc.)
|
||||
|
||||
2. **Network Congestion**
|
||||
- **Solution**: Pause large downloads/uploads
|
||||
- **Solution**: Use QoS on router to prioritize 3DS
|
||||
|
||||
3. **PC Sleep/Screensaver**
|
||||
- **Solution**: Disable sleep mode while streaming
|
||||
- **Linux**: `sudo systemctl mask sleep.target suspend.target`
|
||||
- **Windows**: Settings → Power & Sleep → Never
|
||||
|
||||
4. **Timeout Issues**
|
||||
- **Solution**: Keep 3DS active (prevent sleep)
|
||||
- **Note**: Auto-reconnect should kick in after 3 seconds
|
||||
|
||||
### "Connection Refused" Error
|
||||
|
||||
**Symptoms**: Server logs show "Connection refused" or similar error.
|
||||
|
||||
**Causes**:
|
||||
- Port already in use
|
||||
- Server crashed
|
||||
|
||||
**Solutions**:
|
||||
|
||||
```bash
|
||||
# Linux: Check what's using port 9000
|
||||
sudo lsof -i :9000
|
||||
sudo netstat -tulpn | grep 9000
|
||||
|
||||
# Kill the process if needed
|
||||
sudo kill <PID>
|
||||
|
||||
# Windows: Check port usage
|
||||
netstat -ano | findstr :9000
|
||||
|
||||
# Kill process by PID
|
||||
taskkill /PID <PID> /F
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Virtual Camera Issues
|
||||
|
||||
### Windows: "No Virtual Camera Found"
|
||||
|
||||
**Symptoms**: Server fails to start or can't find OBS Virtual Camera.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Install OBS Studio**
|
||||
```powershell
|
||||
winget install -e --id OBSProject.OBSStudio
|
||||
```
|
||||
|
||||
2. **Initialize Virtual Camera**
|
||||
- Open OBS Studio
|
||||
- Go to **Tools → VirtualCam → Start**
|
||||
- Wait for confirmation
|
||||
- Close OBS (virtual camera stays active)
|
||||
|
||||
3. **Verify Installation**
|
||||
- Open Camera app (Windows built-in)
|
||||
- Check if "OBS Virtual Camera" appears in device list
|
||||
|
||||
4. **Restart Required**
|
||||
- Sometimes requires system restart after OBS installation
|
||||
|
||||
5. **pyvirtualcam Not Installed**
|
||||
```powershell
|
||||
.venv\Scripts\Activate.ps1
|
||||
pip install pyvirtualcam
|
||||
```
|
||||
|
||||
### Linux: "/dev/video0: Device or Resource Busy"
|
||||
|
||||
**Symptoms**: Server fails to open `/dev/video0`, "device busy" error.
|
||||
|
||||
**Causes**: Another application is using the device.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Find Processes Using Device**
|
||||
```bash
|
||||
sudo fuser /dev/video0
|
||||
```
|
||||
|
||||
2. **Close Applications**
|
||||
- Close browser tabs with camera access
|
||||
- Close Zoom, Discord, etc.
|
||||
- Close any other camera applications
|
||||
|
||||
3. **Force Kill Processes**
|
||||
```bash
|
||||
sudo fuser -k /dev/video0
|
||||
```
|
||||
|
||||
4. **Use Different Device**
|
||||
```bash
|
||||
# List available video devices
|
||||
v4l2-ctl --list-devices
|
||||
|
||||
# Use different device
|
||||
./pc-server/run_server.sh --device /dev/video2
|
||||
```
|
||||
|
||||
### Linux: "/dev/video0: No Such Device"
|
||||
|
||||
**Symptoms**: v4l2loopback device doesn't exist.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Load v4l2loopback Module**
|
||||
```bash
|
||||
./scripts/create-v4l2.sh
|
||||
|
||||
# Or manually
|
||||
sudo modprobe v4l2loopback devices=1 video_nr=0 card_label="3DS Webcam" exclusive_caps=1
|
||||
```
|
||||
|
||||
2. **Verify Module Loaded**
|
||||
```bash
|
||||
lsmod | grep v4l2loopback
|
||||
v4l2-ctl --list-devices
|
||||
```
|
||||
|
||||
3. **Install v4l2loopback**
|
||||
```bash
|
||||
sudo apt install v4l2loopback-dkms v4l2loopback-utils
|
||||
|
||||
# Or run installer
|
||||
./scripts/pc-install.sh
|
||||
```
|
||||
|
||||
4. **Check Device Permissions**
|
||||
```bash
|
||||
ls -l /dev/video*
|
||||
|
||||
# Add user to video group if needed
|
||||
sudo usermod -a -G video $USER
|
||||
# Log out and back in for group to take effect
|
||||
```
|
||||
|
||||
### Virtual Camera Not Showing in Applications
|
||||
|
||||
**Symptoms**: Applications (Zoom, Chrome, etc.) don't see the virtual camera.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Verify Device Exists**
|
||||
- **Linux**: `ls /dev/video*`
|
||||
- **Windows**: Check Camera app
|
||||
|
||||
2. **Restart Application**
|
||||
- Some apps only detect cameras on launch
|
||||
- Close and reopen the application
|
||||
|
||||
3. **Check exclusive_caps**
|
||||
```bash
|
||||
# Linux: Reload with exclusive_caps
|
||||
sudo modprobe -r v4l2loopback
|
||||
sudo modprobe v4l2loopback exclusive_caps=1
|
||||
```
|
||||
|
||||
4. **Browser Permissions**
|
||||
- Grant camera permission in browser settings
|
||||
- Chrome: `chrome://settings/content/camera`
|
||||
- Firefox: `about:preferences#privacy` → Permissions → Camera
|
||||
|
||||
5. **Server Must Be Running**
|
||||
- Virtual camera only works when server is actively sending frames
|
||||
- Start server first, then open application
|
||||
|
||||
---
|
||||
|
||||
## 3DS Application Issues
|
||||
|
||||
### 3DS App Won't Launch / Crashes on Start
|
||||
|
||||
**Symptoms**: App doesn't start or crashes immediately.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check Homebrew Launcher**
|
||||
- Ensure Homebrew Launcher is up to date
|
||||
- Try other homebrew apps to verify it's working
|
||||
|
||||
2. **Verify File Placement**
|
||||
```
|
||||
SD:/3ds/3ds-webcam-bridge/3ds-webcam-bridge.3dsx
|
||||
```
|
||||
|
||||
3. **SD Card Issues**
|
||||
- Check SD card is properly inserted
|
||||
- Verify SD card is not corrupted
|
||||
- Try different SD card if available
|
||||
|
||||
4. **Insufficient Memory**
|
||||
- Close other homebrew apps before launching
|
||||
- Restart 3DS to clear memory
|
||||
|
||||
5. **Build Issues**
|
||||
- Rebuild the app: `./scripts/build-3ds.sh`
|
||||
- Ensure stb_image_write.h is properly downloaded
|
||||
|
||||
### Camera Initialization Failed
|
||||
|
||||
**Symptoms**: App starts but shows camera initialization error.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Close Other Apps**
|
||||
- Some homebrew apps may hold camera resources
|
||||
- Restart 3DS and try again
|
||||
|
||||
2. **Try Different Camera**
|
||||
- Press SELECT to switch between Inner/Outer
|
||||
- One camera may work if the other doesn't
|
||||
|
||||
3. **Camera Hardware Issue**
|
||||
- Test camera with official Camera app
|
||||
- If official app fails, hardware issue likely
|
||||
|
||||
### 3D Mode Checkbox Disabled
|
||||
|
||||
**Symptoms**: Can't enable 3D checkbox.
|
||||
|
||||
**This is Normal**: The 3D checkbox is **intentionally disabled** when Inner camera is selected.
|
||||
|
||||
**Reason**: The inner (front-facing) camera has only one lens and cannot provide stereo 3D.
|
||||
|
||||
**Solution**:
|
||||
1. Press **SELECT** to switch to Outer cameras
|
||||
2. The 3D checkbox will become enabled
|
||||
3. Press **X** to toggle 3D mode
|
||||
|
||||
### Black Screen / No Image
|
||||
|
||||
**Symptoms**: Stream connects but shows black screen on PC.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Check Camera Lens**
|
||||
- Ensure lens cover is removed
|
||||
- Clean lens with soft cloth
|
||||
|
||||
2. **Lighting Conditions**
|
||||
- 3DS cameras need adequate lighting
|
||||
- Try in brighter environment
|
||||
|
||||
3. **Auto-Exposure Issues**
|
||||
- Point camera at evenly-lit scene
|
||||
- Avoid pointing directly at lights
|
||||
|
||||
---
|
||||
|
||||
## Performance Issues
|
||||
|
||||
### Low Frame Rate (< 10 FPS)
|
||||
|
||||
**Symptoms**: Choppy/laggy stream, low FPS reported.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Reduce Resolution** (requires rebuilding)
|
||||
- Edit `3ds-app/source/camera_stream.h`
|
||||
- Change `CAM_WIDTH` to 320 and rebuild
|
||||
- Lower resolution = faster capture/encode
|
||||
|
||||
2. **Lower JPEG Quality**
|
||||
- Edit `3ds-app/source/camera_stream.h`
|
||||
- Change `JPEG_QUALITY` from 65 to 50
|
||||
- Lower quality = faster encoding, less bandwidth
|
||||
|
||||
3. **Network Optimization**
|
||||
- Move 3DS closer to Wi-Fi router
|
||||
- Use 5 GHz Wi-Fi for PC (3DS stays on 2.4 GHz)
|
||||
- Reduce network congestion
|
||||
|
||||
4. **Use Mono Mode**
|
||||
- Disable 3D mode on 3DS
|
||||
- Stereo mode requires 2× bandwidth and processing
|
||||
|
||||
5. **PC Performance**
|
||||
```bash
|
||||
# Check CPU usage
|
||||
top # Linux
|
||||
Task Manager # Windows
|
||||
|
||||
# Close unnecessary applications
|
||||
```
|
||||
|
||||
### High Latency / Delay
|
||||
|
||||
**Symptoms**: Noticeable delay between 3DS movement and PC display.
|
||||
|
||||
**Expected Latency**: 100-200ms is normal due to:
|
||||
- Camera capture: ~30ms
|
||||
- JPEG encoding: ~40ms
|
||||
- Network transmission: ~20-50ms
|
||||
- JPEG decoding: ~10ms
|
||||
- Display pipeline: ~20-40ms
|
||||
|
||||
**To Reduce**:
|
||||
1. Use wired Ethernet for PC
|
||||
2. Minimize Wi-Fi interference
|
||||
3. Lower JPEG quality (trades quality for speed)
|
||||
4. Use mono mode instead of stereo
|
||||
|
||||
### Stuttering / Frame Drops
|
||||
|
||||
**Symptoms**: Uneven frame delivery, periodic freezes.
|
||||
|
||||
**Solutions**:
|
||||
|
||||
1. **Network Issues**
|
||||
- Check Wi-Fi signal strength
|
||||
- Move 3DS closer to router
|
||||
- Change Wi-Fi channel to less congested one
|
||||
|
||||
2. **CPU Throttling**
|
||||
- 3DS may throttle when hot
|
||||
- Let 3DS cool down
|
||||
- Use in cooler environment
|
||||
|
||||
3. **Bandwidth Saturation**
|
||||
- Pause downloads/uploads
|
||||
- Close streaming services
|
||||
- Monitor network with `iftop` (Linux) or Task Manager (Windows)
|
||||
|
||||
---
|
||||
|
||||
## Build Issues
|
||||
|
||||
### "DEVKITARM is not set"
|
||||
|
||||
**Symptoms**: Build fails with environment variable error.
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
# Set environment variables
|
||||
export DEVKITPRO=/opt/devkitpro
|
||||
export DEVKITARM=$DEVKITPRO/devkitARM
|
||||
export PATH=$DEVKITARM/bin:$PATH
|
||||
|
||||
# Add to ~/.bashrc for persistence
|
||||
echo 'export DEVKITPRO=/opt/devkitpro' >> ~/.bashrc
|
||||
echo 'export DEVKITARM=$DEVKITPRO/devkitARM' >> ~/.bashrc
|
||||
echo 'export PATH=$DEVKITARM/bin:$PATH' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
```
|
||||
|
||||
### "stb_image_write.h: No Such File"
|
||||
|
||||
**Symptoms**: Compilation fails, can't find stb_image_write.h.
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
# Download stb_image_write.h
|
||||
wget https://raw.githubusercontent.com/nothings/stb/master/stb_image_write.h \
|
||||
-O 3ds-app/source/third_party/stb_image_write.h
|
||||
|
||||
# Or run build script (downloads automatically)
|
||||
./scripts/build-3ds.sh
|
||||
```
|
||||
|
||||
### "libctru Not Found"
|
||||
|
||||
**Symptoms**: Linker errors, missing libctru symbols.
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
# Install libctru
|
||||
sudo dkp-pacman -S libctru
|
||||
|
||||
# Or run full installer
|
||||
./scripts/install-devkitpro.sh
|
||||
```
|
||||
|
||||
### Build Succeeds But .3dsx Not Created
|
||||
|
||||
**Symptoms**: Make completes but no output file.
|
||||
|
||||
**Check**:
|
||||
|
||||
```bash
|
||||
# Look in dist directory
|
||||
ls -la dist/
|
||||
|
||||
# Check for errors in make output
|
||||
./scripts/build-3ds.sh 2>&1 | tee build.log
|
||||
grep -i error build.log
|
||||
```
|
||||
|
||||
**Solution**:
|
||||
- Ensure `dist/` directory exists: `mkdir -p dist`
|
||||
- Check Makefile paths
|
||||
- Verify devkitARM installation
|
||||
|
||||
---
|
||||
|
||||
## Platform-Specific Issues
|
||||
|
||||
### Windows: "winget: command not found"
|
||||
|
||||
**Symptoms**: Installer script fails, winget not available.
|
||||
|
||||
**Solution**:
|
||||
|
||||
1. **Install App Installer** from Microsoft Store
|
||||
2. **Or download manually**: https://github.com/microsoft/winget-cli/releases
|
||||
3. **Update Windows**: winget requires Windows 10 1809 or later
|
||||
|
||||
### Windows: PowerShell Execution Policy Error
|
||||
|
||||
**Symptoms**: Scripts won't run, "execution policy" error.
|
||||
|
||||
**Solution**:
|
||||
|
||||
```powershell
|
||||
# Temporary (current session only)
|
||||
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||
|
||||
# Or run scripts with bypass flag
|
||||
powershell -ExecutionPolicy Bypass -File .\scripts\windows\pc-install.ps1
|
||||
```
|
||||
|
||||
### Linux: Permission Denied on Scripts
|
||||
|
||||
**Symptoms**: Can't execute `.sh` scripts.
|
||||
|
||||
**Solution**:
|
||||
|
||||
```bash
|
||||
# Make scripts executable
|
||||
chmod +x scripts/*.sh
|
||||
chmod +x pc-server/run_server.sh
|
||||
|
||||
# Or run with bash
|
||||
bash scripts/build-3ds.sh
|
||||
```
|
||||
|
||||
### Linux: "apt-get: command not found"
|
||||
|
||||
**Symptoms**: Installer fails on non-Debian systems.
|
||||
|
||||
**Solution** (for other distros):
|
||||
|
||||
**Fedora/RHEL**:
|
||||
```bash
|
||||
sudo dnf install v4l2loopback akmod-v4l2loopback ffmpeg python3 python3-pip
|
||||
```
|
||||
|
||||
**Arch**:
|
||||
```bash
|
||||
sudo pacman -S v4l2loopback-dkms ffmpeg python python-pip
|
||||
```
|
||||
|
||||
Then manually create venv and install Python packages:
|
||||
```bash
|
||||
python3 -m venv .venv
|
||||
source .venv/bin/activate
|
||||
pip install -r pc-server/requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Diagnostic Commands
|
||||
|
||||
### Check System Status
|
||||
|
||||
**Linux**:
|
||||
```bash
|
||||
# Check v4l2loopback
|
||||
lsmod | grep v4l2loopback
|
||||
v4l2-ctl --list-devices
|
||||
|
||||
# Check network
|
||||
ip addr show
|
||||
ss -tulpn | grep 9000
|
||||
|
||||
# Test virtual camera
|
||||
ffplay /dev/video0 # Requires ffmpeg
|
||||
|
||||
# Check Python environment
|
||||
source .venv/bin/activate
|
||||
python --version
|
||||
pip list | grep -E "pyvirtualcam|pillow|numpy"
|
||||
```
|
||||
|
||||
**Windows**:
|
||||
```powershell
|
||||
# Check OBS installation
|
||||
Test-Path "C:\Program Files\obs-studio\bin\64bit\obs64.exe"
|
||||
|
||||
# Check Python
|
||||
python --version
|
||||
pip list | findstr /I "pyvirtualcam pillow numpy"
|
||||
|
||||
# Check network
|
||||
ipconfig
|
||||
netstat -ano | findstr :9000
|
||||
|
||||
# Test virtual camera
|
||||
# Open Camera app and check device list
|
||||
```
|
||||
|
||||
### Enable Verbose Logging
|
||||
|
||||
```bash
|
||||
# Linux
|
||||
./pc-server/run_server.sh --verbose
|
||||
|
||||
# Windows
|
||||
.\pc-server\run_server.ps1 -Verbose
|
||||
```
|
||||
|
||||
This will show detailed frame information:
|
||||
- Frame type (mono/stereo)
|
||||
- JPEG sizes
|
||||
- Dimensions
|
||||
- Processing time
|
||||
|
||||
---
|
||||
|
||||
## Getting Help
|
||||
|
||||
### Before Asking for Help
|
||||
|
||||
1. **Check this guide** for your specific issue
|
||||
2. **Review logs** with `--verbose` flag
|
||||
3. **Verify requirements** (network, versions, etc.)
|
||||
4. **Try basic troubleshooting** (restart, reconnect, etc.)
|
||||
|
||||
### When Reporting Issues
|
||||
|
||||
Include:
|
||||
|
||||
1. **Platform Information**
|
||||
- OS (Ubuntu 22.04, Windows 11, etc.)
|
||||
- Python version: `python --version`
|
||||
- 3DS model (Old 3DS, New 3DS XL, etc.)
|
||||
|
||||
2. **Error Messages**
|
||||
- Full error output
|
||||
- Verbose logs if available
|
||||
|
||||
3. **Steps to Reproduce**
|
||||
- What were you doing when it failed?
|
||||
- Can you reproduce it consistently?
|
||||
|
||||
4. **Configuration**
|
||||
- Command line arguments used
|
||||
- Config file contents (if any)
|
||||
- Network topology (router, switches, etc.)
|
||||
|
||||
5. **What You've Tried**
|
||||
- Troubleshooting steps already attempted
|
||||
- Results of those attempts
|
||||
|
||||
### Community Resources
|
||||
|
||||
- **GitHub Issues**: https://github.com/your-username/3ds-webcam-bridge/issues
|
||||
- **3DS Homebrew Community**: https://www.3dbrew.org/
|
||||
- **devkitPro Forums**: https://devkitpro.org/viewforum.php
|
||||
|
||||
---
|
||||
|
||||
## Known Issues & Workarounds
|
||||
|
||||
### Issue: Stereo Frames Slightly Desynchronized
|
||||
|
||||
**Impact**: Left and right frames may be 1-2 frames apart.
|
||||
|
||||
**Workaround**: None currently. This is due to sequential capture. Impact is minimal (<50ms difference).
|
||||
|
||||
**Future Fix**: Hardware-synchronized capture (if possible via libctru extensions).
|
||||
|
||||
### Issue: Windows Dual Mode Requires Manual OBS Config
|
||||
|
||||
**Impact**: Can't easily create multiple virtual cameras on Windows.
|
||||
|
||||
**Workaround**:
|
||||
1. Use OBS plugins to create multiple virtual cameras
|
||||
2. Or use side-by-side mode instead
|
||||
|
||||
**Future Fix**: Investigate alternative virtual camera drivers for Windows.
|
||||
|
||||
### Issue: 3DS Sleep Mode Disconnects
|
||||
|
||||
**Impact**: 3DS entering sleep mode kills the stream.
|
||||
|
||||
**Workaround**: Keep 3DS active, disable auto-sleep in system settings.
|
||||
|
||||
**Future Fix**: Detect sleep and reconnect automatically.
|
||||
|
||||
---
|
||||
|
||||
## Advanced Troubleshooting
|
||||
|
||||
### Network Packet Capture
|
||||
|
||||
**Linux**:
|
||||
```bash
|
||||
# Capture traffic on port 9000
|
||||
sudo tcpdump -i any -w capture.pcap port 9000
|
||||
|
||||
# Analyze with Wireshark
|
||||
wireshark capture.pcap
|
||||
```
|
||||
|
||||
**Windows**:
|
||||
```powershell
|
||||
# Use Wireshark directly
|
||||
# Filter: tcp.port == 9000
|
||||
```
|
||||
|
||||
### Debugging Python Server
|
||||
|
||||
```bash
|
||||
# Run with Python debugger
|
||||
python -m pdb pc-server/server.py --verbose
|
||||
|
||||
# Or add breakpoints in code
|
||||
import pdb; pdb.set_trace()
|
||||
```
|
||||
|
||||
### Testing Without 3DS
|
||||
|
||||
Create a test client to verify server:
|
||||
|
||||
```python
|
||||
import socket
|
||||
import struct
|
||||
|
||||
sock = socket.socket()
|
||||
sock.connect(('localhost', 9000))
|
||||
|
||||
# Send mono frame header
|
||||
header = b'\x00\x01' + struct.pack('>I', 1024) # 1KB test frame
|
||||
sock.send(header)
|
||||
sock.send(b'\xff\xd8' + b'\x00' * 1022 + b'\xff\xd9') # Fake JPEG
|
||||
|
||||
sock.close()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-10-28
|
||||
@@ -0,0 +1,11 @@
|
||||
# 3DS Webcam Bridge - Configuration File
|
||||
# Place this file at: SD:/3ds/3ds-webcam-bridge/config.txt
|
||||
#
|
||||
# You can also edit IP/Port by pressing Y button in the app,
|
||||
# which will automatically save to this file.
|
||||
|
||||
# Server IP address (your PC's IP)
|
||||
ip=192.168.1.100
|
||||
|
||||
# Server port
|
||||
port=9000
|
||||
@@ -0,0 +1,4 @@
|
||||
pillow>=10.0.0
|
||||
numpy>=1.24.0
|
||||
pyvirtualcam>=0.11.0
|
||||
pyfakewebcam>=0.1.0; platform_system == "Linux"
|
||||
@@ -0,0 +1,55 @@
|
||||
# Run the 3DS Webcam Bridge PC server (Windows)
|
||||
param(
|
||||
[string]$Listen = "0.0.0.0",
|
||||
[int]$Port = 9000,
|
||||
[string]$StereoMode = "off",
|
||||
[string]$Device = $null,
|
||||
[string]$DualDevices = $null,
|
||||
[int]$Width = 640,
|
||||
[int]$Height = 480,
|
||||
[int]$Fps = 15,
|
||||
[string]$PreferBackend = "pyvirtualcam",
|
||||
[switch]$Verbose
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$VenvDir = Join-Path (Split-Path -Parent $ScriptDir) ".venv"
|
||||
$VenvActivate = Join-Path $VenvDir "Scripts\Activate.ps1"
|
||||
|
||||
# Activate virtual environment if it exists
|
||||
if (Test-Path $VenvActivate) {
|
||||
Write-Host "Activating virtual environment..."
|
||||
& $VenvActivate
|
||||
} else {
|
||||
Write-Warning "Virtual environment not found at $VenvDir"
|
||||
Write-Warning "Run ..\scripts\windows\pc-install.ps1 first to set up dependencies"
|
||||
}
|
||||
|
||||
# Build command line arguments
|
||||
$Args = @(
|
||||
"--listen", $Listen,
|
||||
"--port", $Port,
|
||||
"--stereo-mode", $StereoMode,
|
||||
"--width", $Width,
|
||||
"--height", $Height,
|
||||
"--fps", $Fps,
|
||||
"--prefer-backend", $PreferBackend
|
||||
)
|
||||
|
||||
if ($Device) {
|
||||
$Args += "--device"
|
||||
$Args += $Device
|
||||
}
|
||||
|
||||
if ($DualDevices) {
|
||||
$Args += "--dual-devices"
|
||||
$Args += $DualDevices
|
||||
}
|
||||
|
||||
if ($Verbose) {
|
||||
$Args += "--verbose"
|
||||
}
|
||||
|
||||
# Run server
|
||||
$ServerScript = Join-Path $ScriptDir "server.py"
|
||||
python $ServerScript @Args
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
# Run the 3DS Webcam Bridge PC server (Linux)
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
VENV_DIR="$SCRIPT_DIR/../.venv"
|
||||
|
||||
# Activate virtual environment if it exists
|
||||
if [ -d "$VENV_DIR" ]; then
|
||||
source "$VENV_DIR/bin/activate"
|
||||
else
|
||||
echo "Warning: Virtual environment not found at $VENV_DIR"
|
||||
echo "Run ../scripts/pc-install.sh first to set up dependencies"
|
||||
fi
|
||||
|
||||
# Run server with provided arguments or defaults
|
||||
python3 "$SCRIPT_DIR/server.py" "$@"
|
||||
@@ -0,0 +1,599 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
3DS Webcam Bridge - PC Server
|
||||
Receives camera streams from 3DS and exposes them as virtual webcams.
|
||||
Supports mono and stereo modes with multiple output configurations.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import io
|
||||
import socket
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
import platform
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
# Backend selection
|
||||
BACKEND_PYVIRTUALCAM = "pyvirtualcam"
|
||||
BACKEND_PYFAKEWEBCAM = "pyfakewebcam"
|
||||
BACKEND_FFMPEG = "ffmpeg"
|
||||
|
||||
# Protocol constants
|
||||
PROTO_VERSION = 0x01
|
||||
PROTO_FLAG_STEREO = 0x01
|
||||
|
||||
# Global state
|
||||
g_verbose = False
|
||||
|
||||
|
||||
def log(msg):
|
||||
"""Print log message."""
|
||||
print(f"[INFO] {msg}")
|
||||
|
||||
|
||||
def log_verbose(msg):
|
||||
"""Print verbose log message."""
|
||||
if g_verbose:
|
||||
print(f"[VERBOSE] {msg}")
|
||||
|
||||
|
||||
def log_error(msg):
|
||||
"""Print error message."""
|
||||
print(f"[ERROR] {msg}", file=sys.stderr)
|
||||
|
||||
|
||||
def recv_all(conn: socket.socket, n: int) -> bytes:
|
||||
"""Receive exactly n bytes from socket."""
|
||||
data = b''
|
||||
while len(data) < n:
|
||||
chunk = conn.recv(n - len(data))
|
||||
if not chunk:
|
||||
raise ConnectionError("Connection closed by client")
|
||||
data += chunk
|
||||
return data
|
||||
|
||||
|
||||
def read_frame(conn: socket.socket) -> Tuple[Optional[Image.Image], Optional[Image.Image]]:
|
||||
"""
|
||||
Read a frame from the 3DS.
|
||||
Returns (left_image, right_image) where right_image is None for mono mode.
|
||||
"""
|
||||
# Read header
|
||||
header = recv_all(conn, 2)
|
||||
flags = header[0]
|
||||
version = header[1]
|
||||
|
||||
if version != PROTO_VERSION:
|
||||
log_error(f"Unsupported protocol version: {version}")
|
||||
return None, None
|
||||
|
||||
stereo = (flags & PROTO_FLAG_STEREO) != 0
|
||||
|
||||
if not stereo:
|
||||
# Mono frame: [4-byte length][JPEG]
|
||||
length_bytes = recv_all(conn, 4)
|
||||
length = struct.unpack('>I', length_bytes)[0]
|
||||
|
||||
log_verbose(f"Receiving mono frame: {length} bytes")
|
||||
|
||||
jpeg_data = recv_all(conn, length)
|
||||
left_img = Image.open(io.BytesIO(jpeg_data)).convert('RGB')
|
||||
|
||||
return left_img, None
|
||||
else:
|
||||
# Stereo frame: [2B wL][2B hL][2B wR][2B hR][4B left_len][4B right_len][left_jpeg][right_jpeg]
|
||||
dims = recv_all(conn, 8)
|
||||
wL, hL, wR, hR = struct.unpack('>HHHH', dims)
|
||||
|
||||
lengths = recv_all(conn, 8)
|
||||
left_len, right_len = struct.unpack('>II', lengths)
|
||||
|
||||
log_verbose(f"Receiving stereo frame: left={wL}x{hL} ({left_len} bytes), right={wR}x{hR} ({right_len} bytes)")
|
||||
|
||||
left_data = recv_all(conn, left_len)
|
||||
right_data = recv_all(conn, right_len)
|
||||
|
||||
left_img = Image.open(io.BytesIO(left_data)).convert('RGB')
|
||||
right_img = Image.open(io.BytesIO(right_data)).convert('RGB')
|
||||
|
||||
return left_img, right_img
|
||||
|
||||
|
||||
def compose_sbs(left: Image.Image, right: Image.Image, width: int, height: int) -> np.ndarray:
|
||||
"""Compose side-by-side stereo image."""
|
||||
left_resized = left.resize((width, height), Image.BILINEAR)
|
||||
right_resized = right.resize((width, height), Image.BILINEAR)
|
||||
|
||||
left_arr = np.array(left_resized)
|
||||
right_arr = np.array(right_resized)
|
||||
|
||||
# Concatenate horizontally
|
||||
sbs = np.concatenate([left_arr, right_arr], axis=1)
|
||||
return sbs
|
||||
|
||||
|
||||
def compose_anaglyph(left: Image.Image, right: Image.Image, width: int, height: int) -> np.ndarray:
|
||||
"""Compose red-cyan anaglyph image."""
|
||||
left_resized = left.resize((width, height), Image.BILINEAR)
|
||||
right_resized = right.resize((width, height), Image.BILINEAR)
|
||||
|
||||
left_arr = np.array(left_resized)
|
||||
right_arr = np.array(right_resized)
|
||||
|
||||
# Red-cyan anaglyph: red from left, green+blue from right
|
||||
anaglyph = np.zeros_like(left_arr)
|
||||
anaglyph[:, :, 0] = left_arr[:, :, 0] # Red from left
|
||||
anaglyph[:, :, 1] = right_arr[:, :, 1] # Green from right
|
||||
anaglyph[:, :, 2] = right_arr[:, :, 2] # Blue from right
|
||||
|
||||
return anaglyph
|
||||
|
||||
|
||||
class VirtualCameraBackend:
|
||||
"""Base class for virtual camera backends."""
|
||||
|
||||
def __init__(self, args):
|
||||
self.args = args
|
||||
|
||||
def send_frame(self, frame: np.ndarray):
|
||||
"""Send a frame to the virtual camera."""
|
||||
raise NotImplementedError
|
||||
|
||||
def send_dual_frames(self, left: np.ndarray, right: np.ndarray):
|
||||
"""Send dual frames (for dual device mode)."""
|
||||
raise NotImplementedError
|
||||
|
||||
def close(self):
|
||||
"""Close the backend."""
|
||||
pass
|
||||
|
||||
|
||||
class PyVirtualCamBackend(VirtualCameraBackend):
|
||||
"""Backend using pyvirtualcam."""
|
||||
|
||||
def __init__(self, args):
|
||||
super().__init__(args)
|
||||
|
||||
try:
|
||||
import pyvirtualcam
|
||||
self.pyvirtualcam = pyvirtualcam
|
||||
except ImportError:
|
||||
raise RuntimeError("pyvirtualcam is not installed. Install with: pip install pyvirtualcam")
|
||||
|
||||
self.camera = None
|
||||
self.camera_left = None
|
||||
self.camera_right = None
|
||||
|
||||
def _open_single_camera(self, width, height, fps, device=None):
|
||||
"""Open a single pyvirtualcam device."""
|
||||
kwargs = {
|
||||
'width': width,
|
||||
'height': height,
|
||||
'fps': fps,
|
||||
'print_fps': g_verbose
|
||||
}
|
||||
|
||||
if device is not None:
|
||||
# Device can be a name (string) or index (int)
|
||||
try:
|
||||
device_int = int(device)
|
||||
kwargs['device'] = device_int
|
||||
except ValueError:
|
||||
kwargs['device'] = device
|
||||
|
||||
return self.pyvirtualcam.Camera(**kwargs)
|
||||
|
||||
def setup_single(self):
|
||||
"""Setup single camera output."""
|
||||
width = self.args.width
|
||||
height = self.args.height
|
||||
|
||||
# For SBS mode, double the width
|
||||
if self.args.stereo_mode == 'sbs':
|
||||
width = width * 2
|
||||
|
||||
self.camera = self._open_single_camera(
|
||||
width=width,
|
||||
height=height,
|
||||
fps=self.args.fps,
|
||||
device=self.args.device
|
||||
)
|
||||
log(f"Opened virtual camera: {width}x{height} @ {self.args.fps} FPS")
|
||||
|
||||
def setup_dual(self):
|
||||
"""Setup dual camera output."""
|
||||
devices = self.args.dual_devices.split(',')
|
||||
if len(devices) != 2:
|
||||
raise ValueError("dual_devices must specify exactly 2 devices separated by comma")
|
||||
|
||||
left_device = devices[0].strip()
|
||||
right_device = devices[1].strip()
|
||||
|
||||
self.camera_left = self._open_single_camera(
|
||||
width=self.args.width,
|
||||
height=self.args.height,
|
||||
fps=self.args.fps,
|
||||
device=left_device if left_device else None
|
||||
)
|
||||
|
||||
self.camera_right = self._open_single_camera(
|
||||
width=self.args.width,
|
||||
height=self.args.height,
|
||||
fps=self.args.fps,
|
||||
device=right_device if right_device else None
|
||||
)
|
||||
|
||||
log(f"Opened dual virtual cameras: {self.args.width}x{self.args.height} @ {self.args.fps} FPS")
|
||||
|
||||
def send_frame(self, frame: np.ndarray):
|
||||
"""Send frame to single camera."""
|
||||
if self.camera is None:
|
||||
self.setup_single()
|
||||
self.camera.send(frame)
|
||||
|
||||
def send_dual_frames(self, left: np.ndarray, right: np.ndarray):
|
||||
"""Send frames to dual cameras."""
|
||||
if self.camera_left is None or self.camera_right is None:
|
||||
self.setup_dual()
|
||||
self.camera_left.send(left)
|
||||
self.camera_right.send(right)
|
||||
|
||||
def close(self):
|
||||
"""Close cameras."""
|
||||
if self.camera:
|
||||
self.camera.close()
|
||||
if self.camera_left:
|
||||
self.camera_left.close()
|
||||
if self.camera_right:
|
||||
self.camera_right.close()
|
||||
|
||||
|
||||
class PyFakeWebcamBackend(VirtualCameraBackend):
|
||||
"""Backend using pyfakewebcam (Linux only)."""
|
||||
|
||||
def __init__(self, args):
|
||||
super().__init__(args)
|
||||
|
||||
if platform.system() != 'Linux':
|
||||
raise RuntimeError("pyfakewebcam is only available on Linux")
|
||||
|
||||
try:
|
||||
import pyfakewebcam
|
||||
self.pyfakewebcam = pyfakewebcam
|
||||
except ImportError:
|
||||
raise RuntimeError("pyfakewebcam is not installed. Install with: pip install pyfakewebcam")
|
||||
|
||||
self.camera = None
|
||||
self.camera_left = None
|
||||
self.camera_right = None
|
||||
|
||||
def setup_single(self):
|
||||
"""Setup single camera output."""
|
||||
width = self.args.width
|
||||
height = self.args.height
|
||||
|
||||
if self.args.stereo_mode == 'sbs':
|
||||
width = width * 2
|
||||
|
||||
device = self.args.device if self.args.device else '/dev/video0'
|
||||
self.camera = self.pyfakewebcam.FakeWebcam(device, width, height)
|
||||
log(f"Opened pyfakewebcam device: {device} ({width}x{height})")
|
||||
|
||||
def setup_dual(self):
|
||||
"""Setup dual camera output."""
|
||||
devices = self.args.dual_devices.split(',')
|
||||
if len(devices) != 2:
|
||||
raise ValueError("dual_devices must specify exactly 2 devices separated by comma")
|
||||
|
||||
self.camera_left = self.pyfakewebcam.FakeWebcam(
|
||||
devices[0].strip(), self.args.width, self.args.height)
|
||||
self.camera_right = self.pyfakewebcam.FakeWebcam(
|
||||
devices[1].strip(), self.args.width, self.args.height)
|
||||
log(f"Opened dual pyfakewebcam devices: {devices[0]}, {devices[1]}")
|
||||
|
||||
def send_frame(self, frame: np.ndarray):
|
||||
"""Send frame to single camera."""
|
||||
if self.camera is None:
|
||||
self.setup_single()
|
||||
self.camera.schedule_frame(frame)
|
||||
|
||||
def send_dual_frames(self, left: np.ndarray, right: np.ndarray):
|
||||
"""Send frames to dual cameras."""
|
||||
if self.camera_left is None or self.camera_right is None:
|
||||
self.setup_dual()
|
||||
self.camera_left.schedule_frame(left)
|
||||
self.camera_right.schedule_frame(right)
|
||||
|
||||
|
||||
class FFmpegBackend(VirtualCameraBackend):
|
||||
"""Backend using ffmpeg pipe to v4l2 (Linux only)."""
|
||||
|
||||
def __init__(self, args):
|
||||
super().__init__(args)
|
||||
|
||||
if platform.system() != 'Linux':
|
||||
raise RuntimeError("ffmpeg backend is only available on Linux")
|
||||
|
||||
import subprocess
|
||||
self.subprocess = subprocess
|
||||
self.process = None
|
||||
self.process_left = None
|
||||
self.process_right = None
|
||||
|
||||
def setup_single(self):
|
||||
"""Setup single ffmpeg output."""
|
||||
width = self.args.width
|
||||
height = self.args.height
|
||||
|
||||
if self.args.stereo_mode == 'sbs':
|
||||
width = width * 2
|
||||
|
||||
device = self.args.device if self.args.device else '/dev/video0'
|
||||
|
||||
cmd = [
|
||||
'ffmpeg',
|
||||
'-f', 'rawvideo',
|
||||
'-pixel_format', 'rgb24',
|
||||
'-video_size', f'{width}x{height}',
|
||||
'-framerate', str(self.args.fps),
|
||||
'-i', '-',
|
||||
'-f', 'v4l2',
|
||||
device
|
||||
]
|
||||
|
||||
self.process = self.subprocess.Popen(cmd, stdin=self.subprocess.PIPE)
|
||||
log(f"Opened ffmpeg pipe to {device} ({width}x{height})")
|
||||
|
||||
def setup_dual(self):
|
||||
"""Setup dual ffmpeg output."""
|
||||
devices = self.args.dual_devices.split(',')
|
||||
if len(devices) != 2:
|
||||
raise ValueError("dual_devices must specify exactly 2 devices")
|
||||
|
||||
cmd_template = [
|
||||
'ffmpeg',
|
||||
'-f', 'rawvideo',
|
||||
'-pixel_format', 'rgb24',
|
||||
'-video_size', f'{self.args.width}x{self.args.height}',
|
||||
'-framerate', str(self.args.fps),
|
||||
'-i', '-',
|
||||
'-f', 'v4l2',
|
||||
None # Device placeholder
|
||||
]
|
||||
|
||||
cmd_left = cmd_template[:]
|
||||
cmd_left[-1] = devices[0].strip()
|
||||
self.process_left = self.subprocess.Popen(cmd_left, stdin=self.subprocess.PIPE)
|
||||
|
||||
cmd_right = cmd_template[:]
|
||||
cmd_right[-1] = devices[1].strip()
|
||||
self.process_right = self.subprocess.Popen(cmd_right, stdin=self.subprocess.PIPE)
|
||||
|
||||
log(f"Opened dual ffmpeg pipes to {devices[0]}, {devices[1]}")
|
||||
|
||||
def send_frame(self, frame: np.ndarray):
|
||||
"""Send frame via ffmpeg."""
|
||||
if self.process is None:
|
||||
self.setup_single()
|
||||
self.process.stdin.write(frame.tobytes())
|
||||
self.process.stdin.flush()
|
||||
|
||||
def send_dual_frames(self, left: np.ndarray, right: np.ndarray):
|
||||
"""Send frames via dual ffmpeg pipes."""
|
||||
if self.process_left is None or self.process_right is None:
|
||||
self.setup_dual()
|
||||
self.process_left.stdin.write(left.tobytes())
|
||||
self.process_left.stdin.flush()
|
||||
self.process_right.stdin.write(right.tobytes())
|
||||
self.process_right.stdin.flush()
|
||||
|
||||
def close(self):
|
||||
"""Close ffmpeg processes."""
|
||||
if self.process:
|
||||
self.process.stdin.close()
|
||||
self.process.wait()
|
||||
if self.process_left:
|
||||
self.process_left.stdin.close()
|
||||
self.process_left.wait()
|
||||
if self.process_right:
|
||||
self.process_right.stdin.close()
|
||||
self.process_right.wait()
|
||||
|
||||
|
||||
def create_backend(args) -> VirtualCameraBackend:
|
||||
"""Create appropriate backend based on preference."""
|
||||
backend_name = args.prefer_backend
|
||||
|
||||
if backend_name == BACKEND_PYVIRTUALCAM:
|
||||
try:
|
||||
return PyVirtualCamBackend(args)
|
||||
except RuntimeError as e:
|
||||
log_error(f"Failed to create pyvirtualcam backend: {e}")
|
||||
if platform.system() == 'Linux':
|
||||
log("Falling back to pyfakewebcam...")
|
||||
backend_name = BACKEND_PYFAKEWEBCAM
|
||||
else:
|
||||
raise
|
||||
|
||||
if backend_name == BACKEND_PYFAKEWEBCAM:
|
||||
try:
|
||||
return PyFakeWebcamBackend(args)
|
||||
except RuntimeError as e:
|
||||
log_error(f"Failed to create pyfakewebcam backend: {e}")
|
||||
log("Falling back to ffmpeg...")
|
||||
backend_name = BACKEND_FFMPEG
|
||||
|
||||
if backend_name == BACKEND_FFMPEG:
|
||||
return FFmpegBackend(args)
|
||||
|
||||
raise RuntimeError(f"No suitable backend found")
|
||||
|
||||
|
||||
def process_stream(conn: socket.socket, addr, args):
|
||||
"""Process incoming stream from 3DS."""
|
||||
log(f"Client connected from {addr}")
|
||||
|
||||
backend = create_backend(args)
|
||||
frame_count = 0
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
while True:
|
||||
# Read frame from 3DS
|
||||
left_img, right_img = read_frame(conn)
|
||||
|
||||
if left_img is None:
|
||||
break
|
||||
|
||||
# Process based on mode
|
||||
if right_img is None:
|
||||
# Mono mode
|
||||
log_verbose("Processing mono frame")
|
||||
|
||||
left_resized = left_img.resize((args.width, args.height), Image.BILINEAR)
|
||||
frame = np.array(left_resized)
|
||||
|
||||
backend.send_frame(frame)
|
||||
|
||||
else:
|
||||
# Stereo mode
|
||||
log_verbose("Processing stereo frame")
|
||||
|
||||
if args.stereo_mode == 'off':
|
||||
# Send left eye only
|
||||
left_resized = left_img.resize((args.width, args.height), Image.BILINEAR)
|
||||
frame = np.array(left_resized)
|
||||
backend.send_frame(frame)
|
||||
|
||||
elif args.stereo_mode == 'sbs':
|
||||
# Side-by-side
|
||||
frame = compose_sbs(left_img, right_img, args.width, args.height)
|
||||
backend.send_frame(frame)
|
||||
|
||||
elif args.stereo_mode == 'anaglyph':
|
||||
# Red-cyan anaglyph
|
||||
frame = compose_anaglyph(left_img, right_img, args.width, args.height)
|
||||
backend.send_frame(frame)
|
||||
|
||||
elif args.stereo_mode == 'dual':
|
||||
# Dual devices
|
||||
left_resized = left_img.resize((args.width, args.height), Image.BILINEAR)
|
||||
right_resized = right_img.resize((args.width, args.height), Image.BILINEAR)
|
||||
left_arr = np.array(left_resized)
|
||||
right_arr = np.array(right_resized)
|
||||
backend.send_dual_frames(left_arr, right_arr)
|
||||
|
||||
frame_count += 1
|
||||
|
||||
if frame_count % 30 == 0:
|
||||
elapsed = time.time() - start_time
|
||||
fps = frame_count / elapsed
|
||||
log(f"Processed {frame_count} frames ({fps:.1f} FPS)")
|
||||
|
||||
except ConnectionError as e:
|
||||
log_error(f"Connection error: {e}")
|
||||
except KeyboardInterrupt:
|
||||
log("Interrupted by user")
|
||||
except Exception as e:
|
||||
log_error(f"Error processing stream: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
backend.close()
|
||||
log(f"Client disconnected: {addr}")
|
||||
|
||||
|
||||
def main():
|
||||
global g_verbose
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='3DS Webcam Bridge - PC Server',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
# Linux - mono mode with default settings
|
||||
%(prog)s --device /dev/video0
|
||||
|
||||
# Linux - side-by-side stereo
|
||||
%(prog)s --stereo-mode sbs --width 400 --height 240 --device /dev/video0
|
||||
|
||||
# Linux - dual devices for left/right eyes
|
||||
%(prog)s --stereo-mode dual --dual-devices /dev/video0,/dev/video1
|
||||
|
||||
# Windows - side-by-side with OBS Virtual Camera
|
||||
%(prog)s --stereo-mode sbs --device "OBS Virtual Camera"
|
||||
|
||||
# Windows - dual OBS virtual cameras
|
||||
%(prog)s --stereo-mode dual --dual-devices "OBS Virtual Camera,OBS Virtual Camera 2"
|
||||
"""
|
||||
)
|
||||
|
||||
parser.add_argument('--listen', default='0.0.0.0',
|
||||
help='Listen address (default: 0.0.0.0)')
|
||||
parser.add_argument('--port', type=int, default=9000,
|
||||
help='Listen port (default: 9000)')
|
||||
parser.add_argument('--mode', choices=['mono', 'auto'], default='auto',
|
||||
help='Force mono or auto-detect (default: auto)')
|
||||
parser.add_argument('--stereo-mode', choices=['off', 'sbs', 'anaglyph', 'dual'],
|
||||
default='off',
|
||||
help='Stereo output mode (default: off)')
|
||||
parser.add_argument('--device', default=None,
|
||||
help='Virtual camera device (Linux: /dev/videoX, Windows: device name or index)')
|
||||
parser.add_argument('--dual-devices', default=None,
|
||||
help='Dual devices for stereo (comma-separated)')
|
||||
parser.add_argument('--width', type=int, default=640,
|
||||
help='Output width per eye (default: 640)')
|
||||
parser.add_argument('--height', type=int, default=480,
|
||||
help='Output height (default: 480)')
|
||||
parser.add_argument('--fps', type=int, default=15,
|
||||
help='Output FPS (default: 15)')
|
||||
parser.add_argument('--prefer-backend',
|
||||
choices=[BACKEND_PYVIRTUALCAM, BACKEND_PYFAKEWEBCAM, BACKEND_FFMPEG],
|
||||
default=BACKEND_PYVIRTUALCAM,
|
||||
help='Preferred backend (default: pyvirtualcam)')
|
||||
parser.add_argument('--verbose', action='store_true',
|
||||
help='Verbose output')
|
||||
|
||||
args = parser.parse_args()
|
||||
g_verbose = args.verbose
|
||||
|
||||
# Validate arguments
|
||||
if args.stereo_mode == 'dual' and not args.dual_devices:
|
||||
parser.error("--dual-devices is required when using --stereo-mode dual")
|
||||
|
||||
# Create listening socket
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
|
||||
try:
|
||||
sock.bind((args.listen, args.port))
|
||||
sock.listen(1)
|
||||
log(f"Listening on {args.listen}:{args.port}")
|
||||
log(f"Stereo mode: {args.stereo_mode}")
|
||||
log(f"Backend: {args.prefer_backend}")
|
||||
|
||||
while True:
|
||||
log("Waiting for connection...")
|
||||
conn, addr = sock.accept()
|
||||
|
||||
try:
|
||||
process_stream(conn, addr, args)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
log("\nShutting down...")
|
||||
except Exception as e:
|
||||
log_error(f"Server error: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/bin/bash
|
||||
# Build the 3DS webcam app
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
APP_DIR="$PROJECT_ROOT/3ds-app"
|
||||
DIST_DIR="$PROJECT_ROOT/dist"
|
||||
|
||||
echo "======================================"
|
||||
echo "Building 3DS Webcam Bridge"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Check for devkitARM
|
||||
if [ -z "$DEVKITARM" ]; then
|
||||
echo "Error: DEVKITARM is not set!"
|
||||
echo ""
|
||||
echo "Please run: export DEVKITARM=/opt/devkitpro/devkitARM"
|
||||
echo "Or run: ./scripts/install-devkitpro.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check for stb_image_write.h
|
||||
STB_HEADER="$APP_DIR/source/third_party/stb_image_write.h"
|
||||
if ! grep -q "stbi_write_jpg_to_func" "$STB_HEADER" 2>/dev/null || grep -q "PLACEHOLDER" "$STB_HEADER"; then
|
||||
echo "Warning: stb_image_write.h appears to be a placeholder!"
|
||||
echo ""
|
||||
echo "Downloading the real stb_image_write.h..."
|
||||
wget https://raw.githubusercontent.com/nothings/stb/master/stb_image_write.h \
|
||||
-O "$STB_HEADER" 2>/dev/null || {
|
||||
echo ""
|
||||
echo "Error: Could not download stb_image_write.h"
|
||||
echo "Please download it manually from:"
|
||||
echo " https://raw.githubusercontent.com/nothings/stb/master/stb_image_write.h"
|
||||
echo "And place it at:"
|
||||
echo " $STB_HEADER"
|
||||
exit 1
|
||||
}
|
||||
echo "Downloaded successfully!"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Create dist directory
|
||||
mkdir -p "$DIST_DIR"
|
||||
|
||||
# Build
|
||||
echo "Building..."
|
||||
cd "$APP_DIR"
|
||||
make clean
|
||||
make -j$(nproc)
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Build complete!"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
echo "Output files:"
|
||||
ls -lh "$DIST_DIR"/3ds-webcam-bridge.*
|
||||
echo ""
|
||||
echo "To install on your 3DS:"
|
||||
echo " 1. Copy $DIST_DIR/3ds-webcam-bridge.3dsx to your SD card at:"
|
||||
echo " SD:/3ds/3ds-webcam-bridge/"
|
||||
echo " 2. Launch via Homebrew Launcher"
|
||||
echo ""
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/bin/bash
|
||||
# Create v4l2loopback virtual camera device(s)
|
||||
|
||||
set -e
|
||||
|
||||
DUAL=false
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--dual)
|
||||
DUAL=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1"
|
||||
echo "Usage: $0 [--dual]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
echo "======================================"
|
||||
echo "v4l2loopback Device Setup"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Check if v4l2loopback is installed
|
||||
if ! modinfo v4l2loopback &> /dev/null; then
|
||||
echo "Error: v4l2loopback is not installed!"
|
||||
echo "Run: ./scripts/pc-install.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Unload existing module if loaded
|
||||
if lsmod | grep -q v4l2loopback; then
|
||||
echo "Unloading existing v4l2loopback module..."
|
||||
sudo modprobe -r v4l2loopback
|
||||
fi
|
||||
|
||||
# Load module with appropriate parameters
|
||||
if [ "$DUAL" = true ]; then
|
||||
echo "Creating dual virtual camera devices..."
|
||||
sudo modprobe v4l2loopback \
|
||||
devices=2 \
|
||||
video_nr=0,1 \
|
||||
card_label="3DS Webcam Left","3DS Webcam Right" \
|
||||
exclusive_caps=1,1
|
||||
|
||||
echo ""
|
||||
echo "Created devices:"
|
||||
echo " /dev/video0 - 3DS Webcam Left"
|
||||
echo " /dev/video1 - 3DS Webcam Right"
|
||||
echo ""
|
||||
echo "To use with dual mode:"
|
||||
echo " ./pc-server/run_server.sh --stereo-mode dual --dual-devices /dev/video0,/dev/video1"
|
||||
else
|
||||
echo "Creating single virtual camera device..."
|
||||
sudo modprobe v4l2loopback \
|
||||
devices=1 \
|
||||
video_nr=0 \
|
||||
card_label="3DS Webcam" \
|
||||
exclusive_caps=1
|
||||
|
||||
echo ""
|
||||
echo "Created device:"
|
||||
echo " /dev/video0 - 3DS Webcam"
|
||||
echo ""
|
||||
echo "To use:"
|
||||
echo " ./pc-server/run_server.sh --device /dev/video0"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Verify with: v4l2-ctl --list-devices"
|
||||
echo ""
|
||||
echo "To make this persistent across reboots, add to /etc/modules-load.d/v4l2loopback.conf:"
|
||||
if [ "$DUAL" = true ]; then
|
||||
echo " options v4l2loopback devices=2 video_nr=0,1 card_label=\"3DS Webcam Left\",\"3DS Webcam Right\" exclusive_caps=1,1"
|
||||
else
|
||||
echo " options v4l2loopback devices=1 video_nr=0 card_label=\"3DS Webcam\" exclusive_caps=1"
|
||||
fi
|
||||
echo ""
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/bin/bash
|
||||
# Install devkitPro toolchain for 3DS development (Debian/Ubuntu)
|
||||
|
||||
set -e
|
||||
|
||||
echo "======================================"
|
||||
echo "devkitPro Toolchain Installer"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Check if already installed
|
||||
if command -v dkp-pacman &> /dev/null; then
|
||||
echo "devkitPro is already installed!"
|
||||
echo "devkitARM location: $DEVKITARM"
|
||||
echo ""
|
||||
echo "To update, run: sudo dkp-pacman -Syu"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "This script will install devkitPro and the 3DS development tools."
|
||||
echo "You may be prompted for your password (sudo)."
|
||||
echo ""
|
||||
|
||||
# Download and install devkitPro pacman
|
||||
echo "Downloading devkitPro pacman package..."
|
||||
wget https://github.com/devkitPro/pacman/releases/latest/download/devkitpro-pacman.amd64.deb -O /tmp/devkitpro-pacman.deb
|
||||
|
||||
echo "Installing devkitPro pacman..."
|
||||
sudo dpkg -i /tmp/devkitpro-pacman.deb || sudo apt-get install -f -y
|
||||
|
||||
rm /tmp/devkitpro-pacman.deb
|
||||
|
||||
# Install devkitARM and libraries
|
||||
echo ""
|
||||
echo "Installing devkitARM and 3DS libraries..."
|
||||
sudo dkp-pacman -S --noconfirm devkitARM libctru citro3d 3dstools
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Installation complete!"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
echo "IMPORTANT: Add these lines to your ~/.bashrc or ~/.zshrc:"
|
||||
echo ""
|
||||
echo " export DEVKITPRO=/opt/devkitpro"
|
||||
echo " export DEVKITARM=\$DEVKITPRO/devkitARM"
|
||||
echo " export PATH=\$DEVKITARM/bin:\$PATH"
|
||||
echo ""
|
||||
echo "Then run: source ~/.bashrc"
|
||||
echo ""
|
||||
echo "Or run these commands now:"
|
||||
echo " export DEVKITPRO=/opt/devkitpro"
|
||||
echo " export DEVKITARM=\$DEVKITPRO/devkitARM"
|
||||
echo " export PATH=\$DEVKITARM/bin:\$PATH"
|
||||
echo ""
|
||||
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
# Install PC server dependencies (Linux - Debian/Ubuntu)
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
VENV_DIR="$PROJECT_ROOT/.venv"
|
||||
|
||||
echo "======================================"
|
||||
echo "PC Server Installer (Linux)"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
|
||||
# Update package list
|
||||
echo "Updating package list..."
|
||||
sudo apt-get update
|
||||
|
||||
# Install system dependencies
|
||||
echo ""
|
||||
echo "Installing system dependencies..."
|
||||
sudo apt-get install -y \
|
||||
v4l2loopback-dkms \
|
||||
v4l2loopback-utils \
|
||||
ffmpeg \
|
||||
python3 \
|
||||
python3-venv \
|
||||
python3-pip \
|
||||
python3-dev \
|
||||
build-essential
|
||||
|
||||
# Create virtual environment
|
||||
echo ""
|
||||
echo "Creating Python virtual environment..."
|
||||
if [ -d "$VENV_DIR" ]; then
|
||||
echo "Virtual environment already exists, updating..."
|
||||
else
|
||||
python3 -m venv "$VENV_DIR"
|
||||
fi
|
||||
|
||||
# Activate and install Python packages
|
||||
echo ""
|
||||
echo "Installing Python packages..."
|
||||
source "$VENV_DIR/bin/activate"
|
||||
pip install --upgrade pip
|
||||
pip install -r "$PROJECT_ROOT/pc-server/requirements.txt"
|
||||
|
||||
echo ""
|
||||
echo "======================================"
|
||||
echo "Installation complete!"
|
||||
echo "======================================"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo ""
|
||||
echo "1. Create v4l2loopback device(s):"
|
||||
echo " For single device:"
|
||||
echo " ./scripts/create-v4l2.sh"
|
||||
echo " For dual devices (stereo):"
|
||||
echo " ./scripts/create-v4l2.sh --dual"
|
||||
echo ""
|
||||
echo "2. Run the server:"
|
||||
echo " ./pc-server/run_server.sh --device /dev/video0"
|
||||
echo ""
|
||||
echo "3. For stereo side-by-side:"
|
||||
echo " ./pc-server/run_server.sh --stereo-mode sbs --width 400 --height 240"
|
||||
echo ""
|
||||
echo "4. For dual devices:"
|
||||
echo " ./pc-server/run_server.sh --stereo-mode dual --dual-devices /dev/video0,/dev/video1"
|
||||
echo ""
|
||||
@@ -0,0 +1,104 @@
|
||||
# Helper functions for Windows installation
|
||||
|
||||
function Test-WingetInstalled {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check if winget is installed
|
||||
#>
|
||||
try {
|
||||
$null = Get-Command winget -ErrorAction Stop
|
||||
return $true
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Test-CommandInstalled {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check if a command is available
|
||||
#>
|
||||
param(
|
||||
[string]$Command
|
||||
)
|
||||
|
||||
try {
|
||||
$null = Get-Command $Command -ErrorAction Stop
|
||||
return $true
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Install-WithWinget {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Install a package using winget
|
||||
#>
|
||||
param(
|
||||
[string]$PackageId,
|
||||
[string]$Name
|
||||
)
|
||||
|
||||
Write-Host "Installing $Name..." -ForegroundColor Cyan
|
||||
$result = winget install -e --id $PackageId --accept-package-agreements --accept-source-agreements
|
||||
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "$Name installed successfully!" -ForegroundColor Green
|
||||
return $true
|
||||
} else {
|
||||
Write-Warning "$Name installation may have failed. Exit code: $LASTEXITCODE"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Add-FirewallRule {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Add a Windows Defender Firewall rule for the server
|
||||
#>
|
||||
param(
|
||||
[int]$Port,
|
||||
[string]$RuleName = "3DS Webcam Bridge"
|
||||
)
|
||||
|
||||
Write-Host "Checking firewall rules..." -ForegroundColor Cyan
|
||||
|
||||
# Check if rule already exists
|
||||
$existingRule = Get-NetFirewallRule -DisplayName $RuleName -ErrorAction SilentlyContinue
|
||||
|
||||
if ($existingRule) {
|
||||
Write-Host "Firewall rule already exists." -ForegroundColor Yellow
|
||||
return $true
|
||||
}
|
||||
|
||||
try {
|
||||
New-NetFirewallRule `
|
||||
-DisplayName $RuleName `
|
||||
-Direction Inbound `
|
||||
-Protocol TCP `
|
||||
-LocalPort $Port `
|
||||
-Action Allow `
|
||||
-Profile Private,Domain `
|
||||
-ErrorAction Stop | Out-Null
|
||||
|
||||
Write-Host "Firewall rule added successfully!" -ForegroundColor Green
|
||||
return $true
|
||||
} catch {
|
||||
Write-Warning "Failed to add firewall rule: $_"
|
||||
Write-Warning "You may need to manually add a firewall rule for port $Port"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Test-IsAdmin {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Check if running as administrator
|
||||
#>
|
||||
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
|
||||
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Test-WingetInstalled, Test-CommandInstalled, Install-WithWinget, Add-FirewallRule, Test-IsAdmin
|
||||
@@ -0,0 +1,160 @@
|
||||
# PC Server Installer for Windows 10/11
|
||||
# This script installs all dependencies for the 3DS Webcam Bridge PC server
|
||||
|
||||
param(
|
||||
[int]$Port = 9000,
|
||||
[switch]$SkipFirewall,
|
||||
[switch]$Force
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
|
||||
$HelpersModule = Join-Path $ScriptDir "helpers.psm1"
|
||||
|
||||
# Import helpers
|
||||
Import-Module $HelpersModule -Force
|
||||
|
||||
Write-Host "======================================" -ForegroundColor Cyan
|
||||
Write-Host "3DS Webcam Bridge - PC Server Installer" -ForegroundColor Cyan
|
||||
Write-Host "Windows 10/11" -ForegroundColor Cyan
|
||||
Write-Host "======================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# Check for admin privileges (required for some operations)
|
||||
$isAdmin = Test-IsAdmin
|
||||
if (-not $isAdmin) {
|
||||
Write-Warning "Not running as Administrator. Some features may require admin privileges."
|
||||
Write-Warning "To enable firewall rules and ensure full functionality, run as Administrator."
|
||||
Write-Host ""
|
||||
$response = Read-Host "Continue anyway? (y/n)"
|
||||
if ($response -ne 'y') {
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# Check for winget
|
||||
Write-Host "Checking for winget..." -ForegroundColor Cyan
|
||||
if (-not (Test-WingetInstalled)) {
|
||||
Write-Host ""
|
||||
Write-Host "ERROR: winget is not installed!" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
Write-Host "winget is required to install dependencies automatically."
|
||||
Write-Host "Please install 'App Installer' from the Microsoft Store, or"
|
||||
Write-Host "download it from:"
|
||||
Write-Host " https://github.com/microsoft/winget-cli/releases"
|
||||
Write-Host ""
|
||||
exit 1
|
||||
}
|
||||
Write-Host "winget found!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Install Python 3
|
||||
Write-Host "Checking for Python 3..." -ForegroundColor Cyan
|
||||
if (-not (Test-CommandInstalled python) -and -not (Test-CommandInstalled python3)) {
|
||||
Write-Host "Python 3 not found. Installing..." -ForegroundColor Yellow
|
||||
Install-WithWinget -PackageId "Python.Python.3.11" -Name "Python 3.11"
|
||||
|
||||
# Refresh PATH
|
||||
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User")
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "NOTE: You may need to restart your terminal for Python to be available in PATH" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
} else {
|
||||
Write-Host "Python 3 is already installed!" -ForegroundColor Green
|
||||
$pythonVersion = python --version 2>&1
|
||||
Write-Host " $pythonVersion" -ForegroundColor Gray
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# Install OBS Studio (for Virtual Camera)
|
||||
Write-Host "Checking for OBS Studio..." -ForegroundColor Cyan
|
||||
$obsInstalled = Test-Path "C:\Program Files\obs-studio\bin\64bit\obs64.exe"
|
||||
if (-not $obsInstalled -or $Force) {
|
||||
Write-Host "OBS Studio not found. Installing..." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "OBS Studio provides the Virtual Camera that pyvirtualcam uses on Windows." -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Install-WithWinget -PackageId "OBSProject.OBSStudio" -Name "OBS Studio"
|
||||
Write-Host ""
|
||||
} else {
|
||||
Write-Host "OBS Studio is already installed!" -ForegroundColor Green
|
||||
}
|
||||
Write-Host ""
|
||||
|
||||
# Create virtual environment
|
||||
$VenvDir = Join-Path $ProjectRoot ".venv"
|
||||
Write-Host "Setting up Python virtual environment..." -ForegroundColor Cyan
|
||||
|
||||
if (Test-Path $VenvDir) {
|
||||
if ($Force) {
|
||||
Write-Host "Removing existing virtual environment..." -ForegroundColor Yellow
|
||||
Remove-Item -Recurse -Force $VenvDir
|
||||
} else {
|
||||
Write-Host "Virtual environment already exists. Updating..." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path $VenvDir)) {
|
||||
Write-Host "Creating virtual environment..." -ForegroundColor Cyan
|
||||
python -m venv $VenvDir
|
||||
}
|
||||
|
||||
# Activate virtual environment and install packages
|
||||
$ActivateScript = Join-Path $VenvDir "Scripts\Activate.ps1"
|
||||
Write-Host "Installing Python packages..." -ForegroundColor Cyan
|
||||
|
||||
& $ActivateScript
|
||||
|
||||
$RequirementsFile = Join-Path $ProjectRoot "pc-server\requirements.txt"
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install -r $RequirementsFile
|
||||
|
||||
Write-Host "Python packages installed successfully!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# Add firewall rule
|
||||
if (-not $SkipFirewall -and $isAdmin) {
|
||||
Add-FirewallRule -Port $Port
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
Write-Host "======================================" -ForegroundColor Green
|
||||
Write-Host "Installation Complete!" -ForegroundColor Green
|
||||
Write-Host "======================================" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "Next Steps:" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "1. Start OBS Studio at least once to initialize the Virtual Camera:" -ForegroundColor White
|
||||
Write-Host " - Open OBS Studio" -ForegroundColor Gray
|
||||
Write-Host " - Go to Tools -> VirtualCam -> Start" -ForegroundColor Gray
|
||||
Write-Host " - You can close OBS after this" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "2. Run the server:" -ForegroundColor White
|
||||
Write-Host " .\pc-server\run_server.ps1 -Port $Port" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "3. For stereo side-by-side mode:" -ForegroundColor White
|
||||
Write-Host " .\pc-server\run_server.ps1 -StereoMode sbs -Width 400 -Height 240" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "4. To specify a device:" -ForegroundColor White
|
||||
Write-Host " .\pc-server\run_server.ps1 -Device `"OBS Virtual Camera`"" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "5. For dual devices (if you have multiple virtual cameras set up):" -ForegroundColor White
|
||||
Write-Host " .\pc-server\run_server.ps1 -StereoMode dual -DualDevices `"OBS Virtual Camera,OBS Virtual Camera 2`"" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
if (-not $SkipFirewall -and -not $isAdmin) {
|
||||
Write-Warning "Firewall rule was not added (requires admin privileges)."
|
||||
Write-Warning "If you have connection issues, manually allow TCP port $Port in Windows Defender Firewall."
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "For more information, see README.md" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Reference in New Issue
Block a user