/* -LICENSE-START-
** Copyright (c) 2017 Blackmagic Design
**
** Permission is hereby granted, free of charge, to any person or organization
** obtaining a copy of the software and accompanying documentation covered by
** this license (the "Software") to use, reproduce, display, distribute,
** execute, and transmit the Software, and to prepare derivative works of the
** Software, and to permit third-parties to whom the Software is furnished to
** do so, all subject to the following:
** 
** The copyright notices in the Software and this entire statement, including
** the above license grant, this restriction and the following disclaimer,
** must be included in all copies of the Software, in whole or in part, and
** all derivative works of the Software, unless such copies or derivative
** works are solely in the form of machine-executable object code generated by
** a source language processor.
** 
** THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
** IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
** FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
** SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
** FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
** ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
** DEALINGS IN THE SOFTWARE.
** -LICENSE-END-
*/

#import "StreamingPreviewAppDelegate.h"

class DecodeSessionDelegate : public VTDecodeDelegate
{
public:
	DecodeSessionDelegate(StreamingPreviewAppDelegate* delegate)
	: mDelegate(delegate)
	{}

	virtual void haveVideoFrame(CVPixelBufferRef pixBuf, IBMDStreamingH264NALPacket* fromNAL)
	{
		[mDelegate haveVideoFrame:pixBuf fromNAL:fromNAL];
	}

	virtual ~DecodeSessionDelegate() {}

private:
	StreamingPreviewAppDelegate* mDelegate;
};

const char* BMDDeckControlVTRControlStateString(BMDDeckControlVTRControlState state)
{
	switch (state)
	{
		case bmdDeckControlNotInVTRControlMode:
			return "bmdDeckControlNotInVTRControlMode";
		case bmdDeckControlVTRControlPlaying:
			return "bmdDeckControlVTRControlPlaying";
		case bmdDeckControlVTRControlRecording:
			return "bmdDeckControlVTRControlRecording";
		case bmdDeckControlVTRControlStill:
			return "bmdDeckControlVTRControlStill";
		case bmdDeckControlVTRControlShuttleForward:
			return "bmdDeckControlVTRControlShuttleForward";
		case bmdDeckControlVTRControlShuttleReverse:
			return "bmdDeckControlVTRControlShuttleReverse";
		case bmdDeckControlVTRControlJogForward:
			return "bmdDeckControlVTRControlJogForward";
		case bmdDeckControlVTRControlJogReverse:
			return "bmdDeckControlVTRControlJogReverse";
		case bmdDeckControlVTRControlStopped:
			return "bmdDeckControlVTRControlStopped";
		default:
			break;
	}

	return "unknown";
}

const char* BMDDeckControlEventString(BMDDeckControlEvent event)
{
	switch (event)
	{
		case bmdDeckControlAbortedEvent:
			return "bmdDeckControlAbortedEvent";
		case bmdDeckControlPrepareForExportEvent:
			return "bmdDeckControlPrepareForExportEvent";
		case bmdDeckControlExportCompleteEvent:
			return "bmdDeckControlExportCompleteEvent";
		case bmdDeckControlPrepareForCaptureEvent:
			return "bmdDeckControlPrepareForCaptureEvent";
		case bmdDeckControlCaptureCompleteEvent:
			return "bmdDeckControlCaptureCompleteEvent";
		default:
			break;
	}

	return "unknown";
}

