1
0
Fork 0
mirror of https://github.com/juce-framework/JUCE.git synced 2026-01-10 23:44:24 +00:00

Add camera support for iOS and Android.

This commit is contained in:
Lukasz Kozakiewicz 2018-04-25 18:54:04 +02:00
parent bac6996d98
commit 772185f2b0
75 changed files with 6619 additions and 421 deletions

View file

@ -27,22 +27,129 @@
namespace juce
{
#if JUCE_MAC || JUCE_IOS
#if JUCE_MAC
#include "../native/juce_mac_CameraDevice.h"
#elif JUCE_WINDOWS
#include "../native/juce_win32_CameraDevice.h"
#elif JUCE_IOS
#if JUCE_CLANG
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability-new"
#endif
#include "../native/juce_ios_CameraDevice.h"
#if JUCE_CLANG
#pragma clang diagnostic pop
#endif
#elif JUCE_ANDROID
#include "../native/juce_android_CameraDevice.h"
#endif
#if JUCE_ANDROID || JUCE_IOS
//==============================================================================
class CameraDevice::CameraFactory
{
public:
static CameraFactory& getInstance()
{
static CameraFactory factory;
return factory;
}
void openCamera (int index, OpenCameraResultCallback resultCallback,
int minWidth, int minHeight, int maxWidth, int maxHeight, bool useHighQuality)
{
auto cameraId = getAvailableDevices()[index];
if (getCameraIndex (cameraId) != -1)
{
// You are trying to open the same camera twice.
jassertfalse;
return;
}
std::unique_ptr<CameraDevice> device (new CameraDevice (cameraId, index,
minWidth, minHeight, maxWidth,
maxHeight, useHighQuality));
camerasToOpen.add ({ nextRequestId++,
std::unique_ptr<CameraDevice> (device.release()),
resultCallback });
auto& pendingOpen = camerasToOpen.getReference (camerasToOpen.size() - 1);
pendingOpen.device->pimpl->open ([this](const String& deviceId, const String& error)
{
int index = getCameraIndex (deviceId);
if (index == -1)
return;
auto& pendingOpen = camerasToOpen.getReference (index);
if (error.isEmpty())
pendingOpen.resultCallback (pendingOpen.device.release(), error);
else
pendingOpen.resultCallback (nullptr, error);
int id = pendingOpen.requestId;
MessageManager::callAsync ([this, id]() { removeRequestWithId (id); });
});
}
private:
int getCameraIndex (const String& cameraId) const
{
for (int i = 0; i < camerasToOpen.size(); ++i)
{
auto& pendingOpen = camerasToOpen.getReference (i);
if (pendingOpen.device->pimpl->getCameraId() == cameraId)
return i;
}
return -1;
}
void removeRequestWithId (int id)
{
for (int i = camerasToOpen.size(); --i >= 0;)
{
if (camerasToOpen.getReference (i).requestId == id)
{
camerasToOpen.remove (i);
return;
}
}
}
struct PendingCameraOpen
{
int requestId;
std::unique_ptr<CameraDevice> device;
OpenCameraResultCallback resultCallback;
};
Array<PendingCameraOpen> camerasToOpen;
static int nextRequestId;
};
int CameraDevice::CameraFactory::nextRequestId = 0;
#endif
//==============================================================================
CameraDevice::CameraDevice (const String& nm, int index, int minWidth, int minHeight, int maxWidth, int maxHeight, bool useHighQuality)
: name (nm), pimpl (new Pimpl (name, index, minWidth, minHeight, maxWidth, maxHeight, useHighQuality))
: name (nm), pimpl (new Pimpl (*this, name, index, minWidth, minHeight, maxWidth, maxHeight, useHighQuality))
{
}
CameraDevice::~CameraDevice()
{
jassert (juce::MessageManager::getInstance()->currentThreadHasLockedMessageManager());
stopRecording();
pimpl.reset();
}
@ -52,6 +159,11 @@ Component* CameraDevice::createViewerComponent()
return new ViewerComponent (*this);
}
void CameraDevice::takeStillPicture (std::function<void (const Image&)> pictureTakenCallback)
{
pimpl->takeStillPicture (pictureTakenCallback);
}
void CameraDevice::startRecordingToFile (const File& file, int quality)
{
stopRecording();
@ -68,18 +180,6 @@ void CameraDevice::stopRecording()
pimpl->stopRecording();
}
void CameraDevice::addListener (Listener* listenerToAdd)
{
if (listenerToAdd != nullptr)
pimpl->addListener (listenerToAdd);
}
void CameraDevice::removeListener (Listener* listenerToRemove)
{
if (listenerToRemove != nullptr)
pimpl->removeListener (listenerToRemove);
}
//==============================================================================
StringArray CameraDevice::getAvailableDevices()
{
@ -94,12 +194,44 @@ CameraDevice* CameraDevice::openDevice (int index,
int maxWidth, int maxHeight,
bool useHighQuality)
{
jassert (juce::MessageManager::getInstance()->currentThreadHasLockedMessageManager());
#if ! JUCE_ANDROID && ! JUCE_IOS
std::unique_ptr<CameraDevice> d (new CameraDevice (getAvailableDevices() [index], index,
minWidth, minHeight, maxWidth, maxHeight, useHighQuality));
if (d != nullptr && d->pimpl->openedOk())
return d.release();
#else
ignoreUnused (index, minWidth, minHeight);
ignoreUnused (maxWidth, maxHeight, useHighQuality);
// Use openDeviceAsync to open a camera device on iOS or Android.
jassertfalse;
#endif
return nullptr;
}
void CameraDevice::openDeviceAsync (int index, OpenCameraResultCallback resultCallback,
int minWidth, int minHeight, int maxWidth, int maxHeight, bool useHighQuality)
{
jassert (juce::MessageManager::getInstance()->currentThreadHasLockedMessageManager());
if (resultCallback == nullptr)
{
// A valid callback must be passed.
jassertfalse;
return;
}
#if JUCE_ANDROID || JUCE_IOS
CameraFactory::getInstance().openCamera (index, static_cast<OpenCameraResultCallback&&> (resultCallback),
minWidth, minHeight, maxWidth, maxHeight, useHighQuality);
#else
auto* device = openDevice (index, minWidth, minHeight, maxWidth, maxHeight, useHighQuality);
resultCallback (device, device != nullptr ? String() : "Could not open camera device");
#endif
}
} // namespace juce

