diff --git a/libobs/CMakeLists.txt b/libobs/CMakeLists.txt index 47d07271b..50e682b26 100644 --- a/libobs/CMakeLists.txt +++ b/libobs/CMakeLists.txt @@ -22,12 +22,14 @@ add_definitions(-DPTW32_STATIC_LIB) if(WIN32) set(libobs_PLATFORM_SOURCES obs-windows.c + util/threading-windows.c util/platform-windows.c) set(libobs_PLATFORM_DEPS w32-pthreads) elseif(APPLE) set(libobs_PLATFORM_SOURCES obs-cocoa.c + util/threading-posix.c util/platform-cocoa.m) set_source_files_properties(${libobs_PLATFORM_SOURCES} @@ -44,6 +46,7 @@ elseif(APPLE) elseif(UNIX) set(libobs_PLATFORM_SOURCES obs-nix.c + util/threading-posic.c util/platform-nix.c) endif() diff --git a/libobs/media-io/audio-io.c b/libobs/media-io/audio-io.c index c1a6a0b6c..43cfced97 100644 --- a/libobs/media-io/audio-io.c +++ b/libobs/media-io/audio-io.c @@ -38,7 +38,7 @@ struct audio_input { void *param; }; -static inline audio_input_free(struct audio_input *input) +static inline void audio_input_free(struct audio_input *input) { audio_resampler_destroy(input->resampler); } @@ -435,7 +435,7 @@ static void *audio_thread(void *param) uint64_t prev_time = os_gettime_ns() - buffer_time; uint64_t audio_time; - while (event_try(&audio->stop_event) == EAGAIN) { + while (event_try(audio->stop_event) == EAGAIN) { os_sleep_ms(AUDIO_WAIT_TIME); pthread_mutex_lock(&audio->line_mutex); @@ -611,7 +611,7 @@ void audio_output_close(audio_t audio) return; if (audio->initialized) { - event_signal(&audio->stop_event); + event_signal(audio->stop_event); pthread_join(audio->thread, &thread_ret); } @@ -629,7 +629,7 @@ void audio_output_close(audio_t audio) da_free(audio->mix_buffers[i]); da_free(audio->inputs); - event_destroy(&audio->stop_event); + event_destroy(audio->stop_event); pthread_mutex_destroy(&audio->line_mutex); bfree(audio); } @@ -764,7 +764,7 @@ static inline void mul_vol_32bit(void *array, float volume, size_t total_num) for (size_t i = 0; i < total_num; i++) { double val = (double)vals[i] / 2147483647.0; - double output = val * volume; + double output = val * dvol; vals[i] = (int32_t)(CLAMP(output, -1.0, 1.0) * 2147483647.0); } } diff --git a/libobs/media-io/video-io.c b/libobs/media-io/video-io.c index 02847b734..3776c5dc3 100644 --- a/libobs/media-io/video-io.c +++ b/libobs/media-io/video-io.c @@ -125,11 +125,11 @@ static void *video_thread(void *param) struct video_output *video = param; uint64_t cur_time = os_gettime_ns(); - while (event_try(&video->stop_event) == EAGAIN) { + while (event_try(video->stop_event) == EAGAIN) { /* wait half a frame, update frame */ os_sleepto_ns(cur_time += (video->frame_time/2)); video->cur_video_time = cur_time; - event_signal(&video->update_event); + event_signal(video->update_event); /* wait another half a frame, swap and output frames */ os_sleepto_ns(cur_time += (video->frame_time/2)); @@ -198,8 +198,8 @@ void video_output_close(video_t video) video_input_free(&video->inputs.array[i]); da_free(video->inputs); - event_destroy(&video->update_event); - event_destroy(&video->stop_event); + event_destroy(video->update_event); + event_destroy(video->stop_event); pthread_mutex_destroy(&video->data_mutex); pthread_mutex_destroy(&video->input_mutex); bfree(video); @@ -339,8 +339,8 @@ bool video_output_wait(video_t video) { if (!video) return false; - event_wait(&video->update_event); - return event_try(&video->stop_event) == EAGAIN; + event_wait(video->update_event); + return event_try(video->stop_event) == EAGAIN; } uint64_t video_getframetime(video_t video) @@ -361,8 +361,8 @@ void video_output_stop(video_t video) return; if (video->initialized) { - event_signal(&video->stop_event); + event_signal(video->stop_event); pthread_join(video->thread, &thread_ret); - event_signal(&video->update_event); + event_signal(video->update_event); } } diff --git a/libobs/media-io/video-scaler-ffmpeg.c b/libobs/media-io/video-scaler-ffmpeg.c index 1e00bab9d..4fbcca61b 100644 --- a/libobs/media-io/video-scaler-ffmpeg.c +++ b/libobs/media-io/video-scaler-ffmpeg.c @@ -133,9 +133,9 @@ bool video_scaler_scale(video_scaler_t scaler, return false; int ret = sws_scale(scaler->swscale, - input, in_linesize, + input, (const int *)in_linesize, 0, scaler->src_height, - output, out_linesize); + output, (const int *)out_linesize); if (ret <= 0) { blog(LOG_DEBUG, "video_scaler_scale: sws_scale failed: %d", ret); diff --git a/libobs/obs-video.c b/libobs/obs-video.c index baa21955f..6b08a9e90 100644 --- a/libobs/obs-video.c +++ b/libobs/obs-video.c @@ -421,7 +421,6 @@ void *obs_video_thread(void *param) render_displays(); output_frame(cur_time); - } UNUSED_PARAMETER(param); diff --git a/libobs/util/threading-posix.c b/libobs/util/threading-posix.c new file mode 100644 index 000000000..4ab53c594 --- /dev/null +++ b/libobs/util/threading-posix.c @@ -0,0 +1,149 @@ +/* + * Copyright (c) 2014 Hugh Bailey + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifdef __APPLE__ +#include +#endif + +#include "bmem.h" +#include "threading.h" + +struct event_data { + pthread_mutex_t mutex; + pthread_cond_t cond; + volatile bool signalled; + bool manual; +}; + +int event_init(event_t *event, enum event_type type) +{ + int code = 0; + + struct event_data *data = bzalloc(sizeof(struct event_data)); + + if ((code = pthread_mutex_init(&data->mutex, NULL)) < 0) { + bfree(data); + return code; + } + + if ((code = pthread_cond_init(&data->cond, NULL)) < 0) { + pthread_mutex_destroy(&data->mutex); + bfree(data); + return code; + } + + data->manual = (type == EVENT_TYPE_MANUAL); + data->signalled = false; + *event = data; + + return 0; +} + +void event_destroy(event_t event) +{ + if (event) { + pthread_mutex_destroy(&event->mutex); + pthread_cond_destroy(&event->cond); + bfree(event); + } +} + +int event_wait(event_t event) +{ + int code = 0; + pthread_mutex_lock(&event->mutex); + if (!event->signalled) + code = pthread_cond_wait(&event->cond, &event->mutex); + + if (code == 0) { + if (!event->manual) + event->signalled = false; + pthread_mutex_unlock(&event->mutex); + } + + return code; +} + +static inline void add_ms_to_ts(struct timespec *ts, + unsigned long milliseconds) +{ + ts->tv_sec += milliseconds/1000; + ts->tv_nsec += (milliseconds%1000)*1000000; + if (ts->tv_nsec > 1000000000) { + ts->tv_sec += 1; + ts->tv_nsec -= 1000000000; + } +} + +int event_timedwait(event_t event, unsigned long milliseconds) +{ + int code = 0; + pthread_mutex_lock(&event->mutex); + if (!event->signalled) { + struct timespec ts; +#ifdef __APPLE__ + struct timeval tv; + gettimeofday(&tv, NULL); + ts.tv_sec = tv.tv_sec; + ts.tv_nsec = tv.tv_usec * 1000; +#else + clock_gettime(CLOCK_REALTIME, &ts); +#endif + add_ms_to_ts(&ts, milliseconds); + code = pthread_cond_timedwait(&event->cond, &event->mutex, &ts); + } + + if (code == 0) { + if (!event->manual) + event->signalled = false; + pthread_mutex_unlock(&event->mutex); + } + + return code; +} + +int event_try(event_t event) +{ + pthread_mutex_lock(&event->mutex); + if (event->signalled) { + if (!event->manual) + event->signalled = false; + pthread_mutex_unlock(&event->mutex); + return 0; + } + pthread_mutex_unlock(&event->mutex); + + return EAGAIN; +} + +int event_signal(event_t event) +{ + int code = 0; + + pthread_mutex_lock(&event->mutex); + code = pthread_cond_signal(&event->cond); + event->signalled = true; + pthread_mutex_unlock(&event->mutex); + + return code; +} + +void event_reset(event_t event) +{ + pthread_mutex_lock(&event->mutex); + event->signalled = false; + pthread_mutex_unlock(&event->mutex); +} diff --git a/libobs/util/threading-windows.c b/libobs/util/threading-windows.c new file mode 100644 index 000000000..c299a8fc2 --- /dev/null +++ b/libobs/util/threading-windows.c @@ -0,0 +1,114 @@ +/* + * Copyright (c) 2014 Hugh Bailey + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "bmem.h" +#include "threading.h" + +#define WIN32_LEAN_AND_MEAN +#include + +struct event_data { + HANDLE handle; +}; + +int event_init(event_t *event, enum event_type type) +{ + HANDLE handle; + struct event_data *data; + + handle = CreateEvent(NULL, (type == EVENT_TYPE_MANUAL), FALSE, NULL); + if (!handle) + return -1; + + data = bmalloc(sizeof(struct event_data)); + data->handle = handle; + + *event = data; + return 0; +} + +void event_destroy(event_t event) +{ + if (event) { + CloseHandle(event->handle); + bfree(event); + } +} + +int event_wait(event_t event) +{ + DWORD code; + + if (!event) + return EINVAL; + + code = WaitForSingleObject(event->handle, INFINITE); + if (code != WAIT_OBJECT_0) + return EINVAL; + + return 0; +} + +int event_timedwait(event_t event, unsigned long milliseconds) +{ + DWORD code; + + if (!event) + return EINVAL; + + code = WaitForSingleObject(event->handle, milliseconds); + if (code == WAIT_TIMEOUT) + return ETIMEDOUT; + else if (code != WAIT_OBJECT_0) + return EINVAL; + + return 0; +} + +int event_try(event_t event) +{ + DWORD code; + + if (!event) + return EINVAL; + + code = WaitForSingleObject(event->handle, 0); + if (code == WAIT_TIMEOUT) + return EAGAIN; + else if (code != WAIT_OBJECT_0) + return EINVAL; + + return 0; +} + +int event_signal(event_t event) +{ + if (!event) + return EINVAL; + + if (!SetEvent(event->handle)) + return EINVAL; + + return 0; +} + +void event_reset(event_t event) +{ + if (!event) + return; + + ResetEvent(event->handle); +} diff --git a/libobs/util/threading.h b/libobs/util/threading.h index e2f4955f5..f39b92f60 100644 --- a/libobs/util/threading.h +++ b/libobs/util/threading.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013 Hugh Bailey + * Copyright (c) 2013-2014 Hugh Bailey * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -46,90 +46,22 @@ static inline void pthread_mutex_init_value(pthread_mutex_t *mutex) *mutex = init_val; } -struct event_data { - pthread_mutex_t mutex; - pthread_cond_t cond; - bool signalled; - bool manual; -}; - enum event_type { EVENT_TYPE_AUTO, EVENT_TYPE_MANUAL }; -typedef struct event_data event_t; +struct event_data; +typedef struct event_data *event_t; -static inline int event_init(event_t *event, enum event_type type) -{ - int code = 0; +EXPORT int event_init(event_t *event, enum event_type type); +EXPORT void event_destroy(event_t event); +EXPORT int event_wait(event_t event); +EXPORT int event_timedwait(event_t event, unsigned long milliseconds); +EXPORT int event_try(event_t event); +EXPORT int event_signal(event_t event); +EXPORT void event_reset(event_t event); - if ((code = pthread_mutex_init(&event->mutex, NULL)) < 0) - return code; - - if ((code = pthread_cond_init(&event->cond, NULL)) < 0) - pthread_mutex_destroy(&event->mutex); - - event->manual = (type == EVENT_TYPE_MANUAL); - event->signalled = false; - - return code; -} - -static inline void event_destroy(event_t *event) -{ - if (event) { - pthread_mutex_destroy(&event->mutex); - pthread_cond_destroy(&event->cond); - } -} - -static inline int event_wait(event_t *event) -{ - int code = 0; - pthread_mutex_lock(&event->mutex); - if (event->signalled || - (code = pthread_cond_wait(&event->cond, &event->mutex)) == 0) { - if (!event->manual) - event->signalled = false; - pthread_mutex_unlock(&event->mutex); - } - - return code; -} - -static inline int event_try(event_t *event) -{ - pthread_mutex_lock(&event->mutex); - if (event->signalled) { - if (!event->manual) - event->signalled = false; - pthread_mutex_unlock(&event->mutex); - return 0; - } - pthread_mutex_unlock(&event->mutex); - - return EAGAIN; -} - -static inline int event_signal(event_t *event) -{ - int code = 0; - - pthread_mutex_lock(&event->mutex); - code = pthread_cond_signal(&event->cond); - event->signalled = true; - pthread_mutex_unlock(&event->mutex); - - return code; -} - -static inline void event_reset(event_t *event) -{ - pthread_mutex_lock(&event->mutex); - event->signalled = false; - pthread_mutex_unlock(&event->mutex); -} #ifdef __cplusplus } diff --git a/obs/window-basic-main.cpp b/obs/window-basic-main.cpp index 7946dec4c..812a080ed 100644 --- a/obs/window-basic-main.cpp +++ b/obs/window-basic-main.cpp @@ -66,6 +66,9 @@ void OBSBasic::OBSInit() /* TODO: this is a test */ obs_load_module("test-input"); obs_load_module("obs-ffmpeg"); +#ifdef __APPLE__ + obs_load_module("mac-capture"); +#endif /* HACK: fixes a qt bug with native widgets with native repaint */ ui->previewContainer->repaint(); diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index fc87e6dff..2f3a5a853 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -2,6 +2,8 @@ include_directories(SYSTEM "${CMAKE_SOURCE_DIR}/libobs") if(WIN32) add_subdirectory(dshow) +elseif(APPLE) + add_subdirectory(mac-capture) endif() add_subdirectory(obs-ffmpeg) diff --git a/plugins/mac-capture/CMakeLists.txt b/plugins/mac-capture/CMakeLists.txt new file mode 100644 index 000000000..4e9697c65 --- /dev/null +++ b/plugins/mac-capture/CMakeLists.txt @@ -0,0 +1,23 @@ +project(mac-capture) + +find_library(COREAUDIO CoreAudio) +find_library(AUDIOUNIT AudioUnit) +find_library(COREFOUNDATION CoreFoundation) + +include_directories(${COREAUDIO} + ${AUDIOUNIT} + ${COREFOUNDATION}) + +set(mac-capture_SOURCES + plugin-main.c + mac-audio.c) + +add_library(mac-capture MODULE + ${mac-capture_SOURCES}) +target_link_libraries(mac-capture + libobs + ${COREAUDIO} + ${AUDIOUNIT} + ${COREFOUNDATION}) + +install_obs_plugin(mac-capture) diff --git a/plugins/mac-capture/mac-audio.c b/plugins/mac-capture/mac-audio.c new file mode 100644 index 000000000..01b15bd57 --- /dev/null +++ b/plugins/mac-capture/mac-audio.c @@ -0,0 +1,618 @@ +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "mac-helpers.h" + +#define SCOPE_OUTPUT kAudioUnitScope_Output +#define SCOPE_INPUT kAudioUnitScope_Input +#define SCOPE_GLOBAL kAudioUnitScope_Global + +#define BUS_OUTPUT 0 +#define BUS_INPUT 1 + +#define MAX_DEVICES 20 + +#define set_property AudioUnitSetProperty +#define get_property AudioUnitGetProperty + +struct coreaudio_data { + char *device_name; + char *device_uid; + AudioUnit unit; + AudioDeviceID device_id; + AudioBufferList *buf_list; + bool au_initialized; + bool active; + + uint32_t sample_rate; + enum audio_format format; + enum speaker_layout speakers; + + pthread_t reconnect_thread; + event_t exit_event; + volatile bool reconnecting; + + obs_source_t source; +}; + +static bool find_device_id_by_uid(const char *uid, AudioDeviceID *id) +{ + OSStatus stat; + UInt32 size = sizeof(AudioDeviceID); + CFStringRef cf_uid = CFStringCreateWithCString(NULL, uid, + kCFStringEncodingUTF8); + CFStringRef qual = NULL; + UInt32 qual_size = 0; + bool success; + + AudioObjectPropertyAddress addr = { + .mScope = kAudioObjectPropertyScopeGlobal, + .mElement = kAudioObjectPropertyElementMaster + }; + + bool is_default = (strcmp(uid, "Default") == 0); + + if (is_default) { + addr.mSelector = kAudioHardwarePropertyDefaultInputDevice; + } else { + addr.mSelector = kAudioHardwarePropertyTranslateUIDToDevice; + qual = cf_uid; + qual_size = sizeof(CFStringRef); + } + + stat = AudioObjectGetPropertyData(kAudioObjectSystemObject, &addr, + qual_size, &qual, &size, id); + success = (stat == noErr); + + CFRelease(cf_uid); + return success; +} + +static inline void ca_warn(struct coreaudio_data *ca, const char *func, + const char *format, ...) +{ + va_list args; + struct dstr str = {0}; + + va_start(args, format); + dstr_printf(&str, "[%s]:[device '%s'] ", func, ca->device_name); + dstr_vcatf(&str, format, args); + blog(LOG_WARNING, "%s", str.array); + va_end(args); +} + +static inline bool ca_success(OSStatus stat, struct coreaudio_data *ca, + const char *func, const char *action) +{ + if (stat != noErr) { + blog(LOG_WARNING, "[%s]:[device '%s'] %s failed: %d", + func, ca->device_name, action, (int)stat); + return false; + } + + return true; +} + +enum coreaudio_io_type { + IO_TYPE_INPUT, + IO_TYPE_OUTPUT, +}; + +static inline bool enable_io(struct coreaudio_data *ca, + enum coreaudio_io_type type, bool enable) +{ + UInt32 enable_int = enable; + return set_property(ca->unit, kAudioOutputUnitProperty_EnableIO, + (type == IO_TYPE_INPUT) ? SCOPE_INPUT : SCOPE_OUTPUT, + (type == IO_TYPE_INPUT) ? BUS_INPUT : BUS_OUTPUT, + &enable_int, sizeof(enable_int)); +} + +static inline enum audio_format convert_ca_format(UInt32 format_flags, + UInt32 bits) +{ + bool planar = (format_flags & kAudioFormatFlagIsNonInterleaved) != 0; + + if (format_flags & kAudioFormatFlagIsFloat) + return planar ? AUDIO_FORMAT_FLOAT_PLANAR : AUDIO_FORMAT_FLOAT; + + if (!(format_flags & kAudioFormatFlagIsSignedInteger) && bits == 8) + return planar ? AUDIO_FORMAT_U8BIT_PLANAR : AUDIO_FORMAT_U8BIT; + + /* not float? not signed int? no clue, fail */ + if ((format_flags & kAudioFormatFlagIsSignedInteger) == 0) + return AUDIO_FORMAT_UNKNOWN; + + if (bits == 16) + return planar ? AUDIO_FORMAT_16BIT_PLANAR : AUDIO_FORMAT_16BIT; + else if (bits == 32) + return planar ? AUDIO_FORMAT_32BIT_PLANAR : AUDIO_FORMAT_32BIT; + + return AUDIO_FORMAT_UNKNOWN; +} + +static inline enum speaker_layout convert_ca_speaker_layout(UInt32 channels) +{ + /* directly map channel count to enum values */ + if (channels >= 1 && channels <= 8 && channels != 7) + return (enum speaker_layout)channels; + + return SPEAKERS_UNKNOWN; +} + +static bool coreaudio_init_format(struct coreaudio_data *ca) +{ + AudioStreamBasicDescription desc; + OSStatus stat; + UInt32 size = sizeof(desc); + + stat = get_property(ca->unit, kAudioUnitProperty_StreamFormat, + SCOPE_INPUT, BUS_INPUT, &desc, &size); + if (!ca_success(stat, ca, "coreaudio_init_format", "get input format")) + return false; + + stat = set_property(ca->unit, kAudioUnitProperty_StreamFormat, + SCOPE_OUTPUT, BUS_INPUT, &desc, size); + if (!ca_success(stat, ca, "coreaudio_init_format", "set output format")) + return false; + + if (desc.mFormatID != kAudioFormatLinearPCM) { + ca_warn(ca, "coreaudio_init_format", "format is not PCM"); + return false; + } + + ca->format = convert_ca_format(desc.mFormatFlags, desc.mBitsPerChannel); + if (ca->format == AUDIO_FORMAT_UNKNOWN) { + ca_warn(ca, "coreaudio_init_format", "unknown format flags: " + "%u, bits: %u", + (unsigned int)desc.mFormatFlags, + (unsigned int)desc.mBitsPerChannel); + return false; + } + + ca->sample_rate = (uint32_t)desc.mSampleRate; + ca->speakers = convert_ca_speaker_layout(desc.mChannelsPerFrame); + + if (ca->speakers == SPEAKERS_UNKNOWN) { + ca_warn(ca, "coreaudio_init_format", "unknown speaker layout: " + "%u channels", + (unsigned int)desc.mChannelsPerFrame); + return false; + } + + return true; +} + +static bool coreaudio_init_buffer(struct coreaudio_data *ca) +{ + UInt32 buf_size = 0; + UInt32 size = 0; + UInt32 frames = 0; + OSStatus stat; + + AudioObjectPropertyAddress addr = { + kAudioDevicePropertyStreamConfiguration, + kAudioDevicePropertyScopeInput, + kAudioObjectPropertyElementMaster + }; + + stat = AudioObjectGetPropertyDataSize(ca->device_id, &addr, 0, NULL, + &buf_size); + if (!ca_success(stat, ca, "coreaudio_init_buffer", "get list size")) + return false; + + size = sizeof(frames); + stat = get_property(ca->unit, kAudioDevicePropertyBufferFrameSize, + SCOPE_GLOBAL, 0, &frames, &size); + if (!ca_success(stat, ca, "coreaudio_init_buffer", "get frame size")) + return false; + + /* ---------------------- */ + + ca->buf_list = bmalloc(buf_size); + + stat = AudioObjectGetPropertyData(ca->device_id, &addr, 0, NULL, + &buf_size, ca->buf_list); + if (!ca_success(stat, ca, "coreaudio_init_buffer", "allocate")) { + bfree(ca->buf_list); + ca->buf_list = NULL; + return false; + } + + for (UInt32 i = 0; i < ca->buf_list->mNumberBuffers; i++) { + size = ca->buf_list->mBuffers[i].mDataByteSize; + ca->buf_list->mBuffers[i].mData = bmalloc(size); + } + + return true; +} + +static void buf_list_free(AudioBufferList *buf_list) +{ + if (buf_list) { + for (UInt32 i = 0; i < buf_list->mNumberBuffers; i++) + bfree(buf_list->mBuffers[i].mData); + + bfree(buf_list); + } +} + +static OSStatus input_callback( + void *data, + AudioUnitRenderActionFlags *action_flags, + const AudioTimeStamp *ts_data, + UInt32 bus_num, + UInt32 frames, + AudioBufferList *ignored_buffers) +{ + struct coreaudio_data *ca = data; + OSStatus stat; + struct source_audio audio; + + stat = AudioUnitRender(ca->unit, action_flags, ts_data, bus_num, frames, + ca->buf_list); + if (!ca_success(stat, ca, "input_callback", "audio retrieval")) + return noErr; + + for (UInt32 i = 0; i < ca->buf_list->mNumberBuffers; i++) + audio.data[i] = ca->buf_list->mBuffers[i].mData; + + audio.frames = frames; + audio.speakers = ca->speakers; + audio.format = ca->format; + audio.samples_per_sec = ca->sample_rate; + audio.timestamp = ts_data->mHostTime; + + obs_source_output_audio(ca->source, &audio); + + UNUSED_PARAMETER(ignored_buffers); + return noErr; +} + +static void coreaudio_stop(struct coreaudio_data *ca); +static bool coreaudio_init(struct coreaudio_data *ca); +static void coreaudio_uninit(struct coreaudio_data *ca); + +#define RETRY_TIME 3000 + +static void *reconnect_thread(void *param) +{ + struct coreaudio_data *ca = param; + + ca->reconnecting = true; + + while (event_timedwait(ca->exit_event, RETRY_TIME) == ETIMEDOUT) { + if (coreaudio_init(ca)) { + blog(LOG_INFO, "coreaudio: device '%s' connected", + ca->device_name); + break; + } + } + + blog(LOG_DEBUG, "coreaudio: exit the reconnect thread"); + ca->reconnecting = false; + return NULL; +} + +static void coreaudio_begin_reconnect(struct coreaudio_data *ca) +{ + int ret; + + if (ca->reconnecting) + return; + + ret = pthread_create(&ca->reconnect_thread, NULL, reconnect_thread, ca); + if (ret != 0) + blog(LOG_WARNING, "[coreaudio_begin_reconnect] failed to " + "create thread, error code: %d", ret); +} + +static OSStatus disconnection_callback( + AudioObjectID id, + UInt32 num_addresses, + const AudioObjectPropertyAddress addresses[], + void *data) +{ + struct coreaudio_data *ca = data; + UInt32 alive = 0; + UInt32 size = sizeof(alive); + OSStatus stat; + + stat = AudioObjectGetPropertyData(id, addresses, 0, NULL, &size, + &alive); + if (ca_success(stat, ca, "disconnection_callback", "get property")) { + if (!alive) { + coreaudio_stop(ca); + coreaudio_uninit(ca); + + blog(LOG_INFO, "coreaudio: device '%s' disconnected. " + "attempting to reconnect", + ca->device_name); + + coreaudio_begin_reconnect(ca); + } + } + + UNUSED_PARAMETER(num_addresses); + return noErr; +} + +static const AudioObjectPropertyAddress alive_addr = { + kAudioHardwarePropertyDevices, + kAudioDevicePropertyScopeInput, + kAudioDevicePropertyDeviceIsAlive +}; + +static bool coreaudio_init_hooks(struct coreaudio_data *ca) +{ + OSStatus stat; + AURenderCallbackStruct callback_info = { + .inputProc = input_callback, + .inputProcRefCon = ca + }; + + stat = AudioObjectAddPropertyListener(ca->device_id, &alive_addr, + disconnection_callback, ca); + if (!ca_success(stat, ca, "coreaudio_init_hooks", + "set disconnect callback")) + return false; + + stat = set_property(ca->unit, kAudioOutputUnitProperty_SetInputCallback, + SCOPE_GLOBAL, 0, &callback_info, sizeof(callback_info)); + if (!ca_success(stat, ca, "coreaudio_init_hooks", "set input callback")) + return false; + + return true; +} + +static void coreaudio_remove_hooks(struct coreaudio_data *ca) +{ + AURenderCallbackStruct callback_info = { + .inputProc = NULL, + .inputProcRefCon = NULL + }; + + AudioObjectRemovePropertyListener(ca->device_id, &alive_addr, + disconnection_callback, ca); + + set_property(ca->unit, kAudioOutputUnitProperty_SetInputCallback, + SCOPE_GLOBAL, 0, &callback_info, sizeof(callback_info)); +} + +static bool coreaudio_get_device_name(struct coreaudio_data *ca) +{ + CFStringRef cf_name = NULL; + UInt32 size = sizeof(CFStringRef); + char name[1024]; + + const AudioObjectPropertyAddress addr = { + kAudioDevicePropertyDeviceNameCFString, + kAudioObjectPropertyScopeInput, + kAudioObjectPropertyElementMaster + }; + + OSStatus stat = AudioObjectGetPropertyData(ca->device_id, &addr, + 0, NULL, &size, &cf_name); + if (stat != noErr) { + blog(LOG_WARNING, "[coreaudio_get_device_name] failed to " + "get name: %d", (int)stat); + return false; + } + + if (!cf_to_cstr(cf_name, name, 1024)) { + blog(LOG_WARNING, "[coreaudio_get_device_name] failed to " + "convert name to cstr for some reason"); + return false; + } + + bfree(ca->device_name); + ca->device_name = bstrdup(name); + + CFRelease(cf_name); + return true; +} + +static bool coreaudio_start(struct coreaudio_data *ca) +{ + OSStatus stat; + + if (ca->active) + return true; + + stat = AudioOutputUnitStart(ca->unit); + return ca_success(stat, ca, "coreaudio_start", "start audio"); +} + +static void coreaudio_stop(struct coreaudio_data *ca) +{ + OSStatus stat; + + if (!ca->active) + return; + + ca->active = false; + + stat = AudioOutputUnitStop(ca->unit); + ca_success(stat, ca, "coreaudio_stop", "stop audio"); +} + +static bool coreaudio_init_unit(struct coreaudio_data *ca) +{ + AudioComponentDescription desc = { + .componentType = kAudioUnitType_Output, + .componentSubType = kAudioUnitSubType_HALOutput + }; + + AudioComponent component = AudioComponentFindNext(NULL, &desc); + if (!component) { + ca_warn(ca, "coreaudio_init_unit", "find component failed"); + return false; + } + + OSStatus stat = AudioComponentInstanceNew(component, &ca->unit); + if (!ca_success(stat, ca, "coreaudio_init_unit", "instance unit")) + return false; + + return true; +} + +static bool coreaudio_init(struct coreaudio_data *ca) +{ + OSStatus stat; + + if (ca->au_initialized) + return true; + + if (!find_device_id_by_uid(ca->device_uid, &ca->device_id)) + return false; + if (!coreaudio_get_device_name(ca)) + return false; + + if (!coreaudio_init_unit(ca)) + return false; + + stat = enable_io(ca, IO_TYPE_INPUT, true); + if (!ca_success(stat, ca, "coreaudio_init", "enable input io")) + goto fail; + + stat = enable_io(ca, IO_TYPE_OUTPUT, false); + if (!ca_success(stat, ca, "coreaudio_init", "disable output io")) + goto fail; + + stat = set_property(ca->unit, kAudioOutputUnitProperty_CurrentDevice, + SCOPE_GLOBAL, 0, &ca->device_id, sizeof(ca->device_id)); + if (!ca_success(stat, ca, "coreaudio_init", "set current device")) + goto fail; + + if (!coreaudio_init_format(ca)) + goto fail; + if (!coreaudio_init_buffer(ca)) + goto fail; + if (!coreaudio_init_hooks(ca)) + goto fail; + + stat = AudioUnitInitialize(ca->unit); + if (!ca_success(stat, ca, "coreaudio_initialize", "initialize")) + goto fail; + + ca->au_initialized = coreaudio_start(ca); + + if (!ca->au_initialized) { + buf_list_free(ca->buf_list); + ca->buf_list = NULL; + } else { + blog(LOG_INFO, "coreaudio: device '%s' initialized", + ca->device_name); + + } + + return ca->au_initialized; + +fail: + coreaudio_uninit(ca); + return false; +} + +static void coreaudio_try_init(struct coreaudio_data *ca) +{ + if (!coreaudio_init(ca)) { + blog(LOG_INFO, "coreaudio: failed to find device " + "uid: %s, waiting for connection", + ca->device_uid); + + coreaudio_begin_reconnect(ca); + } +} + +static void coreaudio_uninit(struct coreaudio_data *ca) +{ + if (!ca->au_initialized) + return; + + if (ca->unit) { + coreaudio_stop(ca); + + OSStatus stat = AudioUnitUninitialize(ca->unit); + ca_success(stat, ca, "coreaudio_uninit", "uninitialize"); + + coreaudio_remove_hooks(ca); + + stat = AudioComponentInstanceDispose(ca->unit); + ca_success(stat, ca, "coreaudio_uninit", "dispose"); + + ca->unit = NULL; + } + + ca->au_initialized = false; + + buf_list_free(ca->buf_list); + ca->buf_list = NULL; +} + +/* ------------------------------------------------------------------------- */ + +static const char *coreaudio_getname(const char *locale) +{ + /* TODO: Locale */ + UNUSED_PARAMETER(locale); + return "CoreAudio Input"; +} + +static void coreaudio_destroy(void *data) +{ + struct coreaudio_data *ca = data; + + if (ca) { + if (ca->reconnecting) { + event_signal(ca->exit_event); + pthread_join(ca->reconnect_thread, NULL); + } + + coreaudio_uninit(ca); + + if (ca->unit) + AudioComponentInstanceDispose(ca->unit); + + event_destroy(ca->exit_event); + bfree(ca->device_name); + bfree(ca->device_uid); + bfree(ca); + } +} + +static void *coreaudio_create(obs_data_t settings, obs_source_t source) +{ + struct coreaudio_data *ca = bzalloc(sizeof(struct coreaudio_data)); + + obs_data_set_default_string(settings, "device_id", "Default"); + + if (event_init(&ca->exit_event, EVENT_TYPE_MANUAL) != 0) { + blog(LOG_WARNING, "[coreaudio_create] failed to create " + "semephore: %d", errno); + bfree(ca); + return NULL; + } + + ca->device_uid = bstrdup(obs_data_getstring(settings, "device_id")); + ca->source = source; + + coreaudio_try_init(ca); + return ca; +} + +struct obs_source_info coreaudio_info = { + .id = "coreaudio_capture", + .type = OBS_SOURCE_TYPE_INPUT, + .output_flags = OBS_SOURCE_AUDIO, + .getname = coreaudio_getname, + .create = coreaudio_create, + .destroy = coreaudio_destroy, +}; diff --git a/plugins/mac-capture/mac-helpers.h b/plugins/mac-capture/mac-helpers.h new file mode 100644 index 000000000..c124f88ae --- /dev/null +++ b/plugins/mac-capture/mac-helpers.h @@ -0,0 +1,16 @@ +#pragma once + +static inline bool mac_success(OSStatus stat, const char *action) +{ + if (stat != noErr) { + blog(LOG_WARNING, "%s failed: %d", action, (int)stat); + return false; + } + + return true; +} + +static inline bool cf_to_cstr(CFStringRef ref, char *buf, size_t size) +{ + return (bool)CFStringGetCString(ref, buf, size, kCFStringEncodingUTF8); +} diff --git a/plugins/mac-capture/plugin-main.c b/plugins/mac-capture/plugin-main.c new file mode 100644 index 000000000..5bf24a7d5 --- /dev/null +++ b/plugins/mac-capture/plugin-main.c @@ -0,0 +1,13 @@ +#include + +OBS_DECLARE_MODULE() + +extern struct obs_source_info coreaudio_info; + +bool obs_module_load(uint32_t libobs_version) +{ + obs_register_source(&coreaudio_info); + + UNUSED_PARAMETER(libobs_version); + return true; +} diff --git a/test/test-input/test-sinewave.c b/test/test-input/test-sinewave.c index 998714e29..ebba14efd 100644 --- a/test/test-input/test-sinewave.c +++ b/test/test-input/test-sinewave.c @@ -28,7 +28,7 @@ static void *sinewave_thread(void *pdata) double cos_val = 0.0; uint8_t bytes[480]; - while (event_try(&swd->event) == EAGAIN) { + while (event_try(swd->event) == EAGAIN) { if (!os_sleepto_ns(last_time += 10000000)) last_time = os_gettime_ns(); @@ -71,11 +71,11 @@ static void sinewave_destroy(void *data) if (swd) { if (swd->initialized_thread) { void *ret; - event_signal(&swd->event); + event_signal(swd->event); pthread_join(swd->thread, &ret); } - event_destroy(&swd->event); + event_destroy(swd->event); bfree(swd); } }