const char* BMDDeckControlErrorString(BMDDeckControlError error)
{
	switch (error)
	{
		case bmdDeckControlNoError:
			return "bmdDeckControlNoError";
		case bmdDeckControlModeError:
			return "bmdDeckControlModeError";
		case bmdDeckControlMissedInPointError:
			return "bmdDeckControlMissedInPointError";
		case bmdDeckControlDeckTimeoutError:
			return "bmdDeckControlDeckTimeoutError";
		case bmdDeckControlCommandFailedError:
			return "bmdDeckControlCommandFailedError";
		case bmdDeckControlDeviceAlreadyOpenedError:
			return "bmdDeckControlDeviceAlreadyOpenedError";
		case bmdDeckControlFailedToOpenDeviceError:
			return "bmdDeckControlFailedToOpenDeviceError";
		case bmdDeckControlInLocalModeError:
			return "bmdDeckControlInLocalModeError";
		case bmdDeckControlEndOfTapeError:
			return "bmdDeckControlEndOfTapeError";
		case bmdDeckControlUserAbortError:
			return "bmdDeckControlUserAbortError";
		case bmdDeckControlNoTapeInDeckError:
			return "bmdDeckControlNoTapeInDeckError";
		case bmdDeckControlNoVideoFromCardError:
			return "bmdDeckControlNoVideoFromCardError";
		case bmdDeckControlNoCommunicationError:
			return "bmdDeckControlNoCommunicationError";
		case bmdDeckControlBufferTooSmallError:
			return "bmdDeckControlBufferTooSmallError";
		case bmdDeckControlBadChecksumError:
			return "bmdDeckControlBadChecksumError";
		case bmdDeckControlUnknownError:
			return "bmdDeckControlUnknownError";
		default:
			break;
	}

	return "unknown";
}

@implementation StreamingPreviewAppDelegate

@synthesize window;

- (void)applicationDidFinishLaunching:(NSNotification*)aNotification
{
	long err;

	// Objective C object instance variables are initialised to zero by default.
	// Set the ones that need non-zero default values here.
	mDeviceMode = bmdStreamingDeviceUnknown;
	mInputConnector = 0;
	mInputMode = bmdModeUnknown;

	mDecodeSessionLock = [[NSLock alloc] init];
	BAIL_IF(!mDecodeSessionLock, "Failed to allocate Decode Session Lock\n");

	mDecodeDelegate = new DecodeSessionDelegate(self);

	mDeviceNotifier = new BMDStreamingDeviceNotifier(self);
	err = mDeviceNotifier ? 0 : ENOMEM;
	BAIL_IF(err, "Failed to create a BMDStreamingDeviceNotifier instance\n");

	mDeviceInputNotifier = new BMDStreamingDeviceInputNotifier(self);
	err = mDeviceInputNotifier ? 0 : ENOMEM;
	BAIL_IF(err, "Failed to create a BMDStreamingDeviceInputNotifier instance\n");

	mDeckControlNotifier = new BMDStreamingDeckControlNotifier(self);
	err = mDeckControlNotifier ? 0 : ENOMEM;
	BAIL_IF(err, "Failed to create a BMDStreamingDeckControlNotifier instance\n");

	mDiscovery = CreateBMDStreamingDiscoveryInstance();
	err = mDiscovery ? 0 : EINVAL;
	BAIL_IF(err, "CreateBMDStreamingDiscoveryInstance() failed\n");

	err = mDiscovery->InstallDeviceNotifications(mDeviceNotifier);
	BAIL_IF(err, "InstallDeviceNotifications() returned 0x%08lx\n", err);

	mAudioStreamDecoder = new AudioStreamDecoder();
	BAIL_IF(!mAudioStreamDecoder, "Failed to allocate an AudioStreamDecoder\n");

	err = mAudioStreamDecoder->Init();
	BAIL_IF(err, "AudioStreamDecoder::Init() returned %ld\n", err);

	[self updateUIForNoDevice];

bail:
	return;
}