View file

@ -35,9 +35,9 @@ namespace juce
Controls any video capture devices that might be available.
Use getAvailableDevices() to list the devices that are attached to the
system, then call openDevice to open one for use. Once you have a CameraDevice
object, you can get a viewer component from it, and use its methods to
stream to a file or capture still-frames.
system, then call openDevice() or openDeviceAsync() to open one for use.
Once you have a CameraDevice object, you can get a viewer component from it,
and use its methods to stream to a file or capture still-frames.
@tags{Video}
*/
@ -50,17 +50,18 @@ public:
//==============================================================================
/** Returns a list of the available cameras on this machine.
You can open one of these devices by calling openDevice().
You can open one of these devices by calling openDevice() or openDeviceAsync().
*/
static StringArray getAvailableDevices();
/** Opens a camera device.
/** Synchronously opens a camera device. This function should not be used on iOS or
Android, use openDeviceAsync() instead.
The index parameter indicates which of the items returned by getAvailableDevices()
to open.
The size constraints allow the method to choose between different resolutions if
the camera supports this. If the resolution cam't be specified (e.g. on the Mac)
the camera supports this. If the resolution can't be specified (e.g. on the Mac)
then these will be ignored.
On Mac, if highQuality is false, then the camera will be opened in preview mode
@ -72,16 +73,62 @@ public:
int maxWidth = 1024, int maxHeight = 768,
bool highQuality = true);
using OpenCameraResultCallback = std::function<void (CameraDevice*, const String& /*error*/)>;
/** Asynchronously opens a camera device on iOS (iOS 7+) or Android (API 21+).
On other platforms, the function will simply call openDevice(). Upon completion,
resultCallback will be invoked with valid CameraDevice* and an empty error
String on success, or nullptr CameraDevice and a non-empty error String on failure.
This is the preferred method of opening a camera device, because it works on all
platforms, whereas synchronous openDevice() does not work on iOS & Android.
The index parameter indicates which of the items returned by getAvailableDevices()
to open.
The size constraints allow the method to choose between different resolutions if
the camera supports this. If the resolution can't be specified then these will be
ignored.
On iOS, if you want to switch a device, it is more efficient to open a new device
before closing the older one, because this way both devices can share the same
underlying camera session. Otherwise, the session needs to be close first, and this
is a lengthy process that can take several seconds.
The Android implementation currently supports a maximum recording resolution of
1080p. Choosing a larger size will result in larger pictures taken, but the video
will be capped at 1080p.
*/
static void openDeviceAsync (int deviceIndex,
OpenCameraResultCallback resultCallback,
int minWidth = 128, int minHeight = 64,
int maxWidth = 1024, int maxHeight = 768,
bool highQuality = true);
//==============================================================================
/** Returns the name of this device */
const String& getName() const noexcept { return name; }
/** Creates a component that can be used to display a preview of the
video from this camera.
Note: while you can change the size of the preview component, the actual
preview display may be smaller than the size requested, because the correct
aspect ratio is maintained automatically.
*/
Component* createViewerComponent();
//==============================================================================
/** Triggers a still picture capture. Upon completion, pictureTakenCallback will
be invoked on a message thread.
On Android, before calling takeStillPicture(), you need to create a preview with
createViewerComponent() and you need to make it visible on screen.
Android does not support simultaneous video recording and still picture capture.
*/
void takeStillPicture (std::function<void (const Image&)> pictureTakenCallback);
/** Starts recording video to the specified file.
You should use getFileExtension() to find out the correct extension to
@ -95,6 +142,16 @@ public:
The quality parameter can be 0, 1, or 2, to indicate low, medium, or high. It may
or may not be used, depending on the driver.
On Android, before calling startRecordingToFile(), you need to create a preview with
createViewerComponent() and you need to make it visible on screen.
The Android camera also requires exclusive access to the audio device, so make sure
you close any open audio devices with AudioDeviceManager::closeAudioDevice() first.
Android does not support simultaneous video recording and still picture capture.
@see AudioDeviceManager::closeAudioDevice, AudioDeviceManager::restartLastAudioDevice
*/
void startRecordingToFile (const File& file, int quality = 2);
@ -113,36 +170,9 @@ public:
*/
Time getTimeOfFirstRecordedFrame() const;
//==============================================================================
/**
Receives callbacks with images from a CameraDevice.
@see CameraDevice::addListener
*/
class JUCE_API Listener
{
public:
Listener() {}
virtual ~Listener() {}
/** This method is called when a new image arrives.
This may be called by any thread, so be careful about thread-safety,
and make sure that you process the data as quickly as possible to
avoid glitching!
*/
virtual void imageReceived (const Image& image) = 0;
};
/** Adds a listener to receive images from the camera.
Be very careful not to delete the listener without first removing it by calling
removeListener().
*/
void addListener (Listener* listenerToAdd);
/** Removes a listener that was previously added with addListener(). */
void removeListener (Listener* listenerToRemove);
/** Set this callback to be notified whenever an error occurs. You may need to close
and reopen the device to be able to use it further. */
std::function<void (const String& /*error*/)> onErrorOccurred;
private:
String name;
@ -158,6 +188,32 @@ private:
CameraDevice (const String& name, int index,
int minWidth, int minHeight, int maxWidth, int maxHeight, bool highQuality);
#if JUCE_ANDROID || JUCE_IOS
class CameraFactory;
#endif
#if JUCE_ANDROID
friend void juce_cameraDeviceStateClosed (int64);
friend void juce_cameraDeviceStateDisconnected (int64);
friend void juce_cameraDeviceStateError (int64, int);
friend void juce_cameraDeviceStateOpened (int64, void*);
friend void juce_cameraCaptureSessionActive (int64, void*);
friend void juce_cameraCaptureSessionClosed (int64, void*);
friend void juce_cameraCaptureSessionConfigureFailed (int64, void*);
friend void juce_cameraCaptureSessionConfigured (int64, void*);
friend void juce_cameraCaptureSessionReady (int64, void*);
friend void juce_cameraCaptureSessionCaptureCompleted (int64, bool, void*, void*, void*);
friend void juce_cameraCaptureSessionCaptureFailed (int64, bool, void*, void*, void*);
friend void juce_cameraCaptureSessionCaptureProgressed (int64, bool, void*, void*, void*);
friend void juce_cameraCaptureSessionCaptureSequenceAborted (int64, bool, void*, int);
friend void juce_cameraCaptureSessionCaptureSequenceCompleted (int64, bool, void*, int, int64);
friend void juce_cameraCaptureSessionCaptureStarted (int64, bool, void*, void*, int64, int64);
friend void juce_deviceOrientationChanged (int64, int);
#endif
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CameraDevice)
};