- (void)applicationWillTerminate:(NSNotification*)aNotification
{
	long err;

	if (mDiscovery)
	{
		err = mDiscovery->UninstallDeviceNotifications();
		CARP_IF(err, "UninstallDeviceNotifications() returned 0x%08lx\n", err);

		mDiscovery->Release();
		mDiscovery = NULL;
	}

	if (mDeckControlNotifier)
	{
		mDeckControlNotifier->Release();
		mDeckControlNotifier = NULL;
	}

	if (mDeviceInputNotifier)
	{
		mDeviceInputNotifier->Release();
		mDeviceInputNotifier = NULL;
	}

	if (mDecodeDelegate)
	{
		delete mDecodeDelegate;
		mDecodeDelegate = NULL;
	}

	if (mDeviceNotifier)
	{
		mDeviceNotifier->Release();
		mDeviceNotifier = NULL;
	}

	if (mDeckControl)
	{
		mDeckControl->Release();
		mDeckControl = NULL;
	}

	if (mDeviceInput)
	{
		mDeviceInput->Release();
		mDeviceInput = NULL;
	}

	if (mConfiguration)
	{
		mConfiguration->Release();
		mConfiguration = NULL;
	}

	if (mDevice)
	{
		mDevice->Release();
		mDevice = NULL;
	}

	if (mAudioStreamDecoder)
	{
		delete mAudioStreamDecoder;
		mAudioStreamDecoder = NULL;
	}

	[mDecodeSessionLock release];
	mDecodeSessionLock = NULL;
}

- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)sender
{
	return YES;
}

- (void)updateUIForModeChanges
{
	NSString* deviceName;
	mDevice->GetModelName((CFStringRef*)&deviceName);
	[deviceName autorelease];

	const char* status = "unknown";

	switch (mDeviceMode)
	{
		case bmdStreamingDeviceIdle:
			status = "idle";
			break;
		case bmdStreamingDeviceEncoding:
			status = "encoding";
			break;
		case bmdStreamingDeviceStopping:
			status = "stopping";
			break;
		default:
			break;
	}

	[mDeviceNameTextField setStringValue:[NSString stringWithFormat:@"Device: %@ (%s)", deviceName, status]];

	bool enablePresets = mDeviceMode == bmdStreamingDeviceIdle && mInputMode != bmdModeUnknown;
	[mVideoEncodingPresetPopup setEnabled:enablePresets];
	
	bool enableStartStop = (mDeviceMode == bmdStreamingDeviceIdle || mDeviceMode == bmdStreamingDeviceEncoding) && mInputMode != bmdModeUnknown;
	[mStartPreviewButton setEnabled:enableStartStop];

	bool start = mDeviceMode != bmdStreamingDeviceEncoding;
	[mStartPreviewButton setTitle:[NSString stringWithFormat:@"%s Capture", start ? "Start" : "Stop"]];

	if (mDeviceMode == bmdStreamingDeviceEncoding)
	{
		if (mInputMode != bmdModeUnknown)
			[self startCapture];
	}
	else
		[self stopCapture];
}

- (void)haveVideoFrame:(CVPixelBufferRef)pixBuf fromNAL:(IBMDStreamingH264NALPacket*)nal
{
	[mPreview enqueueVideoFrame:pixBuf fromNAL:nal];
}

- (void)updateUIForNewDevice
{
	NSString* deviceName;
	mDevice->GetModelName((CFStringRef*)&deviceName);
	[deviceName autorelease];
	
	[mDeviceNameTextField setStringValue:[NSString stringWithFormat:@"Device: %@", deviceName]];
	
	[mVideoInputModePopup removeAllItems];
	[mVideoEncodingPresetPopup removeAllItems];
	[mStartPreviewButton setTitle:@"Start Capture"];

	[mVideoInputModePopup addItemWithTitle:@"No input"];
	[[mVideoInputModePopup lastItem] setTag:bmdModeUnknown];

	// Fill video input mode:
	IDeckLinkDisplayModeIterator* inputModeIterator;
	if (FAILED(mDeviceInput->GetVideoInputModeIterator(&inputModeIterator)))
	{
		[NSAlert alertWithMessageText:@"Streaming API error"
						defaultButton:@"OK" alternateButton:nil otherButton:nil
			informativeTextWithFormat:@"Failed to get input mode iterator"];
		goto bail;
	}

	IDeckLinkDisplayMode* inputMode;
	while (inputModeIterator->Next(&inputMode) == S_OK)
	{
		NSString* modeName;
		
		inputMode->GetName((CFStringRef*)&modeName);
		[modeName autorelease];
		
		[mVideoInputModePopup addItemWithTitle:modeName];
		[[mVideoInputModePopup lastItem] setTag:inputMode->GetDisplayMode()];
		
		inputMode->Release();
	}

	inputModeIterator->Release();

	BMDDisplayMode mode;
	if (mDeviceInput->GetCurrentDetectedVideoInputMode(&mode) != S_OK)
	{
		[NSAlert alertWithMessageText:@"Streaming API error"
						defaultButton:@"OK" alternateButton:nil otherButton:nil
			informativeTextWithFormat:@"Failed to get current detected input mode"];
		goto bail;
	}

	[mVideoInputModePopup selectItemWithTag:mode];

	// Select the first displayed mode, update the preset menu
	[self videoInputModeChanged:self];
	
	return;

bail:
	[self updateUIForNoDevice];
}

- (void)updateUIForNoDevice
{
	mDeviceMode = bmdStreamingDeviceUnknown;
	mInputMode = bmdModeUnknown;

	[mDeviceNameTextField setStringValue:@"No device detected"];

	[mVideoInputModePopup removeAllItems];
	[mVideoInputModePopup addItemWithTitle:@"No input"];
	[[mVideoInputModePopup lastItem] setTag:bmdModeUnknown];
	[mVideoInputModePopup setEnabled:NO];

	[mVideoEncodingPresetPopup removeAllItems];
	[mVideoEncodingPresetPopup addItemWithTitle:@"No input"];
	[[mVideoEncodingPresetPopup lastItem] setTag:bmdModeUnknown];
	[mVideoEncodingPresetPopup setEnabled:NO];

	[mStartPreviewButton setTitle:@"Start Capture"];
	[mStartPreviewButton setEnabled:NO];
}


- (void)updateEncodingPresetsUIForInputMode
{
	BMDDisplayMode inputMode = [[mVideoInputModePopup selectedItem] tag];

	[mVideoEncodingPresetPopup removeAllItems];
	
	IBMDStreamingVideoEncodingModePresetIterator* presetIterator;
	
	int indexToSelect = 0;
	if (SUCCEEDED(mDeviceInput->GetVideoEncodingModePresetIterator(inputMode, &presetIterator)))
	{
		IBMDStreamingVideoEncodingMode* encodingMode = NULL;
		NSString* encodingModeName = NULL;
		NSString* currentEncodingModeName = NULL;
		IBMDStreamingVideoEncodingMode* currentEncodingMode = NULL;
		int index = 0;
		
		if (SUCCEEDED(mDeviceInput->GetVideoEncodingMode(&currentEncodingMode)))
			currentEncodingMode->GetName((CFStringRef*)&currentEncodingModeName);
		
		while (presetIterator->Next(&encodingMode) == S_OK)
		{
			encodingMode->GetName((CFStringRef*)&encodingModeName);
			[mVideoEncodingPresetPopup addItemWithTitle:encodingModeName];
			
			if ([currentEncodingModeName compare:encodingModeName] == NSOrderedSame)
				indexToSelect = index;

			index++;
			[encodingModeName release];
			encodingMode->Release();
		}

		[currentEncodingModeName release];
		presetIterator->Release();
	}
	
	// Select the current encoding mode, if encoding, or just the first preset in the list
	[mVideoEncodingPresetPopup selectItemAtIndex:indexToSelect];
}

- (IBMDStreamingVideoEncodingMode*)getCurrentEncodingMode
{
	long err;
	BMDDisplayMode mode;
	IBMDStreamingVideoEncodingMode* encodingMode = NULL;
	IBMDStreamingVideoEncodingModePresetIterator* it = NULL;

	BAIL_IF(!mDeviceInput, "Device input is NULL\n");

	err = mDeviceInput->GetCurrentDetectedVideoInputMode(&mode);
	BAIL_IF(err, "GetCurrentDetectedVideoInputMode returned 0x%lx\n", err);

	if (mode == bmdModeUnknown)
		goto bail;

	err = mDeviceInput->GetVideoEncodingModePresetIterator(mode, &it);
	BAIL_IF(err, "GetVideoEncodingModePresetIterator returned 0x%lx\n", err);
	
	while (it->Next(&encodingMode) == S_OK)
	{
		NSString* modeName;
		encodingMode->GetName((CFStringRef*)&modeName);
		[modeName autorelease];

		if ([[mVideoEncodingPresetPopup titleOfSelectedItem] compare:modeName] == NSOrderedSame)
			break;

		encodingMode->Release();
		encodingMode = NULL;
	}

	BAIL_IF(!encodingMode, "Failed to find matching encoding mode\n");

bail:
	if (it)
		it->Release();

	return encodingMode;
}