View file

@ -59,13 +59,23 @@
//=============================================================================
/** Config: JUCE_USE_CAMERA
Enables web-cam support using the CameraDevice class (Mac and Windows).
Enables camera support using the CameraDevice class (Mac, Windows, iOS, Android).
*/
#ifndef JUCE_USE_CAMERA
#define JUCE_USE_CAMERA 0
#endif
#if ! (JUCE_MAC || JUCE_WINDOWS)
#ifndef JUCE_CAMERA_LOG_ENABLED
#define JUCE_CAMERA_LOG_ENABLED 0
#endif
#if JUCE_CAMERA_LOG_ENABLED
#define JUCE_CAMERA_LOG(x) DBG(x)
#else
#define JUCE_CAMERA_LOG(x) {}
#endif
#if ! (JUCE_MAC || JUCE_WINDOWS || JUCE_IOS || JUCE_ANDROID)
#undef JUCE_USE_CAMERA
#endif

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -26,8 +26,9 @@
struct CameraDevice::Pimpl
{
Pimpl (const String&, int /*index*/, int /*minWidth*/, int /*minHeight*/,
Pimpl (CameraDevice& ownerToUse, const String&, int /*index*/, int /*minWidth*/, int /*minHeight*/,
int /*maxWidth*/, int /*maxHeight*/, bool useHighQuality)
: owner (ownerToUse)
{
JUCE_AUTORELEASEPOOL
{
@ -42,11 +43,20 @@ struct CameraDevice::Pimpl
static DelegateClass cls;
callbackDelegate = (id<AVCaptureFileOutputRecordingDelegate>) [cls.createInstance() init];
DelegateClass::setOwner (callbackDelegate, this);
SEL runtimeErrorSel = NSSelectorFromString (nsStringLiteral ("captureSessionRuntimeError:"));
[[NSNotificationCenter defaultCenter] addObserver: callbackDelegate
selector: runtimeErrorSel
name: AVCaptureSessionRuntimeErrorNotification
object: session];
}
}
~Pimpl()
{
[[NSNotificationCenter defaultCenter] removeObserver: callbackDelegate];
[session stopRunning];
removeImageCapture();
removeMovieCapture();
@ -113,6 +123,19 @@ struct CameraDevice::Pimpl
refreshConnections();
}
void takeStillPicture (std::function<void (const Image&)> pictureTakenCallbackToUse)
{
if (pictureTakenCallbackToUse == nullptr)
{
jassertfalse;
return;
}
pictureTakenCallback = static_cast<std::function<void (const Image&)>&&> (pictureTakenCallbackToUse);
triggerImageCapture();
}
void startRecordingToFile (const File& file, int /*quality*/)
{
stopRecording();
@ -150,21 +173,10 @@ struct CameraDevice::Pimpl
return nil;
}
void handleImageCapture (const void* data, size_t size)
void handleImageCapture (const Image& image)
{
auto image = ImageFileFormat::loadFrom (data, size);
const ScopedLock sl (listenerLock);
if (! listeners.isEmpty())
{
for (int i = listeners.size(); --i >= 0;)
if (auto* l = listeners[i])
l->imageReceived (image);
if (! listeners.isEmpty())
triggerImageCapture();
}
if (pictureTakenCallback != nullptr)
pictureTakenCallback (image);
}
void triggerImageCapture()
@ -174,33 +186,25 @@ struct CameraDevice::Pimpl
if (auto* videoConnection = getVideoConnection())
{
[imageOutput captureStillImageAsynchronouslyFromConnection: videoConnection
completionHandler: ^(CMSampleBufferRef sampleBuffer, NSError*)
completionHandler: ^(CMSampleBufferRef sampleBuffer, NSError* error)
{
auto buffer = CMSampleBufferGetDataBuffer (sampleBuffer);
size_t size = CMBlockBufferGetDataLength (buffer);
jassert (CMBlockBufferIsRangeContiguous (buffer, 0, size)); // TODO: need to add code to handle this if it happens
char* data = nullptr;
CMBlockBufferGetDataPointer (buffer, 0, &size, nullptr, &data);
handleImageCapture (data, size);
if (error != nil)
{
JUCE_CAMERA_LOG ("Still picture capture failed, error: " + nsStringToJuce (error.localizedDescription));
jassertfalse;
return;
}
NSData* imageData = [AVCaptureStillImageOutput jpegStillImageNSDataRepresentation: sampleBuffer];
auto image = ImageFileFormat::loadFrom (imageData.bytes, (size_t) imageData.length);
WeakReference<Pimpl> weakRef (this);
MessageManager::callAsync ([weakRef, image]() mutable { if (weakRef != nullptr) weakRef->handleImageCapture (image); });
}];
}
}
void addListener (CameraDevice::Listener* listenerToAdd)
{
const ScopedLock sl (listenerLock);
listeners.addIfNotAlreadyThere (listenerToAdd);
if (listeners.size() == 1)
triggerImageCapture();
}
void removeListener (CameraDevice::Listener* listenerToRemove)
{
const ScopedLock sl (listenerLock);
listeners.removeFirstMatchingValue (listenerToRemove);
}
static StringArray getAvailableDevices()
{
StringArray results;
@ -208,6 +212,15 @@ struct CameraDevice::Pimpl
return results;
}
void cameraSessionRuntimeError (const String& error)
{
JUCE_CAMERA_LOG ("cameraSessionRuntimeError(), error = " + error);
if (owner.onErrorOccurred != nullptr)
owner.onErrorOccurred (error);
}
CameraDevice& owner;
AVCaptureView* captureView = nil;
AVCaptureSession* session = nil;
AVCaptureMovieFileOutput* fileOutput = nil;
@ -218,8 +231,9 @@ struct CameraDevice::Pimpl
Time firstPresentationTime;
bool isRecording = false;
Array<CameraDevice::Listener*> listeners;
CriticalSection listenerLock;
std::function<void (const Image&)> pictureTakenCallback;
JUCE_DECLARE_WEAK_REFERENCEABLE (Pimpl)
private:
//==============================================================================
@ -235,17 +249,29 @@ private:
addMethod (@selector (captureOutput:didResumeRecordingToOutputFileAtURL: fromConnections:), didResumeRecordingToOutputFileAtURL, "v@:@@@");
addMethod (@selector (captureOutput:willFinishRecordingToOutputFileAtURL:fromConnections:error:), willFinishRecordingToOutputFileAtURL, "v@:@@@@");
SEL runtimeErrorSel = NSSelectorFromString (nsStringLiteral ("captureSessionRuntimeError:"));
addMethod (runtimeErrorSel, sessionRuntimeError, "v@:@");
registerClass();
}
static void setOwner (id self, Pimpl* owner) { object_setInstanceVariable (self, "owner", owner); }
static Pimpl* getOwner (id self) { return getIvar<Pimpl*> (self, "owner"); }
static Pimpl& getOwner (id self) { return *getIvar<Pimpl*> (self, "owner"); }
private:
static void didStartRecordingToOutputFileAtURL (id, SEL, AVCaptureFileOutput*, NSURL*, NSArray*) {}
static void didPauseRecordingToOutputFileAtURL (id, SEL, AVCaptureFileOutput*, NSURL*, NSArray*) {}
static void didResumeRecordingToOutputFileAtURL (id, SEL, AVCaptureFileOutput*, NSURL*, NSArray*) {}
static void willFinishRecordingToOutputFileAtURL (id, SEL, AVCaptureFileOutput*, NSURL*, NSArray*, NSError*) {}
static void sessionRuntimeError (id self, SEL, NSNotification* notification)
{
JUCE_CAMERA_LOG (nsStringToJuce ([notification description]));
NSError* error = notification.userInfo[AVCaptureSessionErrorKey];
auto errorString = error != nil ? nsStringToJuce (error.localizedDescription) : String();
getOwner (self).cameraSessionRuntimeError (errorString);
}
};
JUCE_DECLARE_NON_COPYABLE (Pimpl)