- (void)startCapture
{
	if (mCapturing)
		return;

	// Set the encoding mode based on the current preset:
	IBMDStreamingVideoEncodingMode* encodingMode = [self getCurrentEncodingMode];
	if (!encodingMode)
		return;

	// customise encoding parameters here

	mDeviceInput->SetVideoEncodingMode(encodingMode);

	mDecodeSession = new VTDecodeSession();
	mDecodeSession->setDelegate(mDecodeDelegate);

	[mPreview startPlayback];

	long err = mAudioStreamDecoder->Start();
	CARP_IF(err, "Start returned %ld\n", err);

	HRESULT hr = mDeviceInput->StartCapture();
	CARP_IF(FAILED(hr), "StartCapture returned %ld\n", hr);

	mCapturing = true;

	encodingMode->Release();
}

- (void)stopCapture
{
	if (!mCapturing)
		return;
	
	mCapturing = false;

	if (mDeviceInput)
		mDeviceInput->StopCapture();

	[mDecodeSessionLock lock];
	mDecodeSession->setDelegate(NULL);
	delete mDecodeSession;
	mDecodeSession = NULL;
	[mDecodeSessionLock unlock];

	[mPreview stopPlayback];

	long err = mAudioStreamDecoder->Stop();
	CARP_IF(err, "Stop returned %ld\n", err);
}

- (IBAction)startEncodingPressed:(id)sender
{
	if (mDeviceMode == bmdStreamingDeviceEncoding)
		[self stopCapture];
	else if (mDeviceMode == bmdStreamingDeviceIdle)
		[self startCapture];
}

- (IBAction)videoInputModeChanged:(id)sender
{
	if (mDevice == NULL)
		return;
	
	[self updateEncodingPresetsUIForInputMode];
}

- (void)streamingDeviceArrived:(IDeckLink*)device
{
	long err;

	if (mDevice != NULL)
	{
		// This application only supports a single device at a time.
		NSLog(@"We already have a device (%p) ignoring arrival of new device (%p)", mDevice, device);
		goto bail;
	}

	if (device->QueryInterface(IID_IDeckLinkConfiguration, (void**)&mConfiguration) != S_OK)
	{
		NSLog(@"The discovered device is not configurable\n");
		goto bail;
	}
	
	if (device->QueryInterface(IID_IBMDStreamingDeviceInput, (void**)&mDeviceInput) != S_OK)
	{
		// Check we can get the IBMDStreamingDeviceInput interface.
		// If not, we'll abort before holding any references to it.
		//
		// Note: if the QueryInterface calls succeeds, we now own a reference to
		// that class. We release it in streamingDeviceRemoved.
		NSLog(@"The discovered device is not an input device\n");
		goto bail;
	}

	mDevice = device;
	mDevice->AddRef();
	
	err = mDeviceInput->SetCallback(mDeviceInputNotifier);
	if (err)
	{
		NSLog(@"Failed to install our device input callback handler (%08lx)\n", err);
		mDeviceInput->Release();
		mDeviceInput = NULL;
		mDevice->Release();
		mDevice = NULL;
		goto bail;
	}

	if (device->QueryInterface(IID_IDeckLinkDeckControl, (void**)&mDeckControl) == S_OK)
	{
		err = mDeckControl->SetCallback(mDeckControlNotifier);
		if (err)
		{
			NSLog(@"Failed to install our deck control callback handler (%08lx)\n", err);
			mDeckControl->Release();
			mDeckControl = NULL;
			mDeviceInput->Release();
			mDeviceInput = NULL;
			mDevice->Release();
			mDevice = NULL;
			goto bail;
		}
	}

	[self performSelectorOnMainThread:@selector(updateUIForNewDevice) withObject:NULL waitUntilDone:NO];

bail:
	return;
}

- (void)streamingDeviceRemoved:(IDeckLink*)device
{
	if (mDevice == NULL || mDevice != device)
	{
		// We only care about removals of the device we are actually using
		// (mDevice).
		goto bail;
	}
	
	// Remove our device input callback handler (not strictly necessary, but good to do anyhow)
	mDeviceInput->SetCallback(NULL);

	if (mDeckControl)
	{
		mDeckControl->SetCallback(NULL);
		mDeckControl->Release();
		mDeckControl = NULL;
	}

	mDeviceInput->Release();
	mDeviceInput = NULL;

	mConfiguration->Release();
	mConfiguration = NULL;

	mDevice->Release();
	mDevice = NULL;
	
	[self performSelectorOnMainThread:@selector(stopCapture) withObject:NULL waitUntilDone:YES];
	[self performSelectorOnMainThread:@selector(updateUIForNoDevice) withObject:NULL waitUntilDone:NO];

bail:
	return;
}

- (void)streamingDeviceModeChanged:(IDeckLink*)device toMode:(BMDStreamingDeviceMode)mode
{
	if (mode == mDeviceMode)
		return;

	mDeviceMode = mode;

	[self performSelectorOnMainThread:@selector(updateUIForModeChanges) withObject:NULL waitUntilDone:NO];
}

- (void)streamingDeviceFirmwareUpdateProgress:(IDeckLink*)device percent:(uint8_t)percent
{
}

- (void)h264NALPacketArrived:(IBMDStreamingH264NALPacket*)packet
{
	if (!mDecodeSession)
		return;

	[mDecodeSessionLock lock];
	if (mDecodeSession)
		mDecodeSession->handleH264NAL(packet);
	[mDecodeSessionLock unlock];
}

- (void)h264AudioPacketArrived:(IBMDStreamingAudioPacket*)packet
{
	if (!mAudioStreamDecoder || !mCapturing)
		return;

	unsigned char* buffer;
	long err = packet->GetBytes((void**)&buffer);
	BAIL_IF(err, "GetBytes() returned 0x%lx\n", err);

	err = mAudioStreamDecoder->EnqueueData(buffer, packet->GetPayloadSize(), true);
	BAIL_IF(err, "EnqueueData() returned %ld\n", err);

bail:
	return;
}

- (void)mpeg2TSPacketArrived:(IBMDStreamingMPEG2TSPacket*)packet
{
}

- (void)h264VideoInputConnectorChanged
{
	int64_t value;
	if (mConfiguration->GetInt(bmdDeckLinkConfigVideoInputConnection, &value) == S_OK)
		mInputConnector = value;
	else
		[NSAlert alertWithMessageText:@"Streaming API error"
						defaultButton:@"OK" alternateButton:nil otherButton:nil
			informativeTextWithFormat:@"Failed to get current input connector"];
}

- (void)h264VideoInputConnectorScanningChanged
{
	bool enabled = false;
	if (mConfiguration->GetFlag(bmdDeckLinkConfigVideoInputScanning, &enabled) != S_OK)
		[NSAlert alertWithMessageText:@"Streaming API error"
						defaultButton:@"OK" alternateButton:nil otherButton:nil
			informativeTextWithFormat:@"Failed to get connector scanning status"];
}