View file

@ -49,10 +49,11 @@ static const CLSID CLSID_NullRenderer = { 0xC1F400A4, 0x3F08, 0x11d3, { 0x9F, 0
struct CameraDevice::Pimpl : public ChangeBroadcaster
{
Pimpl (const String&, int index,
int minWidth, int minHeight,
int maxWidth, int maxHeight, bool /*highQuality*/)
: isRecording (false),
Pimpl (CameraDevice& ownerToUse, const String&, int index,
int minWidth, int minHeight, int maxWidth, int maxHeight,
bool /*highQuality*/)
: owner (ownerToUse),
isRecording (false),
openedSuccessfully (false),
imageNeedsFlipping (false),
width (0), height (0),
@ -191,6 +192,22 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster
bool openedOk() const noexcept { return openedSuccessfully; }
void takeStillPicture (std::function<void (const Image&)> pictureTakenCallbackToUse)
{
{
const ScopedLock sl (callbackLock);
jassert (pictureTakenCallbackToUse != nullptr);
if (pictureTakenCallbackToUse == nullptr)
return;
pictureTakenCallback = static_cast<std::function<void (const Image&)>&&> (pictureTakenCallbackToUse);
}
addUser();
}
void startRecordingToFile (const File& file, int quality)
{
addUser();
@ -212,32 +229,26 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster
return firstRecordedTime;
}
void addListener (CameraDevice::Listener* listenerToAdd)
void notifyImageReceivedIfNeeded (const Image& image)
{
const ScopedLock sl (listenerLock);
{
const ScopedLock sl (callbackLock);
if (listeners.size() == 0)
addUser();
if (pictureTakenCallback == nullptr)
return;
}
listeners.addIfNotAlreadyThere (listenerToAdd);
}
WeakReference<Pimpl> weakRef (this);
MessageManager::callAsync ([weakRef, image]() mutable
{
if (weakRef == nullptr)
return;
void removeListener (CameraDevice::Listener* listenerToRemove)
{
const ScopedLock sl (listenerLock);
listeners.removeAllInstancesOf (listenerToRemove);
if (weakRef->pictureTakenCallback != nullptr)
weakRef->pictureTakenCallback (image);
if (listeners.size() == 0)
removeUser();
}
void callListeners (const Image& image)
{
const ScopedLock sl (listenerLock);
for (int i = listeners.size(); --i >= 0;)
if (CameraDevice::Listener* const l = listeners[i])
l->imageReceived (image);
weakRef->pictureTakenCallback = nullptr;
});
}
void addUser()
@ -294,8 +305,7 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster
imageNeedsFlipping = true;
}
if (listeners.size() > 0)
callListeners (loadingImage);
notifyImageReceivedIfNeeded (loadingImage);
sendChangeMessage();
}
@ -520,9 +530,9 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster
JUCE_DECLARE_NON_COPYABLE (GrabberCallback)
};
CameraDevice& owner;
ComSmartPtr<GrabberCallback> callback;
Array<CameraDevice::Listener*> listeners;
CriticalSection listenerLock;
bool isRecording, openedSuccessfully;
int width, height;
@ -547,6 +557,11 @@ struct CameraDevice::Pimpl : public ChangeBroadcaster
bool recordNextFrameTime;
int previewMaxFPS;
CriticalSection callbackLock;
std::function<void (const Image&)> pictureTakenCallback;
JUCE_DECLARE_WEAK_REFERENCEABLE (Pimpl)
private:
void getVideoSizes (IAMStreamConfig* const streamConfig)
{

View file

@ -54,12 +54,12 @@ public:
//==============================================================================
/** Tries to load a video from a local file.
@returns am error if the file failed to be loaded correctly
@returns an error if the file failed to be loaded correctly
*/
Result load (const File& file);
/** Tries to load a video from a URL.
@returns am error if the file failed to be loaded correctly
@returns an error if the file failed to be loaded correctly
*/
Result load (const URL& url);
@ -75,7 +75,7 @@ public:
File getCurrentVideoFile() const;
/** Returns the last URL that was loaded.
If nothing is open, or if it was a file rather than a URL, this will return File().
If nothing is open, or if it was a file rather than a URL, this will return URL().
*/
URL getCurrentVideoURL() const;