- (void)h264VideoInputModeChanged
{
	BMDDisplayMode oldMode = mInputMode;

	if (mDeviceInput->GetCurrentDetectedVideoInputMode(&mInputMode) != S_OK)
	{
		[NSAlert alertWithMessageText:@"Streaming API error"
						defaultButton:@"OK" alternateButton:nil otherButton:nil
			informativeTextWithFormat:@"Failed to get current detected input mode"];
	}
	else
	{
		[mVideoInputModePopup selectItemWithTag:mInputMode];

		[self performSelectorOnMainThread:@selector(videoInputModeChanged:) withObject:self waitUntilDone:YES];
	}

	if (mInputMode == bmdModeUnknown)
	{
		[mVideoEncodingPresetPopup removeAllItems];
		[mVideoEncodingPresetPopup addItemWithTitle:@"No input"];
		[[mVideoEncodingPresetPopup lastItem] setTag:bmdModeUnknown];
	}

	[self performSelectorOnMainThread:@selector(updateUIForModeChanges) withObject:NULL waitUntilDone:YES];

	if (oldMode != bmdModeUnknown && mDeckControl)
		mDeckControl->Close(true);

	if (mInputMode == bmdModeUnknown)
		return;

	long err;
	BMDDeckControlError error = bmdDeckControlNoError;

	if (mInputMode != oldMode)
	{
		int rateNum, rateDen = 0;

		switch (mInputMode)
		{
			case bmdModeNTSC2398:
			case bmdModeHD1080p2398:
				rateNum = 24000;
				rateDen = 1001;
				break;
			case bmdModeHD1080p24:
				rateNum = 24;
				rateDen = 1;
				break;
			case bmdModePAL:
			case bmdModePALp:
			case bmdModeHD1080p25:
			case bmdModeHD1080i50:
				rateNum = 25;
				rateDen = 1;
				break;
			case bmdModeNTSC:
			case bmdModeNTSCp:
			case bmdModeHD1080p2997:
			case bmdModeHD1080i5994:
				rateNum = 30000;
				rateDen = 1001;
				break;
			case bmdModeHD1080p30:
			case bmdModeHD1080i6000:
				rateNum = 30;
				rateDen = 1;
				break;
			case bmdModeHD1080p50:
			case bmdModeHD720p50:
				rateNum = 50;
				rateDen = 1;
				break;
			case bmdModeHD1080p5994:
			case bmdModeHD720p5994:
				rateNum = 60000;
				rateDen = 1001;
				break;
			case bmdModeHD1080p6000:
			case bmdModeHD720p60:
				rateNum = 60;
				rateDen = 1;
				break;
			default:
				break;
		}

		BAIL_IF(!rateDen, "Invalid input mode %d\n", mInputMode);

		if (mDeckControl)
		{
			err = mDeckControl->Open(rateNum, rateDen, true, &error);
			BAIL_IF(err || error != bmdDeckControlNoError, "Open returned %lx and %s\n", err, BMDDeckControlErrorString(error));
		}
	}

	if (mDeckControl)
	{
//#define TEST_GOTO
#ifdef TEST_GOTO
		BMDTimecodeBCD currentTimecode;
		unsigned int hours, minutes, seconds, frames;

		err = mDeckControl->GetTimecodeBCD(&currentTimecode, &error);
		BAIL_IF(err || error != bmdDeckControlNoError, "GetTimecodeBCD returned %lx and %s\n", err, BMDDeckControlErrorString(error));

		hours = currentTimecode >> 24;
		minutes = (currentTimecode >> 16) & 0xff;
		seconds = (currentTimecode >> 8) & 0xff;
		frames = currentTimecode & 0xff;

		hours = (hours >> 4) * 10 + (hours & 0xf);
		minutes = (minutes >> 4) * 10 + (minutes & 0xf);
		seconds = (seconds >> 4) * 10 + (seconds & 0xf);
		frames = (frames >> 4) * 10 + (frames & 0xf);

		fprintf(stderr, "%s: current %02u:%02u:%02u:%02u\n", __FUNCTION__, hours, minutes, seconds, frames);

		if (++minutes == 60)
		{
			minutes = 0;
			hours++;
		}

		fprintf(stderr, "%s: seeking %02u:%02u:%02u:%02u\n", __FUNCTION__, hours, minutes, seconds, frames);

		hours = ((hours / 10) << 4) | hours % 10;
		minutes = ((minutes / 10) << 4) | minutes % 10;
		seconds = ((seconds / 10) << 4) | seconds % 10;
		frames = ((frames / 10) << 4) | frames % 10;

		err = mDeckControl->GoToTimecode((hours << 24) | (minutes << 16) | (seconds << 8) | frames, &error);
		CARP_IF(err || error != bmdDeckControlNoError, "GoToTimecode returned %lx and %s\n", err, BMDDeckControlErrorString(error));
#endif

//#define TEST_CAPTURE
#ifdef TEST_CAPTURE
		mDeviceInput->SetVideoEncodingMode([self getCurrentEncodingMode]);

		err = mDeckControl->StartCapture(true,
										 (0x01 << 24) | (0x00 << 16) | (0x20 << 8) | 0x00,
										 (0x01 << 24) | (0x00 << 16) | (0x30 << 8) | 0x00,
										 &error);
		CARP_IF(err || error != bmdDeckControlNoError, "StartCapture returned %lx and %s\n", err, BMDDeckControlErrorString(error));
#endif
	}

bail:
	return;
}

- (void)timecodeUpdate:(BMDTimecodeBCD)currentTimecode
{
	unsigned int hours = currentTimecode >> 24;
	unsigned int minutes = (currentTimecode >> 16) & 0xff;
	unsigned int seconds = (currentTimecode >> 8) & 0xff;
	unsigned int frames = currentTimecode & 0xff;

	hours = (hours >> 4) * 10 + (hours & 0xf);
	minutes = (minutes >> 4) * 10 + (minutes & 0xf);
	seconds = (seconds >> 4) * 10 + (seconds & 0xf);
	frames = (frames >> 4) * 10 + (frames & 0xf);

	fprintf(stderr, "%s: %02u:%02u:%02u:%02u\n", __FUNCTION__, hours, minutes, seconds, frames);
}

- (void)vtrControlStateChanged:(BMDDeckControlVTRControlState)newState error:(BMDDeckControlError)error
{
	fprintf(stderr, "%s: %s error %s\n", __FUNCTION__, BMDDeckControlVTRControlStateString(newState), BMDDeckControlErrorString(error));
}

- (void)deckControlEventReceived:(BMDDeckControlEvent)event error:(BMDDeckControlError)error
{
	fprintf(stderr, "%s: %s error %s\n", __FUNCTION__, BMDDeckControlEventString(event), BMDDeckControlErrorString(error));
}

- (void)deckControlStatusChanged:(BMDDeckControlStatusFlags)flags mask:(uint32_t)mask
{
	if (mask & bmdDeckControlStatusDeckConnected)
		fprintf(stderr, "%s: bmdDeckControlStatusDeckConnected %d -> %d\n", __FUNCTION__,
				(flags & bmdDeckControlStatusDeckConnected) == 0,
				(flags & bmdDeckControlStatusDeckConnected) != 0);
	if (mask & bmdDeckControlStatusRemoteMode)
		fprintf(stderr, "%s: bmdDeckControlStatusRemoteMode %d -> %d\n", __FUNCTION__,
				(flags & bmdDeckControlStatusRemoteMode) == 0,
				(flags & bmdDeckControlStatusRemoteMode) != 0);
	if (mask & bmdDeckControlStatusRecordInhibited)
		fprintf(stderr, "%s: bmdDeckControlStatusRecordInhibited %d -> %d\n", __FUNCTION__,
				(flags & bmdDeckControlStatusRecordInhibited) == 0,
				(flags & bmdDeckControlStatusRecordInhibited) != 0);
	if (mask & bmdDeckControlStatusCassetteOut)
		fprintf(stderr, "%s: bmdDeckControlStatusCassetteOut %d -> %d\n", __FUNCTION__,
				(flags & bmdDeckControlStatusCassetteOut) == 0,
				(flags & bmdDeckControlStatusCassetteOut) != 0);
}

@end
