fix macOS build (following Projucer changes made in Windows, which removed /Applications/JUCE/modules from its headers). move JUCE headers under source control, so that Windows and macOS can both build against same version of JUCE. remove AUv3 target (I think it's an iOS thing, so it will never work with this macOS fluidsynth dylib).
This commit is contained in:
249
modules/juce_video/capture/juce_CameraDevice.cpp
Normal file
249
modules/juce_video/capture/juce_CameraDevice.cpp
Normal file
@ -0,0 +1,249 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
#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 (*this, name, index, minWidth, minHeight, maxWidth, maxHeight, useHighQuality))
|
||||
{
|
||||
}
|
||||
|
||||
CameraDevice::~CameraDevice()
|
||||
{
|
||||
jassert (juce::MessageManager::getInstance()->currentThreadHasLockedMessageManager());
|
||||
|
||||
stopRecording();
|
||||
pimpl.reset();
|
||||
}
|
||||
|
||||
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();
|
||||
pimpl->startRecordingToFile (file, quality);
|
||||
}
|
||||
|
||||
Time CameraDevice::getTimeOfFirstRecordedFrame() const
|
||||
{
|
||||
return pimpl->getTimeOfFirstRecordedFrame();
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
return Pimpl::getAvailableDevices();
|
||||
}
|
||||
}
|
||||
|
||||
CameraDevice* CameraDevice::openDevice (int index,
|
||||
int minWidth, int minHeight,
|
||||
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
|
264
modules/juce_video/capture/juce_CameraDevice.h
Normal file
264
modules/juce_video/capture/juce_CameraDevice.h
Normal file
@ -0,0 +1,264 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
#if JUCE_USE_CAMERA || DOXYGEN
|
||||
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Controls any video capture devices that might be available.
|
||||
|
||||
Use getAvailableDevices() to list the devices that are attached to the
|
||||
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}
|
||||
*/
|
||||
class JUCE_API CameraDevice
|
||||
{
|
||||
public:
|
||||
/** Destructor. */
|
||||
virtual ~CameraDevice();
|
||||
|
||||
//==============================================================================
|
||||
/** Returns a list of the available cameras on this machine.
|
||||
|
||||
You can open one of these devices by calling openDevice() or openDeviceAsync().
|
||||
*/
|
||||
static StringArray getAvailableDevices();
|
||||
|
||||
/** 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 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
|
||||
which will allow the OS to drop frames if the computer cannot keep up in processing
|
||||
the frames.
|
||||
*/
|
||||
static CameraDevice* openDevice (int deviceIndex,
|
||||
int minWidth = 128, int minHeight = 64,
|
||||
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
|
||||
use for your filename.
|
||||
|
||||
If the file exists, it will be deleted before the recording starts.
|
||||
|
||||
This method may not start recording instantly, so if you need to know the
|
||||
exact time at which the file begins, you can call getTimeOfFirstRecordedFrame()
|
||||
after the recording has finished.
|
||||
|
||||
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);
|
||||
|
||||
/** Stops recording, after a call to startRecordingToFile(). */
|
||||
void stopRecording();
|
||||
|
||||
/** Returns the file extension that should be used for the files
|
||||
that you pass to startRecordingToFile().
|
||||
|
||||
This may be platform-specific, e.g. ".mov" or ".avi".
|
||||
*/
|
||||
static String getFileExtension();
|
||||
|
||||
/** After calling stopRecording(), this method can be called to return the timestamp
|
||||
of the first frame that was written to the file.
|
||||
*/
|
||||
Time getTimeOfFirstRecordedFrame() const;
|
||||
|
||||
/** 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;
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
Receives callbacks with individual frames from a CameraDevice. It is mainly
|
||||
useful for processing multiple frames that has to be done as quickly as
|
||||
possible. The callbacks can be called from any thread.
|
||||
|
||||
If you just need to take one picture, you should use takeStillPicture() instead.
|
||||
|
||||
@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!
|
||||
|
||||
Simply add a listener to be continuously notified about new frames becoming
|
||||
available and remove the listener when you no longer need new frames.
|
||||
|
||||
If you just need to take one picture, use takeStillPicture() instead.
|
||||
|
||||
@see CameraDevice::takeStillPicture
|
||||
*/
|
||||
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);
|
||||
|
||||
private:
|
||||
String name;
|
||||
|
||||
struct Pimpl;
|
||||
friend struct Pimpl;
|
||||
friend struct ContainerDeletePolicy<Pimpl>;
|
||||
std::unique_ptr<Pimpl> pimpl;
|
||||
|
||||
struct ViewerComponent;
|
||||
friend struct ViewerComponent;
|
||||
|
||||
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)
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce
|
74
modules/juce_video/juce_video.cpp
Normal file
74
modules/juce_video/juce_video.cpp
Normal file
@ -0,0 +1,74 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#ifdef JUCE_VIDEO_H_INCLUDED
|
||||
/* When you add this cpp file to your project, you mustn't include it in a file where you've
|
||||
already included any other headers - just put it inside a file on its own, possibly with your config
|
||||
flags preceding it, but don't include anything else. That also includes avoiding any automatic prefix
|
||||
header files that the compiler may be using.
|
||||
*/
|
||||
#error "Incorrect use of JUCE cpp file"
|
||||
#endif
|
||||
|
||||
#define JUCE_CORE_INCLUDE_OBJC_HELPERS 1
|
||||
#define JUCE_CORE_INCLUDE_JNI_HELPERS 1
|
||||
#define JUCE_CORE_INCLUDE_COM_SMART_PTR 1
|
||||
#define JUCE_CORE_INCLUDE_NATIVE_HEADERS 1
|
||||
|
||||
#include "juce_video.h"
|
||||
|
||||
#if JUCE_MAC || JUCE_IOS
|
||||
#import <AVFoundation/AVFoundation.h>
|
||||
#import <AVKit/AVKit.h>
|
||||
|
||||
//==============================================================================
|
||||
#elif JUCE_MSVC
|
||||
/* If you're using the camera classes, you'll need access to a few DirectShow headers.
|
||||
These files are provided in the normal Windows SDK. */
|
||||
#include <dshow.h>
|
||||
#include <dshowasf.h>
|
||||
#include <evr.h>
|
||||
|
||||
#if JUCE_USE_CAMERA && JUCE_MSVC && ! JUCE_DONT_AUTOLINK_TO_WIN32_LIBRARIES
|
||||
#pragma comment (lib, "Strmiids.lib")
|
||||
#pragma comment (lib, "wmvcore.lib")
|
||||
#endif
|
||||
|
||||
#if JUCE_MEDIAFOUNDATION && JUCE_MSVC && ! JUCE_DONT_AUTOLINK_TO_WIN32_LIBRARIES
|
||||
#pragma comment (lib, "mfuuid.lib")
|
||||
#endif
|
||||
|
||||
#if JUCE_MSVC && ! JUCE_DONT_AUTOLINK_TO_WIN32_LIBRARIES
|
||||
#pragma comment (lib, "strmiids.lib")
|
||||
#endif
|
||||
#endif
|
||||
|
||||
//==============================================================================
|
||||
#include "playback/juce_VideoComponent.cpp"
|
||||
|
||||
#if JUCE_USE_CAMERA
|
||||
#include "capture/juce_CameraDevice.cpp"
|
||||
#endif
|
84
modules/juce_video/juce_video.h
Normal file
84
modules/juce_video/juce_video.h
Normal file
@ -0,0 +1,84 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
The block below describes the properties of this module, and is read by
|
||||
the Projucer to automatically generate project code that uses it.
|
||||
For details about the syntax and how to create or use a module, see the
|
||||
JUCE Module Format.txt file.
|
||||
|
||||
|
||||
BEGIN_JUCE_MODULE_DECLARATION
|
||||
|
||||
ID: juce_video
|
||||
vendor: juce
|
||||
version: 5.3.2
|
||||
name: JUCE video playback and capture classes
|
||||
description: Classes for playing video and capturing camera input.
|
||||
website: http://www.juce.com/juce
|
||||
license: GPL/Commercial
|
||||
|
||||
dependencies: juce_gui_extra
|
||||
OSXFrameworks: AVKit AVFoundation CoreMedia
|
||||
iOSFrameworks: AVKit AVFoundation CoreMedia
|
||||
|
||||
END_JUCE_MODULE_DECLARATION
|
||||
|
||||
*******************************************************************************/
|
||||
|
||||
|
||||
#pragma once
|
||||
#define JUCE_VIDEO_H_INCLUDED
|
||||
|
||||
//==============================================================================
|
||||
#include <juce_gui_extra/juce_gui_extra.h>
|
||||
|
||||
//=============================================================================
|
||||
/** Config: JUCE_USE_CAMERA
|
||||
Enables camera support using the CameraDevice class (Mac, Windows, iOS, Android).
|
||||
*/
|
||||
#ifndef JUCE_USE_CAMERA
|
||||
#define JUCE_USE_CAMERA 0
|
||||
#endif
|
||||
|
||||
#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
|
||||
|
||||
//=============================================================================
|
||||
#include "playback/juce_VideoComponent.h"
|
||||
#include "capture/juce_CameraDevice.h"
|
27
modules/juce_video/juce_video.mm
Normal file
27
modules/juce_video/juce_video.mm
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#include "juce_video.cpp"
|
3409
modules/juce_video/native/juce_android_CameraDevice.h
Normal file
3409
modules/juce_video/native/juce_android_CameraDevice.h
Normal file
File diff suppressed because it is too large
Load Diff
1331
modules/juce_video/native/juce_ios_CameraDevice.h
Normal file
1331
modules/juce_video/native/juce_ios_CameraDevice.h
Normal file
File diff suppressed because it is too large
Load Diff
329
modules/juce_video/native/juce_mac_CameraDevice.h
Normal file
329
modules/juce_video/native/juce_mac_CameraDevice.h
Normal file
@ -0,0 +1,329 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
struct CameraDevice::Pimpl
|
||||
{
|
||||
Pimpl (CameraDevice& ownerToUse, const String&, int /*index*/, int /*minWidth*/, int /*minHeight*/,
|
||||
int /*maxWidth*/, int /*maxHeight*/, bool useHighQuality)
|
||||
: owner (ownerToUse)
|
||||
{
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
captureView = [[AVCaptureView alloc] init];
|
||||
session = captureView.session;
|
||||
|
||||
session.sessionPreset = useHighQuality ? AVCaptureSessionPresetHigh
|
||||
: AVCaptureSessionPresetMedium;
|
||||
|
||||
refreshConnections();
|
||||
|
||||
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();
|
||||
[session release];
|
||||
[callbackDelegate release];
|
||||
}
|
||||
|
||||
bool openedOk() const noexcept { return openingError.isEmpty(); }
|
||||
|
||||
void addImageCapture()
|
||||
{
|
||||
if (imageOutput == nil)
|
||||
{
|
||||
imageOutput = [[AVCaptureStillImageOutput alloc] init];
|
||||
auto* imageSettings = [[NSDictionary alloc] initWithObjectsAndKeys: AVVideoCodecJPEG, AVVideoCodecKey, nil];
|
||||
[imageOutput setOutputSettings: imageSettings];
|
||||
[imageSettings release];
|
||||
[session addOutput: imageOutput];
|
||||
}
|
||||
}
|
||||
|
||||
void addMovieCapture()
|
||||
{
|
||||
if (fileOutput == nil)
|
||||
{
|
||||
fileOutput = [[AVCaptureMovieFileOutput alloc] init];
|
||||
[session addOutput: fileOutput];
|
||||
}
|
||||
}
|
||||
|
||||
void removeImageCapture()
|
||||
{
|
||||
if (imageOutput != nil)
|
||||
{
|
||||
[session removeOutput: imageOutput];
|
||||
[imageOutput release];
|
||||
imageOutput = nil;
|
||||
}
|
||||
}
|
||||
|
||||
void removeMovieCapture()
|
||||
{
|
||||
if (fileOutput != nil)
|
||||
{
|
||||
[session removeOutput: fileOutput];
|
||||
[fileOutput release];
|
||||
fileOutput = nil;
|
||||
}
|
||||
}
|
||||
|
||||
void refreshConnections()
|
||||
{
|
||||
[session beginConfiguration];
|
||||
removeImageCapture();
|
||||
removeMovieCapture();
|
||||
addImageCapture();
|
||||
addMovieCapture();
|
||||
[session commitConfiguration];
|
||||
}
|
||||
|
||||
void refreshIfNeeded()
|
||||
{
|
||||
if (getVideoConnection() == nullptr)
|
||||
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();
|
||||
refreshIfNeeded();
|
||||
firstPresentationTime = Time::getCurrentTime();
|
||||
file.deleteFile();
|
||||
|
||||
[fileOutput startRecordingToOutputFileURL: createNSURLFromFile (file)
|
||||
recordingDelegate: callbackDelegate];
|
||||
}
|
||||
|
||||
void stopRecording()
|
||||
{
|
||||
if (isRecording)
|
||||
{
|
||||
[fileOutput stopRecording];
|
||||
isRecording = false;
|
||||
}
|
||||
}
|
||||
|
||||
Time getTimeOfFirstRecordedFrame() const
|
||||
{
|
||||
return firstPresentationTime;
|
||||
}
|
||||
|
||||
AVCaptureConnection* getVideoConnection() const
|
||||
{
|
||||
if (imageOutput != nil)
|
||||
for (AVCaptureConnection* connection in imageOutput.connections)
|
||||
if ([connection isActive] && [connection isEnabled])
|
||||
for (AVCaptureInputPort* port in [connection inputPorts])
|
||||
if ([[port mediaType] isEqual: AVMediaTypeVideo])
|
||||
return connection;
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
void handleImageCapture (const Image& image)
|
||||
{
|
||||
const ScopedLock sl (listenerLock);
|
||||
listeners.call ([=] (Listener& l) { l.imageReceived (image); });
|
||||
|
||||
if (! listeners.isEmpty())
|
||||
triggerImageCapture();
|
||||
}
|
||||
|
||||
void triggerImageCapture()
|
||||
{
|
||||
refreshIfNeeded();
|
||||
|
||||
if (auto* videoConnection = getVideoConnection())
|
||||
{
|
||||
[imageOutput captureStillImageAsynchronouslyFromConnection: videoConnection
|
||||
completionHandler: ^(CMSampleBufferRef sampleBuffer, NSError* error)
|
||||
{
|
||||
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);
|
||||
|
||||
handleImageCapture (image);
|
||||
|
||||
WeakReference<Pimpl> weakRef (this);
|
||||
MessageManager::callAsync ([weakRef, image]() mutable
|
||||
{
|
||||
if (weakRef != nullptr && weakRef->pictureTakenCallback != nullptr)
|
||||
weakRef->pictureTakenCallback (image);
|
||||
});
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
void addListener (CameraDevice::Listener* listenerToAdd)
|
||||
{
|
||||
const ScopedLock sl (listenerLock);
|
||||
listeners.add (listenerToAdd);
|
||||
|
||||
if (listeners.size() == 1)
|
||||
triggerImageCapture();
|
||||
}
|
||||
|
||||
void removeListener (CameraDevice::Listener* listenerToRemove)
|
||||
{
|
||||
const ScopedLock sl (listenerLock);
|
||||
listeners.remove (listenerToRemove);
|
||||
}
|
||||
|
||||
static StringArray getAvailableDevices()
|
||||
{
|
||||
StringArray results;
|
||||
results.add ("default");
|
||||
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;
|
||||
AVCaptureStillImageOutput* imageOutput = nil;
|
||||
|
||||
id<AVCaptureFileOutputRecordingDelegate> callbackDelegate = nil;
|
||||
String openingError;
|
||||
Time firstPresentationTime;
|
||||
bool isRecording = false;
|
||||
|
||||
CriticalSection listenerLock;
|
||||
ListenerList<Listener> listeners;
|
||||
|
||||
std::function<void (const Image&)> pictureTakenCallback;
|
||||
|
||||
JUCE_DECLARE_WEAK_REFERENCEABLE (Pimpl)
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
struct DelegateClass : public ObjCClass<NSObject>
|
||||
{
|
||||
DelegateClass() : ObjCClass<NSObject> ("JUCECameraDelegate_")
|
||||
{
|
||||
addIvar<Pimpl*> ("owner");
|
||||
addProtocol (@protocol (AVCaptureFileOutputRecordingDelegate));
|
||||
|
||||
addMethod (@selector (captureOutput:didStartRecordingToOutputFileAtURL: fromConnections:), didStartRecordingToOutputFileAtURL, "v@:@@@");
|
||||
addMethod (@selector (captureOutput:didPauseRecordingToOutputFileAtURL: fromConnections:), didPauseRecordingToOutputFileAtURL, "v@:@@@");
|
||||
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"); }
|
||||
|
||||
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)
|
||||
};
|
||||
|
||||
struct CameraDevice::ViewerComponent : public NSViewComponent
|
||||
{
|
||||
ViewerComponent (CameraDevice& d)
|
||||
{
|
||||
JUCE_AUTORELEASEPOOL
|
||||
{
|
||||
setSize (640, 480);
|
||||
setView (d.pimpl->captureView);
|
||||
}
|
||||
}
|
||||
|
||||
~ViewerComponent()
|
||||
{
|
||||
setView (nil);
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (ViewerComponent)
|
||||
};
|
||||
|
||||
String CameraDevice::getFileExtension()
|
||||
{
|
||||
return ".mov";
|
||||
}
|
199
modules/juce_video/native/juce_mac_Video.h
Normal file
199
modules/juce_video/native/juce_mac_Video.h
Normal file
@ -0,0 +1,199 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2015 - ROLI Ltd.
|
||||
|
||||
Permission is granted to use this software under the terms of either:
|
||||
a) the GPL v2 (or any later version)
|
||||
b) the Affero GPL v3
|
||||
|
||||
Details of these licenses can be found at: www.gnu.org/licenses
|
||||
|
||||
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
To release a closed-source product which uses JUCE, commercial licenses are
|
||||
available: visit www.juce.com for more information.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#if JUCE_IOS
|
||||
using BaseClass = UIViewComponent;
|
||||
#else
|
||||
using BaseClass = NSViewComponent;
|
||||
#endif
|
||||
|
||||
struct VideoComponent::Pimpl : public BaseClass
|
||||
{
|
||||
Pimpl()
|
||||
{
|
||||
setVisible (true);
|
||||
|
||||
#if JUCE_MAC && JUCE_32BIT
|
||||
auto view = [[NSView alloc] init]; // 32-bit builds don't have AVPlayerView, so need to use a layer
|
||||
controller = [[AVPlayerLayer alloc] init];
|
||||
setView (view);
|
||||
[view setNextResponder: [view superview]];
|
||||
[view setWantsLayer: YES];
|
||||
[view setLayer: controller];
|
||||
[view release];
|
||||
#elif JUCE_MAC
|
||||
controller = [[AVPlayerView alloc] init];
|
||||
setView (controller);
|
||||
[controller setNextResponder: [controller superview]];
|
||||
[controller setWantsLayer: YES];
|
||||
#else
|
||||
controller = [[AVPlayerViewController alloc] init];
|
||||
setView ([controller view]);
|
||||
#endif
|
||||
}
|
||||
|
||||
~Pimpl()
|
||||
{
|
||||
close();
|
||||
setView (nil);
|
||||
[controller release];
|
||||
}
|
||||
|
||||
Result load (const File& file)
|
||||
{
|
||||
auto r = load (createNSURLFromFile (file));
|
||||
|
||||
if (r.wasOk())
|
||||
currentFile = file;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
Result load (const URL& url)
|
||||
{
|
||||
auto r = load ([NSURL URLWithString: juceStringToNS (url.toString (true))]);
|
||||
|
||||
if (r.wasOk())
|
||||
currentURL = url;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
Result load (NSURL* url)
|
||||
{
|
||||
if (url != nil)
|
||||
{
|
||||
close();
|
||||
|
||||
if (auto* player = [AVPlayer playerWithURL: url])
|
||||
{
|
||||
[controller setPlayer: player];
|
||||
return Result::ok();
|
||||
}
|
||||
}
|
||||
|
||||
return Result::fail ("Couldn't open movie");
|
||||
}
|
||||
|
||||
void close()
|
||||
{
|
||||
stop();
|
||||
[controller setPlayer: nil];
|
||||
currentFile = File();
|
||||
currentURL = {};
|
||||
}
|
||||
|
||||
bool isOpen() const noexcept { return getAVPlayer() != nil; }
|
||||
bool isPlaying() const noexcept { return getSpeed() != 0; }
|
||||
|
||||
void play() noexcept { [getAVPlayer() play]; }
|
||||
void stop() noexcept { [getAVPlayer() pause]; }
|
||||
|
||||
void setPosition (double newPosition)
|
||||
{
|
||||
if (auto* p = getAVPlayer())
|
||||
{
|
||||
CMTime t = { (CMTimeValue) (100000.0 * newPosition),
|
||||
(CMTimeScale) 100000, kCMTimeFlags_Valid };
|
||||
|
||||
[p seekToTime: t
|
||||
toleranceBefore: kCMTimeZero
|
||||
toleranceAfter: kCMTimeZero];
|
||||
}
|
||||
}
|
||||
|
||||
double getPosition() const
|
||||
{
|
||||
if (auto* p = getAVPlayer())
|
||||
return toSeconds ([p currentTime]);
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
void setSpeed (double newSpeed)
|
||||
{
|
||||
[getAVPlayer() setRate: (float) newSpeed];
|
||||
}
|
||||
|
||||
double getSpeed() const
|
||||
{
|
||||
if (auto* p = getAVPlayer())
|
||||
return [p rate];
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
Rectangle<int> getNativeSize() const
|
||||
{
|
||||
if (auto* player = getAVPlayer())
|
||||
{
|
||||
auto s = [[player currentItem] presentationSize];
|
||||
return { (int) s.width, (int) s.height };
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
double getDuration() const
|
||||
{
|
||||
if (auto* player = getAVPlayer())
|
||||
return toSeconds ([[player currentItem] duration]);
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
void setVolume (float newVolume)
|
||||
{
|
||||
[getAVPlayer() setVolume: newVolume];
|
||||
}
|
||||
|
||||
float getVolume() const
|
||||
{
|
||||
if (auto* p = getAVPlayer())
|
||||
return [p volume];
|
||||
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
File currentFile;
|
||||
URL currentURL;
|
||||
|
||||
private:
|
||||
#if JUCE_IOS
|
||||
AVPlayerViewController* controller = nil;
|
||||
#elif JUCE_32BIT
|
||||
AVPlayerLayer* controller = nil;
|
||||
#else
|
||||
AVPlayerView* controller = nil;
|
||||
#endif
|
||||
|
||||
AVPlayer* getAVPlayer() const noexcept { return [controller player]; }
|
||||
|
||||
static double toSeconds (const CMTime& t) noexcept
|
||||
{
|
||||
return t.timescale != 0 ? (t.value / (double) t.timescale) : 0.0;
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
||||
};
|
838
modules/juce_video/native/juce_win32_CameraDevice.h
Normal file
838
modules/juce_video/native/juce_win32_CameraDevice.h
Normal file
@ -0,0 +1,838 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2017 - ROLI Ltd.
|
||||
|
||||
JUCE is an open source library subject to commercial or open-source
|
||||
licensing.
|
||||
|
||||
By using JUCE, you agree to the terms of both the JUCE 5 End-User License
|
||||
Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
|
||||
27th April 2017).
|
||||
|
||||
End User License Agreement: www.juce.com/juce-5-licence
|
||||
Privacy Policy: www.juce.com/juce-5-privacy-policy
|
||||
|
||||
Or: You may also use this code under the terms of the GPL v3 (see
|
||||
www.gnu.org/licenses).
|
||||
|
||||
JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
|
||||
EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
|
||||
DISCLAIMED.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
interface ISampleGrabberCB : public IUnknown
|
||||
{
|
||||
virtual STDMETHODIMP SampleCB (double, IMediaSample*) = 0;
|
||||
virtual STDMETHODIMP BufferCB (double, BYTE*, long) = 0;
|
||||
};
|
||||
|
||||
interface ISampleGrabber : public IUnknown
|
||||
{
|
||||
virtual HRESULT STDMETHODCALLTYPE SetOneShot (BOOL) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE SetMediaType (const AM_MEDIA_TYPE*) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetConnectedMediaType (AM_MEDIA_TYPE*) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE SetBufferSamples (BOOL) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetCurrentBuffer (long*, long*) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE GetCurrentSample (IMediaSample**) = 0;
|
||||
virtual HRESULT STDMETHODCALLTYPE SetCallback (ISampleGrabberCB*, long) = 0;
|
||||
};
|
||||
|
||||
static const IID IID_ISampleGrabberCB = { 0x0579154A, 0x2B53, 0x4994, { 0xB0, 0xD0, 0xE7, 0x73, 0x14, 0x8E, 0xFF, 0x85 } };
|
||||
static const IID IID_ISampleGrabber = { 0x6B652FFF, 0x11FE, 0x4fce, { 0x92, 0xAD, 0x02, 0x66, 0xB5, 0xD7, 0xC7, 0x8F } };
|
||||
static const CLSID CLSID_SampleGrabber = { 0xC1F400A0, 0x3F08, 0x11d3, { 0x9F, 0x0B, 0x00, 0x60, 0x08, 0x03, 0x9E, 0x37 } };
|
||||
static const CLSID CLSID_NullRenderer = { 0xC1F400A4, 0x3F08, 0x11d3, { 0x9F, 0x0B, 0x00, 0x60, 0x08, 0x03, 0x9E, 0x37 } };
|
||||
|
||||
|
||||
struct CameraDevice::Pimpl : public ChangeBroadcaster
|
||||
{
|
||||
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),
|
||||
activeUsers (0),
|
||||
recordNextFrameTime (false),
|
||||
previewMaxFPS (60)
|
||||
{
|
||||
HRESULT hr = captureGraphBuilder.CoCreateInstance (CLSID_CaptureGraphBuilder2);
|
||||
if (FAILED (hr))
|
||||
return;
|
||||
|
||||
filter = enumerateCameras (nullptr, index);
|
||||
if (filter == nullptr)
|
||||
return;
|
||||
|
||||
hr = graphBuilder.CoCreateInstance (CLSID_FilterGraph);
|
||||
if (FAILED (hr))
|
||||
return;
|
||||
|
||||
hr = captureGraphBuilder->SetFiltergraph (graphBuilder);
|
||||
if (FAILED (hr))
|
||||
return;
|
||||
|
||||
hr = graphBuilder.QueryInterface (mediaControl);
|
||||
if (FAILED (hr))
|
||||
return;
|
||||
|
||||
{
|
||||
ComSmartPtr<IAMStreamConfig> streamConfig;
|
||||
|
||||
hr = captureGraphBuilder->FindInterface (&PIN_CATEGORY_CAPTURE, 0, filter,
|
||||
IID_IAMStreamConfig, (void**) streamConfig.resetAndGetPointerAddress());
|
||||
|
||||
if (streamConfig != nullptr)
|
||||
{
|
||||
getVideoSizes (streamConfig);
|
||||
|
||||
if (! selectVideoSize (streamConfig, minWidth, minHeight, maxWidth, maxHeight))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
hr = graphBuilder->AddFilter (filter, _T("Video Capture"));
|
||||
if (FAILED (hr))
|
||||
return;
|
||||
|
||||
hr = smartTee.CoCreateInstance (CLSID_SmartTee);
|
||||
if (FAILED (hr))
|
||||
return;
|
||||
|
||||
hr = graphBuilder->AddFilter (smartTee, _T("Smart Tee"));
|
||||
if (FAILED (hr))
|
||||
return;
|
||||
|
||||
if (! connectFilters (filter, smartTee))
|
||||
return;
|
||||
|
||||
ComSmartPtr<IBaseFilter> sampleGrabberBase;
|
||||
hr = sampleGrabberBase.CoCreateInstance (CLSID_SampleGrabber);
|
||||
if (FAILED (hr))
|
||||
return;
|
||||
|
||||
hr = sampleGrabberBase.QueryInterface (IID_ISampleGrabber, sampleGrabber);
|
||||
if (FAILED (hr))
|
||||
return;
|
||||
|
||||
{
|
||||
AM_MEDIA_TYPE mt = { 0 };
|
||||
mt.majortype = MEDIATYPE_Video;
|
||||
mt.subtype = MEDIASUBTYPE_RGB24;
|
||||
mt.formattype = FORMAT_VideoInfo;
|
||||
sampleGrabber->SetMediaType (&mt);
|
||||
}
|
||||
|
||||
callback = new GrabberCallback (*this);
|
||||
hr = sampleGrabber->SetCallback (callback, 1);
|
||||
|
||||
hr = graphBuilder->AddFilter (sampleGrabberBase, _T("Sample Grabber"));
|
||||
if (FAILED (hr))
|
||||
return;
|
||||
|
||||
ComSmartPtr<IPin> grabberInputPin;
|
||||
if (! (getPin (smartTee, PINDIR_OUTPUT, smartTeeCaptureOutputPin, "capture")
|
||||
&& getPin (smartTee, PINDIR_OUTPUT, smartTeePreviewOutputPin, "preview")
|
||||
&& getPin (sampleGrabberBase, PINDIR_INPUT, grabberInputPin)))
|
||||
return;
|
||||
|
||||
hr = graphBuilder->Connect (smartTeePreviewOutputPin, grabberInputPin);
|
||||
if (FAILED (hr))
|
||||
return;
|
||||
|
||||
AM_MEDIA_TYPE mt = { 0 };
|
||||
hr = sampleGrabber->GetConnectedMediaType (&mt);
|
||||
VIDEOINFOHEADER* pVih = (VIDEOINFOHEADER*) (mt.pbFormat);
|
||||
width = pVih->bmiHeader.biWidth;
|
||||
height = pVih->bmiHeader.biHeight;
|
||||
|
||||
ComSmartPtr<IBaseFilter> nullFilter;
|
||||
hr = nullFilter.CoCreateInstance (CLSID_NullRenderer);
|
||||
hr = graphBuilder->AddFilter (nullFilter, _T("Null Renderer"));
|
||||
|
||||
if (connectFilters (sampleGrabberBase, nullFilter)
|
||||
&& addGraphToRot())
|
||||
{
|
||||
activeImage = Image (Image::RGB, width, height, true);
|
||||
loadingImage = Image (Image::RGB, width, height, true);
|
||||
|
||||
openedSuccessfully = true;
|
||||
}
|
||||
}
|
||||
|
||||
~Pimpl()
|
||||
{
|
||||
if (mediaControl != nullptr)
|
||||
mediaControl->Stop();
|
||||
|
||||
removeGraphFromRot();
|
||||
disconnectAnyViewers();
|
||||
|
||||
if (sampleGrabber != nullptr)
|
||||
{
|
||||
sampleGrabber->SetCallback (nullptr, 0);
|
||||
sampleGrabber = nullptr;
|
||||
}
|
||||
|
||||
callback = nullptr;
|
||||
graphBuilder = nullptr;
|
||||
mediaControl = nullptr;
|
||||
filter = nullptr;
|
||||
captureGraphBuilder = nullptr;
|
||||
smartTee = nullptr;
|
||||
smartTeePreviewOutputPin = nullptr;
|
||||
smartTeeCaptureOutputPin = nullptr;
|
||||
asfWriter = nullptr;
|
||||
}
|
||||
|
||||
bool openedOk() const noexcept { return openedSuccessfully; }
|
||||
|
||||
void takeStillPicture (std::function<void (const Image&)> pictureTakenCallbackToUse)
|
||||
{
|
||||
{
|
||||
const ScopedLock sl (pictureTakenCallbackLock);
|
||||
|
||||
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();
|
||||
isRecording = createFileCaptureFilter (file, quality);
|
||||
}
|
||||
|
||||
void stopRecording()
|
||||
{
|
||||
if (isRecording)
|
||||
{
|
||||
removeFileCaptureFilter();
|
||||
removeUser();
|
||||
isRecording = false;
|
||||
}
|
||||
}
|
||||
|
||||
Time getTimeOfFirstRecordedFrame() const
|
||||
{
|
||||
return firstRecordedTime;
|
||||
}
|
||||
|
||||
void addListener (CameraDevice::Listener* listenerToAdd)
|
||||
{
|
||||
const ScopedLock sl (listenerLock);
|
||||
|
||||
if (listeners.size() == 0)
|
||||
addUser();
|
||||
|
||||
listeners.add (listenerToAdd);
|
||||
}
|
||||
|
||||
void removeListener (CameraDevice::Listener* listenerToRemove)
|
||||
{
|
||||
const ScopedLock sl (listenerLock);
|
||||
listeners.remove (listenerToRemove);
|
||||
|
||||
if (listeners.size() == 0)
|
||||
removeUser();
|
||||
}
|
||||
|
||||
void callListeners (const Image& image)
|
||||
{
|
||||
const ScopedLock sl (listenerLock);
|
||||
listeners.call ([=] (Listener& l) { l.imageReceived (image); });
|
||||
}
|
||||
|
||||
void notifyPictureTakenIfNeeded (const Image& image)
|
||||
{
|
||||
{
|
||||
const ScopedLock sl (pictureTakenCallbackLock);
|
||||
|
||||
if (pictureTakenCallback == nullptr)
|
||||
return;
|
||||
}
|
||||
|
||||
WeakReference<Pimpl> weakRef (this);
|
||||
MessageManager::callAsync ([weakRef, image]() mutable
|
||||
{
|
||||
if (weakRef == nullptr)
|
||||
return;
|
||||
|
||||
if (weakRef->pictureTakenCallback != nullptr)
|
||||
weakRef->pictureTakenCallback (image);
|
||||
|
||||
weakRef->pictureTakenCallback = nullptr;
|
||||
});
|
||||
}
|
||||
|
||||
void addUser()
|
||||
{
|
||||
if (openedSuccessfully && activeUsers++ == 0)
|
||||
mediaControl->Run();
|
||||
}
|
||||
|
||||
void removeUser()
|
||||
{
|
||||
if (openedSuccessfully && --activeUsers == 0)
|
||||
mediaControl->Stop();
|
||||
}
|
||||
|
||||
void handleFrame (double /*time*/, BYTE* buffer, long /*bufferSize*/)
|
||||
{
|
||||
if (recordNextFrameTime)
|
||||
{
|
||||
const double defaultCameraLatency = 0.1;
|
||||
|
||||
firstRecordedTime = Time::getCurrentTime() - RelativeTime (defaultCameraLatency);
|
||||
recordNextFrameTime = false;
|
||||
|
||||
ComSmartPtr<IPin> pin;
|
||||
if (getPin (filter, PINDIR_OUTPUT, pin))
|
||||
{
|
||||
ComSmartPtr<IAMPushSource> pushSource;
|
||||
HRESULT hr = pin.QueryInterface (pushSource);
|
||||
|
||||
if (pushSource != nullptr)
|
||||
{
|
||||
REFERENCE_TIME latency = 0;
|
||||
hr = pushSource->GetLatency (&latency);
|
||||
|
||||
firstRecordedTime = firstRecordedTime - RelativeTime ((double) latency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
const int lineStride = width * 3;
|
||||
const ScopedLock sl (imageSwapLock);
|
||||
|
||||
{
|
||||
loadingImage.duplicateIfShared();
|
||||
const Image::BitmapData destData (loadingImage, 0, 0, width, height, Image::BitmapData::writeOnly);
|
||||
|
||||
for (int i = 0; i < height; ++i)
|
||||
memcpy (destData.getLinePointer ((height - 1) - i),
|
||||
buffer + lineStride * i,
|
||||
lineStride);
|
||||
}
|
||||
|
||||
imageNeedsFlipping = true;
|
||||
}
|
||||
|
||||
if (listeners.size() > 0)
|
||||
callListeners (loadingImage);
|
||||
|
||||
notifyPictureTakenIfNeeded (loadingImage);
|
||||
|
||||
sendChangeMessage();
|
||||
}
|
||||
|
||||
void drawCurrentImage (Graphics& g, Rectangle<int> area)
|
||||
{
|
||||
if (imageNeedsFlipping)
|
||||
{
|
||||
const ScopedLock sl (imageSwapLock);
|
||||
std::swap (loadingImage, activeImage);
|
||||
imageNeedsFlipping = false;
|
||||
}
|
||||
|
||||
Rectangle<int> centred (RectanglePlacement (RectanglePlacement::centred)
|
||||
.appliedTo (Rectangle<int> (width, height), area));
|
||||
|
||||
RectangleList<int> borders (area);
|
||||
borders.subtract (centred);
|
||||
g.setColour (Colours::black);
|
||||
g.fillRectList (borders);
|
||||
|
||||
g.drawImage (activeImage, centred.getX(), centred.getY(),
|
||||
centred.getWidth(), centred.getHeight(), 0, 0, width, height);
|
||||
}
|
||||
|
||||
bool createFileCaptureFilter (const File& file, int quality)
|
||||
{
|
||||
removeFileCaptureFilter();
|
||||
file.deleteFile();
|
||||
mediaControl->Stop();
|
||||
firstRecordedTime = Time();
|
||||
recordNextFrameTime = true;
|
||||
previewMaxFPS = 60;
|
||||
|
||||
HRESULT hr = asfWriter.CoCreateInstance (CLSID_WMAsfWriter);
|
||||
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
ComSmartPtr<IFileSinkFilter> fileSink;
|
||||
hr = asfWriter.QueryInterface (fileSink);
|
||||
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
hr = fileSink->SetFileName (file.getFullPathName().toWideCharPointer(), 0);
|
||||
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
hr = graphBuilder->AddFilter (asfWriter, _T("AsfWriter"));
|
||||
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
ComSmartPtr<IConfigAsfWriter> asfConfig;
|
||||
hr = asfWriter.QueryInterface (asfConfig);
|
||||
asfConfig->SetIndexMode (true);
|
||||
ComSmartPtr<IWMProfileManager> profileManager;
|
||||
hr = WMCreateProfileManager (profileManager.resetAndGetPointerAddress());
|
||||
|
||||
// This gibberish is the DirectShow profile for a video-only wmv file.
|
||||
String prof ("<profile version=\"589824\" storageformat=\"1\" name=\"Quality\" description=\"Quality type for output.\">"
|
||||
"<streamconfig majortype=\"{73646976-0000-0010-8000-00AA00389B71}\" streamnumber=\"1\" "
|
||||
"streamname=\"Video Stream\" inputname=\"Video409\" bitrate=\"894960\" "
|
||||
"bufferwindow=\"0\" reliabletransport=\"1\" decodercomplexity=\"AU\" rfc1766langid=\"en-us\">"
|
||||
"<videomediaprops maxkeyframespacing=\"50000000\" quality=\"90\"/>"
|
||||
"<wmmediatype subtype=\"{33564D57-0000-0010-8000-00AA00389B71}\" bfixedsizesamples=\"0\" "
|
||||
"btemporalcompression=\"1\" lsamplesize=\"0\">"
|
||||
"<videoinfoheader dwbitrate=\"894960\" dwbiterrorrate=\"0\" avgtimeperframe=\"$AVGTIMEPERFRAME\">"
|
||||
"<rcsource left=\"0\" top=\"0\" right=\"$WIDTH\" bottom=\"$HEIGHT\"/>"
|
||||
"<rctarget left=\"0\" top=\"0\" right=\"$WIDTH\" bottom=\"$HEIGHT\"/>"
|
||||
"<bitmapinfoheader biwidth=\"$WIDTH\" biheight=\"$HEIGHT\" biplanes=\"1\" bibitcount=\"24\" "
|
||||
"bicompression=\"WMV3\" bisizeimage=\"0\" bixpelspermeter=\"0\" biypelspermeter=\"0\" "
|
||||
"biclrused=\"0\" biclrimportant=\"0\"/>"
|
||||
"</videoinfoheader>"
|
||||
"</wmmediatype>"
|
||||
"</streamconfig>"
|
||||
"</profile>");
|
||||
|
||||
const int fps[] = { 10, 15, 30 };
|
||||
int maxFramesPerSecond = fps [jlimit (0, numElementsInArray (fps) - 1, quality & 0xff)];
|
||||
|
||||
if ((quality & 0xff000000) != 0) // (internal hacky way to pass explicit frame rates for testing)
|
||||
maxFramesPerSecond = (quality >> 24) & 0xff;
|
||||
|
||||
prof = prof.replace ("$WIDTH", String (width))
|
||||
.replace ("$HEIGHT", String (height))
|
||||
.replace ("$AVGTIMEPERFRAME", String (10000000 / maxFramesPerSecond));
|
||||
|
||||
ComSmartPtr<IWMProfile> currentProfile;
|
||||
hr = profileManager->LoadProfileByData (prof.toWideCharPointer(), currentProfile.resetAndGetPointerAddress());
|
||||
hr = asfConfig->ConfigureFilterUsingProfile (currentProfile);
|
||||
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
ComSmartPtr<IPin> asfWriterInputPin;
|
||||
|
||||
if (getPin (asfWriter, PINDIR_INPUT, asfWriterInputPin, "Video Input 01"))
|
||||
{
|
||||
hr = graphBuilder->Connect (smartTeeCaptureOutputPin, asfWriterInputPin);
|
||||
|
||||
if (SUCCEEDED (hr) && openedSuccessfully && activeUsers > 0
|
||||
&& SUCCEEDED (mediaControl->Run()))
|
||||
{
|
||||
previewMaxFPS = (quality < 2) ? 15 : 25; // throttle back the preview comps to try to leave the cpu free for encoding
|
||||
|
||||
if ((quality & 0x00ff0000) != 0) // (internal hacky way to pass explicit frame rates for testing)
|
||||
previewMaxFPS = (quality >> 16) & 0xff;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeFileCaptureFilter();
|
||||
|
||||
if (openedSuccessfully && activeUsers > 0)
|
||||
mediaControl->Run();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void removeFileCaptureFilter()
|
||||
{
|
||||
mediaControl->Stop();
|
||||
|
||||
if (asfWriter != nullptr)
|
||||
{
|
||||
graphBuilder->RemoveFilter (asfWriter);
|
||||
asfWriter = nullptr;
|
||||
}
|
||||
|
||||
if (openedSuccessfully && activeUsers > 0)
|
||||
mediaControl->Run();
|
||||
|
||||
previewMaxFPS = 60;
|
||||
}
|
||||
|
||||
static ComSmartPtr<IBaseFilter> enumerateCameras (StringArray* names, const int deviceIndexToOpen)
|
||||
{
|
||||
int index = 0;
|
||||
ComSmartPtr<ICreateDevEnum> pDevEnum;
|
||||
|
||||
if (SUCCEEDED (pDevEnum.CoCreateInstance (CLSID_SystemDeviceEnum)))
|
||||
{
|
||||
ComSmartPtr<IEnumMoniker> enumerator;
|
||||
HRESULT hr = pDevEnum->CreateClassEnumerator (CLSID_VideoInputDeviceCategory, enumerator.resetAndGetPointerAddress(), 0);
|
||||
|
||||
if (SUCCEEDED (hr) && enumerator != nullptr)
|
||||
{
|
||||
ComSmartPtr<IMoniker> moniker;
|
||||
ULONG fetched;
|
||||
|
||||
while (enumerator->Next (1, moniker.resetAndGetPointerAddress(), &fetched) == S_OK)
|
||||
{
|
||||
ComSmartPtr<IBaseFilter> captureFilter;
|
||||
hr = moniker->BindToObject (0, 0, IID_IBaseFilter, (void**) captureFilter.resetAndGetPointerAddress());
|
||||
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
ComSmartPtr<IPropertyBag> propertyBag;
|
||||
hr = moniker->BindToStorage (0, 0, IID_IPropertyBag, (void**) propertyBag.resetAndGetPointerAddress());
|
||||
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
VARIANT var;
|
||||
var.vt = VT_BSTR;
|
||||
|
||||
hr = propertyBag->Read (_T("FriendlyName"), &var, 0);
|
||||
propertyBag = nullptr;
|
||||
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
if (names != nullptr)
|
||||
names->add (var.bstrVal);
|
||||
|
||||
if (index == deviceIndexToOpen)
|
||||
return captureFilter;
|
||||
|
||||
++index;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static StringArray getAvailableDevices()
|
||||
{
|
||||
StringArray devs;
|
||||
enumerateCameras (&devs, -1);
|
||||
return devs;
|
||||
}
|
||||
|
||||
struct GrabberCallback : public ComBaseClassHelperBase<ISampleGrabberCB>
|
||||
{
|
||||
GrabberCallback (Pimpl& p)
|
||||
: ComBaseClassHelperBase<ISampleGrabberCB> (0), owner (p) {}
|
||||
|
||||
JUCE_COMRESULT QueryInterface (REFIID refId, void** result)
|
||||
{
|
||||
if (refId == IID_ISampleGrabberCB)
|
||||
return castToType<ISampleGrabberCB> (result);
|
||||
|
||||
return ComBaseClassHelperBase<ISampleGrabberCB>::QueryInterface (refId, result);
|
||||
}
|
||||
|
||||
STDMETHODIMP SampleCB (double, IMediaSample*) { return E_FAIL; }
|
||||
|
||||
STDMETHODIMP BufferCB (double time, BYTE* buffer, long bufferSize)
|
||||
{
|
||||
owner.handleFrame (time, buffer, bufferSize);
|
||||
return S_OK;
|
||||
}
|
||||
|
||||
Pimpl& owner;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (GrabberCallback)
|
||||
};
|
||||
|
||||
CameraDevice& owner;
|
||||
|
||||
ComSmartPtr<GrabberCallback> callback;
|
||||
|
||||
CriticalSection listenerLock;
|
||||
ListenerList<Listener> listeners;
|
||||
|
||||
CriticalSection pictureTakenCallbackLock;
|
||||
std::function<void (const Image&)> pictureTakenCallback;
|
||||
|
||||
bool isRecording, openedSuccessfully;
|
||||
int width, height;
|
||||
Time firstRecordedTime;
|
||||
|
||||
Array<ViewerComponent*> viewerComps;
|
||||
|
||||
ComSmartPtr<ICaptureGraphBuilder2> captureGraphBuilder;
|
||||
ComSmartPtr<IBaseFilter> filter, smartTee, asfWriter;
|
||||
ComSmartPtr<IGraphBuilder> graphBuilder;
|
||||
ComSmartPtr<ISampleGrabber> sampleGrabber;
|
||||
ComSmartPtr<IMediaControl> mediaControl;
|
||||
ComSmartPtr<IPin> smartTeePreviewOutputPin, smartTeeCaptureOutputPin;
|
||||
int activeUsers;
|
||||
Array<int> widths, heights;
|
||||
DWORD graphRegistrationID;
|
||||
|
||||
CriticalSection imageSwapLock;
|
||||
bool imageNeedsFlipping;
|
||||
Image loadingImage, activeImage;
|
||||
|
||||
bool recordNextFrameTime;
|
||||
int previewMaxFPS;
|
||||
|
||||
JUCE_DECLARE_WEAK_REFERENCEABLE (Pimpl)
|
||||
|
||||
private:
|
||||
void getVideoSizes (IAMStreamConfig* const streamConfig)
|
||||
{
|
||||
widths.clear();
|
||||
heights.clear();
|
||||
|
||||
int count = 0, size = 0;
|
||||
streamConfig->GetNumberOfCapabilities (&count, &size);
|
||||
|
||||
if (size == sizeof (VIDEO_STREAM_CONFIG_CAPS))
|
||||
{
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
VIDEO_STREAM_CONFIG_CAPS scc;
|
||||
AM_MEDIA_TYPE* config;
|
||||
|
||||
HRESULT hr = streamConfig->GetStreamCaps (i, &config, (BYTE*) &scc);
|
||||
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
const int w = scc.InputSize.cx;
|
||||
const int h = scc.InputSize.cy;
|
||||
|
||||
bool duplicate = false;
|
||||
|
||||
for (int j = widths.size(); --j >= 0;)
|
||||
{
|
||||
if (w == widths.getUnchecked (j) && h == heights.getUnchecked (j))
|
||||
{
|
||||
duplicate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! duplicate)
|
||||
{
|
||||
DBG ("Camera capture size: " + String (w) + ", " + String (h));
|
||||
widths.add (w);
|
||||
heights.add (h);
|
||||
}
|
||||
|
||||
deleteMediaType (config);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool selectVideoSize (IAMStreamConfig* const streamConfig,
|
||||
const int minWidth, const int minHeight,
|
||||
const int maxWidth, const int maxHeight)
|
||||
{
|
||||
int count = 0, size = 0, bestArea = 0, bestIndex = -1;
|
||||
streamConfig->GetNumberOfCapabilities (&count, &size);
|
||||
|
||||
if (size == sizeof (VIDEO_STREAM_CONFIG_CAPS))
|
||||
{
|
||||
AM_MEDIA_TYPE* config;
|
||||
VIDEO_STREAM_CONFIG_CAPS scc;
|
||||
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
HRESULT hr = streamConfig->GetStreamCaps (i, &config, (BYTE*) &scc);
|
||||
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
if (scc.InputSize.cx >= minWidth
|
||||
&& scc.InputSize.cy >= minHeight
|
||||
&& scc.InputSize.cx <= maxWidth
|
||||
&& scc.InputSize.cy <= maxHeight)
|
||||
{
|
||||
int area = scc.InputSize.cx * scc.InputSize.cy;
|
||||
if (area > bestArea)
|
||||
{
|
||||
bestIndex = i;
|
||||
bestArea = area;
|
||||
}
|
||||
}
|
||||
|
||||
deleteMediaType (config);
|
||||
}
|
||||
}
|
||||
|
||||
if (bestIndex >= 0)
|
||||
{
|
||||
HRESULT hr = streamConfig->GetStreamCaps (bestIndex, &config, (BYTE*) &scc);
|
||||
|
||||
hr = streamConfig->SetFormat (config);
|
||||
deleteMediaType (config);
|
||||
return SUCCEEDED (hr);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool getPin (IBaseFilter* filter, const PIN_DIRECTION wantedDirection,
|
||||
ComSmartPtr<IPin>& result, const char* pinName = nullptr)
|
||||
{
|
||||
ComSmartPtr<IEnumPins> enumerator;
|
||||
ComSmartPtr<IPin> pin;
|
||||
|
||||
filter->EnumPins (enumerator.resetAndGetPointerAddress());
|
||||
|
||||
while (enumerator->Next (1, pin.resetAndGetPointerAddress(), 0) == S_OK)
|
||||
{
|
||||
PIN_DIRECTION dir;
|
||||
pin->QueryDirection (&dir);
|
||||
|
||||
if (wantedDirection == dir)
|
||||
{
|
||||
PIN_INFO info = { 0 };
|
||||
pin->QueryPinInfo (&info);
|
||||
|
||||
if (pinName == nullptr || String (pinName).equalsIgnoreCase (String (info.achName)))
|
||||
{
|
||||
result = pin;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool connectFilters (IBaseFilter* const first, IBaseFilter* const second) const
|
||||
{
|
||||
ComSmartPtr<IPin> in, out;
|
||||
|
||||
return getPin (first, PINDIR_OUTPUT, out)
|
||||
&& getPin (second, PINDIR_INPUT, in)
|
||||
&& SUCCEEDED (graphBuilder->Connect (out, in));
|
||||
}
|
||||
|
||||
bool addGraphToRot()
|
||||
{
|
||||
ComSmartPtr<IRunningObjectTable> rot;
|
||||
if (FAILED (GetRunningObjectTable (0, rot.resetAndGetPointerAddress())))
|
||||
return false;
|
||||
|
||||
ComSmartPtr<IMoniker> moniker;
|
||||
WCHAR buffer[128];
|
||||
HRESULT hr = CreateItemMoniker (_T("!"), buffer, moniker.resetAndGetPointerAddress());
|
||||
if (FAILED (hr))
|
||||
return false;
|
||||
|
||||
graphRegistrationID = 0;
|
||||
return SUCCEEDED (rot->Register (0, graphBuilder, moniker, &graphRegistrationID));
|
||||
}
|
||||
|
||||
void removeGraphFromRot()
|
||||
{
|
||||
ComSmartPtr<IRunningObjectTable> rot;
|
||||
|
||||
if (SUCCEEDED (GetRunningObjectTable (0, rot.resetAndGetPointerAddress())))
|
||||
rot->Revoke (graphRegistrationID);
|
||||
}
|
||||
|
||||
void disconnectAnyViewers();
|
||||
|
||||
static void deleteMediaType (AM_MEDIA_TYPE* const pmt)
|
||||
{
|
||||
if (pmt->cbFormat != 0)
|
||||
CoTaskMemFree ((PVOID) pmt->pbFormat);
|
||||
|
||||
if (pmt->pUnk != nullptr)
|
||||
pmt->pUnk->Release();
|
||||
|
||||
CoTaskMemFree (pmt);
|
||||
}
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (Pimpl)
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct CameraDevice::ViewerComponent : public Component,
|
||||
public ChangeListener
|
||||
{
|
||||
ViewerComponent (CameraDevice& d)
|
||||
: owner (d.pimpl.get()), maxFPS (15), lastRepaintTime (0)
|
||||
{
|
||||
setOpaque (true);
|
||||
owner->addChangeListener (this);
|
||||
owner->addUser();
|
||||
owner->viewerComps.add (this);
|
||||
setSize (owner->width, owner->height);
|
||||
}
|
||||
|
||||
~ViewerComponent()
|
||||
{
|
||||
if (owner != nullptr)
|
||||
{
|
||||
owner->viewerComps.removeFirstMatchingValue (this);
|
||||
owner->removeUser();
|
||||
owner->removeChangeListener (this);
|
||||
}
|
||||
}
|
||||
|
||||
void ownerDeleted()
|
||||
{
|
||||
owner = nullptr;
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
g.setColour (Colours::black);
|
||||
g.setImageResamplingQuality (Graphics::lowResamplingQuality);
|
||||
|
||||
if (owner != nullptr)
|
||||
owner->drawCurrentImage (g, getLocalBounds());
|
||||
else
|
||||
g.fillAll();
|
||||
}
|
||||
|
||||
void changeListenerCallback (ChangeBroadcaster*) override
|
||||
{
|
||||
const int64 now = Time::currentTimeMillis();
|
||||
|
||||
if (now >= lastRepaintTime + (1000 / maxFPS))
|
||||
{
|
||||
lastRepaintTime = now;
|
||||
repaint();
|
||||
|
||||
if (owner != nullptr)
|
||||
maxFPS = owner->previewMaxFPS;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
Pimpl* owner;
|
||||
int maxFPS;
|
||||
int64 lastRepaintTime;
|
||||
};
|
||||
|
||||
void CameraDevice::Pimpl::disconnectAnyViewers()
|
||||
{
|
||||
for (int i = viewerComps.size(); --i >= 0;)
|
||||
viewerComps.getUnchecked(i)->ownerDeleted();
|
||||
}
|
||||
|
||||
String CameraDevice::getFileExtension()
|
||||
{
|
||||
return ".wmv";
|
||||
}
|
892
modules/juce_video/native/juce_win32_Video.h
Normal file
892
modules/juce_video/native/juce_win32_Video.h
Normal file
@ -0,0 +1,892 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2015 - ROLI Ltd.
|
||||
|
||||
Permission is granted to use this software under the terms of either:
|
||||
a) the GPL v2 (or any later version)
|
||||
b) the Affero GPL v3
|
||||
|
||||
Details of these licenses can be found at: www.gnu.org/licenses
|
||||
|
||||
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
To release a closed-source product which uses JUCE, commercial licenses are
|
||||
available: visit www.juce.com for more information.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace VideoRenderers
|
||||
{
|
||||
//======================================================================
|
||||
struct Base
|
||||
{
|
||||
virtual ~Base() {}
|
||||
|
||||
virtual HRESULT create (ComSmartPtr<IGraphBuilder>&, ComSmartPtr<IBaseFilter>&, HWND) = 0;
|
||||
virtual void setVideoWindow (HWND) = 0;
|
||||
virtual void setVideoPosition (HWND) = 0;
|
||||
virtual void repaintVideo (HWND, HDC) = 0;
|
||||
virtual void displayModeChanged() = 0;
|
||||
virtual HRESULT getVideoSize (long& videoWidth, long& videoHeight) = 0;
|
||||
};
|
||||
|
||||
//======================================================================
|
||||
struct VMR7 : public Base
|
||||
{
|
||||
VMR7() {}
|
||||
|
||||
HRESULT create (ComSmartPtr<IGraphBuilder>& graphBuilder,
|
||||
ComSmartPtr<IBaseFilter>& baseFilter, HWND hwnd) override
|
||||
{
|
||||
ComSmartPtr<IVMRFilterConfig> filterConfig;
|
||||
|
||||
HRESULT hr = baseFilter.CoCreateInstance (CLSID_VideoMixingRenderer);
|
||||
|
||||
if (SUCCEEDED (hr)) hr = graphBuilder->AddFilter (baseFilter, L"VMR-7");
|
||||
if (SUCCEEDED (hr)) hr = baseFilter.QueryInterface (filterConfig);
|
||||
if (SUCCEEDED (hr)) hr = filterConfig->SetRenderingMode (VMRMode_Windowless);
|
||||
if (SUCCEEDED (hr)) hr = baseFilter.QueryInterface (windowlessControl);
|
||||
if (SUCCEEDED (hr)) hr = windowlessControl->SetVideoClippingWindow (hwnd);
|
||||
if (SUCCEEDED (hr)) hr = windowlessControl->SetAspectRatioMode (VMR_ARMODE_LETTER_BOX);
|
||||
|
||||
return hr;
|
||||
}
|
||||
|
||||
void setVideoWindow (HWND hwnd) override
|
||||
{
|
||||
windowlessControl->SetVideoClippingWindow (hwnd);
|
||||
}
|
||||
|
||||
void setVideoPosition (HWND hwnd) override
|
||||
{
|
||||
long videoWidth = 0, videoHeight = 0;
|
||||
windowlessControl->GetNativeVideoSize (&videoWidth, &videoHeight, nullptr, nullptr);
|
||||
|
||||
RECT src, dest;
|
||||
SetRect (&src, 0, 0, videoWidth, videoHeight);
|
||||
GetClientRect (hwnd, &dest);
|
||||
|
||||
windowlessControl->SetVideoPosition (&src, &dest);
|
||||
}
|
||||
|
||||
void repaintVideo (HWND hwnd, HDC hdc) override
|
||||
{
|
||||
windowlessControl->RepaintVideo (hwnd, hdc);
|
||||
}
|
||||
|
||||
void displayModeChanged() override
|
||||
{
|
||||
windowlessControl->DisplayModeChanged();
|
||||
}
|
||||
|
||||
HRESULT getVideoSize (long& videoWidth, long& videoHeight) override
|
||||
{
|
||||
return windowlessControl->GetNativeVideoSize (&videoWidth, &videoHeight, nullptr, nullptr);
|
||||
}
|
||||
|
||||
ComSmartPtr<IVMRWindowlessControl> windowlessControl;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VMR7)
|
||||
};
|
||||
|
||||
|
||||
//======================================================================
|
||||
struct EVR : public Base
|
||||
{
|
||||
EVR() {}
|
||||
|
||||
HRESULT create (ComSmartPtr<IGraphBuilder>& graphBuilder,
|
||||
ComSmartPtr<IBaseFilter>& baseFilter, HWND hwnd) override
|
||||
{
|
||||
ComSmartPtr<IMFGetService> getService;
|
||||
|
||||
HRESULT hr = baseFilter.CoCreateInstance (CLSID_EnhancedVideoRenderer);
|
||||
|
||||
if (SUCCEEDED (hr)) hr = graphBuilder->AddFilter (baseFilter, L"EVR");
|
||||
if (SUCCEEDED (hr)) hr = baseFilter.QueryInterface (getService);
|
||||
if (SUCCEEDED (hr)) hr = getService->GetService (MR_VIDEO_RENDER_SERVICE, IID_IMFVideoDisplayControl,
|
||||
(void**) videoDisplayControl.resetAndGetPointerAddress());
|
||||
if (SUCCEEDED (hr)) hr = videoDisplayControl->SetVideoWindow (hwnd);
|
||||
if (SUCCEEDED (hr)) hr = videoDisplayControl->SetAspectRatioMode (MFVideoARMode_PreservePicture);
|
||||
|
||||
return hr;
|
||||
}
|
||||
|
||||
void setVideoWindow (HWND hwnd) override
|
||||
{
|
||||
videoDisplayControl->SetVideoWindow (hwnd);
|
||||
}
|
||||
|
||||
void setVideoPosition (HWND hwnd) override
|
||||
{
|
||||
const MFVideoNormalizedRect src = { 0.0f, 0.0f, 1.0f, 1.0f };
|
||||
|
||||
RECT dest;
|
||||
GetClientRect (hwnd, &dest);
|
||||
|
||||
videoDisplayControl->SetVideoPosition (&src, &dest);
|
||||
}
|
||||
|
||||
void repaintVideo (HWND, HDC) override
|
||||
{
|
||||
videoDisplayControl->RepaintVideo();
|
||||
}
|
||||
|
||||
void displayModeChanged() override {}
|
||||
|
||||
HRESULT getVideoSize (long& videoWidth, long& videoHeight) override
|
||||
{
|
||||
SIZE sz = { 0, 0 };
|
||||
HRESULT hr = videoDisplayControl->GetNativeVideoSize (&sz, nullptr);
|
||||
videoWidth = sz.cx;
|
||||
videoHeight = sz.cy;
|
||||
return hr;
|
||||
}
|
||||
|
||||
ComSmartPtr<IMFVideoDisplayControl> videoDisplayControl;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (EVR)
|
||||
};
|
||||
};
|
||||
|
||||
//==============================================================================
|
||||
struct VideoComponent::Pimpl : public Component
|
||||
{
|
||||
Pimpl() : videoLoaded (false)
|
||||
{
|
||||
setOpaque (true);
|
||||
context.reset (new DirectShowContext (*this));
|
||||
componentWatcher.reset (new ComponentWatcher (*this));
|
||||
}
|
||||
|
||||
~Pimpl()
|
||||
{
|
||||
close();
|
||||
context = nullptr;
|
||||
componentWatcher = nullptr;
|
||||
}
|
||||
|
||||
Result loadFromString (const String& fileOrURLPath)
|
||||
{
|
||||
close();
|
||||
auto r = context->loadFile (fileOrURLPath);
|
||||
|
||||
if (r.wasOk())
|
||||
{
|
||||
videoLoaded = true;
|
||||
context->updateVideoPosition();
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
Result load (const File& file)
|
||||
{
|
||||
auto r = loadFromString (file.getFullPathName());
|
||||
|
||||
if (r.wasOk())
|
||||
currentFile = file;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
Result load (const URL& url)
|
||||
{
|
||||
auto r = loadFromString (url.toString (true));
|
||||
|
||||
if (r.wasOk())
|
||||
currentURL = url;
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
void close()
|
||||
{
|
||||
stop();
|
||||
context->release();
|
||||
|
||||
videoLoaded = false;
|
||||
currentFile = File();
|
||||
currentURL = {};
|
||||
}
|
||||
|
||||
bool isOpen() const
|
||||
{
|
||||
return videoLoaded;
|
||||
}
|
||||
|
||||
bool isPlaying() const
|
||||
{
|
||||
return context->state == DirectShowContext::runningState;
|
||||
}
|
||||
|
||||
void play()
|
||||
{
|
||||
if (videoLoaded)
|
||||
context->play();
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
if (videoLoaded)
|
||||
context->pause();
|
||||
}
|
||||
|
||||
void setPosition (double newPosition)
|
||||
{
|
||||
if (videoLoaded)
|
||||
context->setPosition (newPosition);
|
||||
}
|
||||
|
||||
double getPosition() const
|
||||
{
|
||||
return videoLoaded ? context->getPosition() : 0.0;
|
||||
}
|
||||
|
||||
void setSpeed (double newSpeed)
|
||||
{
|
||||
if (videoLoaded)
|
||||
context->setSpeed (newSpeed);
|
||||
}
|
||||
|
||||
Rectangle<int> getNativeSize() const
|
||||
{
|
||||
return videoLoaded ? context->getVideoSize()
|
||||
: Rectangle<int>();
|
||||
}
|
||||
|
||||
double getDuration() const
|
||||
{
|
||||
return videoLoaded ? context->getDuration() : 0.0;
|
||||
}
|
||||
|
||||
void setVolume (float newVolume)
|
||||
{
|
||||
if (videoLoaded)
|
||||
context->setVolume (newVolume);
|
||||
}
|
||||
|
||||
float getVolume() const
|
||||
{
|
||||
return videoLoaded ? context->getVolume() : 0.0f;
|
||||
}
|
||||
|
||||
void paint (Graphics& g) override
|
||||
{
|
||||
if (videoLoaded)
|
||||
context->handleUpdateNowIfNeeded();
|
||||
else
|
||||
g.fillAll (Colours::grey);
|
||||
}
|
||||
|
||||
void updateContextPosition()
|
||||
{
|
||||
context->updateContextPosition();
|
||||
|
||||
if (getWidth() > 0 && getHeight() > 0)
|
||||
if (auto* peer = getTopLevelComponent()->getPeer())
|
||||
context->updateWindowPosition (peer->getAreaCoveredBy (*this));
|
||||
}
|
||||
|
||||
void updateContextVisibility()
|
||||
{
|
||||
context->showWindow (isShowing());
|
||||
}
|
||||
|
||||
void recreateNativeWindowAsync()
|
||||
{
|
||||
context->recreateNativeWindowAsync();
|
||||
repaint();
|
||||
}
|
||||
|
||||
File currentFile;
|
||||
URL currentURL;
|
||||
|
||||
private:
|
||||
bool videoLoaded;
|
||||
|
||||
//==============================================================================
|
||||
struct ComponentWatcher : public ComponentMovementWatcher
|
||||
{
|
||||
ComponentWatcher (Pimpl& c) : ComponentMovementWatcher (&c), owner (c)
|
||||
{
|
||||
}
|
||||
|
||||
void componentMovedOrResized (bool, bool) override
|
||||
{
|
||||
if (owner.videoLoaded)
|
||||
owner.updateContextPosition();
|
||||
}
|
||||
|
||||
void componentPeerChanged() override
|
||||
{
|
||||
if (owner.videoLoaded)
|
||||
owner.recreateNativeWindowAsync();
|
||||
}
|
||||
|
||||
void componentVisibilityChanged() override
|
||||
{
|
||||
if (owner.videoLoaded)
|
||||
owner.updateContextVisibility();
|
||||
}
|
||||
|
||||
Pimpl& owner;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ComponentWatcher)
|
||||
};
|
||||
|
||||
std::unique_ptr<ComponentWatcher> componentWatcher;
|
||||
|
||||
//======================================================================
|
||||
struct DirectShowContext : public AsyncUpdater
|
||||
{
|
||||
DirectShowContext (Pimpl& c) : component (c)
|
||||
{
|
||||
CoInitialize (0);
|
||||
}
|
||||
|
||||
~DirectShowContext()
|
||||
{
|
||||
release();
|
||||
CoUninitialize();
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
void updateWindowPosition (const Rectangle<int>& newBounds)
|
||||
{
|
||||
nativeWindow->setWindowPosition (newBounds);
|
||||
}
|
||||
|
||||
void showWindow (bool shouldBeVisible)
|
||||
{
|
||||
nativeWindow->showWindow (shouldBeVisible);
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
void repaint()
|
||||
{
|
||||
if (hasVideo)
|
||||
videoRenderer->repaintVideo (nativeWindow->hwnd, nativeWindow->hdc);
|
||||
}
|
||||
|
||||
void updateVideoPosition()
|
||||
{
|
||||
if (hasVideo)
|
||||
videoRenderer->setVideoPosition (nativeWindow->hwnd);
|
||||
}
|
||||
|
||||
void displayResolutionChanged()
|
||||
{
|
||||
if (hasVideo)
|
||||
videoRenderer->displayModeChanged();
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
void peerChanged()
|
||||
{
|
||||
deleteNativeWindow();
|
||||
|
||||
mediaEvent->SetNotifyWindow (0, 0, 0);
|
||||
if (videoRenderer != nullptr)
|
||||
videoRenderer->setVideoWindow (nullptr);
|
||||
|
||||
createNativeWindow();
|
||||
|
||||
mediaEvent->SetNotifyWindow ((OAHWND) hwnd, graphEventID, 0);
|
||||
if (videoRenderer != nullptr)
|
||||
videoRenderer->setVideoWindow (hwnd);
|
||||
}
|
||||
|
||||
void handleAsyncUpdate() override
|
||||
{
|
||||
if (hwnd != 0)
|
||||
{
|
||||
if (needToRecreateNativeWindow)
|
||||
{
|
||||
peerChanged();
|
||||
needToRecreateNativeWindow = false;
|
||||
}
|
||||
|
||||
if (needToUpdateViewport)
|
||||
{
|
||||
updateVideoPosition();
|
||||
needToUpdateViewport = false;
|
||||
}
|
||||
|
||||
repaint();
|
||||
}
|
||||
else
|
||||
{
|
||||
triggerAsyncUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
void recreateNativeWindowAsync()
|
||||
{
|
||||
needToRecreateNativeWindow = true;
|
||||
triggerAsyncUpdate();
|
||||
}
|
||||
|
||||
void updateContextPosition()
|
||||
{
|
||||
needToUpdateViewport = true;
|
||||
triggerAsyncUpdate();
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
Result loadFile (const String& fileOrURLPath)
|
||||
{
|
||||
jassert (state == uninitializedState);
|
||||
|
||||
if (! createNativeWindow())
|
||||
return Result::fail ("Can't create window");
|
||||
|
||||
HRESULT hr = graphBuilder.CoCreateInstance (CLSID_FilterGraph);
|
||||
|
||||
// basic playback interfaces
|
||||
if (SUCCEEDED (hr)) hr = graphBuilder.QueryInterface (mediaControl);
|
||||
if (SUCCEEDED (hr)) hr = graphBuilder.QueryInterface (mediaPosition);
|
||||
if (SUCCEEDED (hr)) hr = graphBuilder.QueryInterface (mediaEvent);
|
||||
if (SUCCEEDED (hr)) hr = graphBuilder.QueryInterface (basicAudio);
|
||||
|
||||
// video renderer interface
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
if (SystemStats::getOperatingSystemType() >= SystemStats::WinVista)
|
||||
{
|
||||
videoRenderer.reset (new VideoRenderers::EVR());
|
||||
hr = videoRenderer->create (graphBuilder, baseFilter, hwnd);
|
||||
|
||||
if (FAILED (hr))
|
||||
videoRenderer = nullptr;
|
||||
}
|
||||
|
||||
if (videoRenderer == nullptr)
|
||||
{
|
||||
videoRenderer.reset (new VideoRenderers::VMR7());
|
||||
hr = videoRenderer->create (graphBuilder, baseFilter, hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
// build filter graph
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
hr = graphBuilder->RenderFile (fileOrURLPath.toWideCharPointer(), nullptr);
|
||||
|
||||
if (FAILED (hr))
|
||||
{
|
||||
#if JUCE_MODAL_LOOPS_PERMITTED
|
||||
// Annoyingly, if we don't run the msg loop between failing and deleting the window, the
|
||||
// whole OS message-dispatch system gets itself into a state, and refuses to deliver any
|
||||
// more messages for the whole app. (That's what happens in Win7, anyway)
|
||||
MessageManager::getInstance()->runDispatchLoopUntil (200);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// remove video renderer if not connected (no video)
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
if (isRendererConnected())
|
||||
{
|
||||
hasVideo = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasVideo = false;
|
||||
graphBuilder->RemoveFilter (baseFilter);
|
||||
videoRenderer = nullptr;
|
||||
baseFilter = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// set window to receive events
|
||||
if (SUCCEEDED (hr))
|
||||
hr = mediaEvent->SetNotifyWindow ((OAHWND) hwnd, graphEventID, 0);
|
||||
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
state = stoppedState;
|
||||
pause();
|
||||
return Result::ok();
|
||||
}
|
||||
|
||||
// Note that if you're trying to open a file and this method fails, you may
|
||||
// just need to install a suitable codec. It seems that by default DirectShow
|
||||
// doesn't support a very good range of formats.
|
||||
release();
|
||||
return getErrorMessageFromResult (hr);
|
||||
}
|
||||
|
||||
static Result getErrorMessageFromResult (HRESULT hr)
|
||||
{
|
||||
switch (hr)
|
||||
{
|
||||
case VFW_E_INVALID_FILE_FORMAT: return Result::fail ("Invalid file format");
|
||||
case VFW_E_NOT_FOUND: return Result::fail ("File not found");
|
||||
case VFW_E_UNKNOWN_FILE_TYPE: return Result::fail ("Unknown file type");
|
||||
case VFW_E_UNSUPPORTED_STREAM: return Result::fail ("Unsupported stream");
|
||||
case VFW_E_CANNOT_CONNECT: return Result::fail ("Cannot connect");
|
||||
case VFW_E_CANNOT_LOAD_SOURCE_FILTER: return Result::fail ("Cannot load source filter");
|
||||
}
|
||||
|
||||
TCHAR messageBuffer[512] = { 0 };
|
||||
|
||||
FormatMessage (FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
nullptr, hr, MAKELANGID (LANG_NEUTRAL, SUBLANG_DEFAULT),
|
||||
messageBuffer, (DWORD) numElementsInArray (messageBuffer) - 1, nullptr);
|
||||
|
||||
return Result::fail (String (messageBuffer));
|
||||
}
|
||||
|
||||
void release()
|
||||
{
|
||||
if (mediaControl != nullptr)
|
||||
mediaControl->Stop();
|
||||
|
||||
if (mediaEvent != nullptr)
|
||||
mediaEvent->SetNotifyWindow (0, 0, 0);
|
||||
|
||||
if (videoRenderer != nullptr)
|
||||
videoRenderer->setVideoWindow (0);
|
||||
|
||||
hasVideo = false;
|
||||
videoRenderer = nullptr;
|
||||
baseFilter = nullptr;
|
||||
basicAudio = nullptr;
|
||||
mediaEvent = nullptr;
|
||||
mediaPosition = nullptr;
|
||||
mediaControl = nullptr;
|
||||
graphBuilder = nullptr;
|
||||
|
||||
state = uninitializedState;
|
||||
|
||||
if (nativeWindow != nullptr)
|
||||
deleteNativeWindow();
|
||||
}
|
||||
|
||||
void graphEventProc()
|
||||
{
|
||||
LONG ec = 0;
|
||||
LONG_PTR p1 = {}, p2 = {};
|
||||
|
||||
jassert (mediaEvent != nullptr);
|
||||
|
||||
while (SUCCEEDED (mediaEvent->GetEvent (&ec, &p1, &p2, 0)))
|
||||
{
|
||||
mediaEvent->FreeEventParams (ec, p1, p2);
|
||||
|
||||
switch (ec)
|
||||
{
|
||||
case EC_REPAINT:
|
||||
component.repaint();
|
||||
break;
|
||||
|
||||
case EC_COMPLETE:
|
||||
component.stop();
|
||||
break;
|
||||
|
||||
case EC_USERABORT:
|
||||
case EC_ERRORABORT:
|
||||
case EC_ERRORABORTEX:
|
||||
component.close();
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
void play()
|
||||
{
|
||||
mediaControl->Run();
|
||||
state = runningState;
|
||||
}
|
||||
|
||||
void stop()
|
||||
{
|
||||
mediaControl->Stop();
|
||||
state = stoppedState;
|
||||
}
|
||||
|
||||
void pause()
|
||||
{
|
||||
mediaControl->Pause();
|
||||
state = pausedState;
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
Rectangle<int> getVideoSize() const noexcept
|
||||
{
|
||||
long width = 0, height = 0;
|
||||
|
||||
if (hasVideo)
|
||||
videoRenderer->getVideoSize (width, height);
|
||||
|
||||
return { (int) width, (int) height };
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
double getDuration() const
|
||||
{
|
||||
REFTIME duration;
|
||||
mediaPosition->get_Duration (&duration);
|
||||
return duration;
|
||||
}
|
||||
|
||||
double getPosition() const
|
||||
{
|
||||
REFTIME seconds;
|
||||
mediaPosition->get_CurrentPosition (&seconds);
|
||||
return seconds;
|
||||
}
|
||||
|
||||
void setSpeed (double newSpeed) { mediaPosition->put_Rate (newSpeed); }
|
||||
void setPosition (double seconds) { mediaPosition->put_CurrentPosition (seconds); }
|
||||
void setVolume (float newVolume) { basicAudio->put_Volume (convertToDShowVolume (newVolume)); }
|
||||
|
||||
// in DirectShow, full volume is 0, silence is -10000
|
||||
static long convertToDShowVolume (float vol) noexcept
|
||||
{
|
||||
if (vol >= 1.0f) return 0;
|
||||
if (vol <= 0.0f) return -10000;
|
||||
|
||||
return roundToInt ((vol * 10000.0f) - 10000.0f);
|
||||
}
|
||||
|
||||
float getVolume() const
|
||||
{
|
||||
long volume;
|
||||
basicAudio->get_Volume (&volume);
|
||||
return (volume + 10000) / 10000.0f;
|
||||
}
|
||||
|
||||
enum State { uninitializedState, runningState, pausedState, stoppedState };
|
||||
State state = uninitializedState;
|
||||
|
||||
private:
|
||||
//======================================================================
|
||||
enum { graphEventID = WM_APP + 0x43f0 };
|
||||
|
||||
Pimpl& component;
|
||||
HWND hwnd = {};
|
||||
HDC hdc = {};
|
||||
|
||||
ComSmartPtr<IGraphBuilder> graphBuilder;
|
||||
ComSmartPtr<IMediaControl> mediaControl;
|
||||
ComSmartPtr<IMediaPosition> mediaPosition;
|
||||
ComSmartPtr<IMediaEventEx> mediaEvent;
|
||||
ComSmartPtr<IBasicAudio> basicAudio;
|
||||
ComSmartPtr<IBaseFilter> baseFilter;
|
||||
|
||||
std::unique_ptr<VideoRenderers::Base> videoRenderer;
|
||||
|
||||
bool hasVideo = false, needToUpdateViewport = true, needToRecreateNativeWindow = false;
|
||||
|
||||
//======================================================================
|
||||
bool createNativeWindow()
|
||||
{
|
||||
jassert (nativeWindow == nullptr);
|
||||
|
||||
if (auto* topLevelPeer = component.getTopLevelComponent()->getPeer())
|
||||
{
|
||||
nativeWindow.reset (new NativeWindow ((HWND) topLevelPeer->getNativeHandle(), this));
|
||||
|
||||
hwnd = nativeWindow->hwnd;
|
||||
|
||||
if (hwnd != 0)
|
||||
{
|
||||
hdc = GetDC (hwnd);
|
||||
component.updateContextPosition();
|
||||
component.updateContextVisibility();
|
||||
return true;
|
||||
}
|
||||
|
||||
nativeWindow = nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
jassertfalse;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void deleteNativeWindow()
|
||||
{
|
||||
jassert (nativeWindow != nullptr);
|
||||
ReleaseDC (hwnd, hdc);
|
||||
hwnd = {};
|
||||
hdc = {};
|
||||
nativeWindow = nullptr;
|
||||
}
|
||||
|
||||
bool isRendererConnected()
|
||||
{
|
||||
ComSmartPtr<IEnumPins> enumPins;
|
||||
|
||||
HRESULT hr = baseFilter->EnumPins (enumPins.resetAndGetPointerAddress());
|
||||
|
||||
if (SUCCEEDED (hr))
|
||||
hr = enumPins->Reset();
|
||||
|
||||
ComSmartPtr<IPin> pin;
|
||||
|
||||
while (SUCCEEDED (hr)
|
||||
&& enumPins->Next (1, pin.resetAndGetPointerAddress(), nullptr) == S_OK)
|
||||
{
|
||||
ComSmartPtr<IPin> otherPin;
|
||||
|
||||
hr = pin->ConnectedTo (otherPin.resetAndGetPointerAddress());
|
||||
|
||||
if (SUCCEEDED (hr))
|
||||
{
|
||||
PIN_DIRECTION direction;
|
||||
hr = pin->QueryDirection (&direction);
|
||||
|
||||
if (SUCCEEDED (hr) && direction == PINDIR_INPUT)
|
||||
return true;
|
||||
}
|
||||
else if (hr == VFW_E_NOT_CONNECTED)
|
||||
{
|
||||
hr = S_OK;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
struct NativeWindowClass : private DeletedAtShutdown
|
||||
{
|
||||
bool isRegistered() const noexcept { return atom != 0; }
|
||||
LPCTSTR getWindowClassName() const noexcept { return (LPCTSTR) (pointer_sized_uint) MAKELONG (atom, 0); }
|
||||
|
||||
JUCE_DECLARE_SINGLETON_SINGLETHREADED_MINIMAL (NativeWindowClass)
|
||||
|
||||
private:
|
||||
NativeWindowClass()
|
||||
{
|
||||
String windowClassName ("JUCE_DIRECTSHOW_");
|
||||
windowClassName << (int) (Time::currentTimeMillis() & 0x7fffffff);
|
||||
|
||||
HINSTANCE moduleHandle = (HINSTANCE) Process::getCurrentModuleInstanceHandle();
|
||||
|
||||
TCHAR moduleFile [1024] = { 0 };
|
||||
GetModuleFileName (moduleHandle, moduleFile, 1024);
|
||||
|
||||
WNDCLASSEX wcex = { 0 };
|
||||
wcex.cbSize = sizeof (wcex);
|
||||
wcex.style = CS_OWNDC;
|
||||
wcex.lpfnWndProc = (WNDPROC) wndProc;
|
||||
wcex.lpszClassName = windowClassName.toWideCharPointer();
|
||||
wcex.hInstance = moduleHandle;
|
||||
|
||||
atom = RegisterClassEx (&wcex);
|
||||
jassert (atom != 0);
|
||||
}
|
||||
|
||||
~NativeWindowClass()
|
||||
{
|
||||
if (atom != 0)
|
||||
UnregisterClass (getWindowClassName(), (HINSTANCE) Process::getCurrentModuleInstanceHandle());
|
||||
|
||||
clearSingletonInstance();
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK wndProc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (auto* c = (DirectShowContext*) GetWindowLongPtr (hwnd, GWLP_USERDATA))
|
||||
{
|
||||
switch (msg)
|
||||
{
|
||||
case WM_NCHITTEST: return HTTRANSPARENT;
|
||||
case WM_ERASEBKGND: return 1;
|
||||
case WM_DISPLAYCHANGE: c->displayResolutionChanged(); break;
|
||||
case graphEventID: c->graphEventProc(); return 0;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
return DefWindowProc (hwnd, msg, wParam, lParam);
|
||||
}
|
||||
|
||||
ATOM atom = {};
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE (NativeWindowClass)
|
||||
};
|
||||
|
||||
//======================================================================
|
||||
struct NativeWindow
|
||||
{
|
||||
NativeWindow (HWND parentToAddTo, void* userData)
|
||||
{
|
||||
auto* wc = NativeWindowClass::getInstance();
|
||||
|
||||
if (wc->isRegistered())
|
||||
{
|
||||
DWORD exstyle = 0;
|
||||
DWORD type = WS_CHILD;
|
||||
|
||||
hwnd = CreateWindowEx (exstyle, wc->getWindowClassName(),
|
||||
L"", type, 0, 0, 0, 0, parentToAddTo, 0,
|
||||
(HINSTANCE) Process::getCurrentModuleInstanceHandle(), 0);
|
||||
|
||||
if (hwnd != 0)
|
||||
{
|
||||
hdc = GetDC (hwnd);
|
||||
SetWindowLongPtr (hwnd, GWLP_USERDATA, (LONG_PTR) userData);
|
||||
}
|
||||
}
|
||||
|
||||
jassert (hwnd != 0);
|
||||
}
|
||||
|
||||
~NativeWindow()
|
||||
{
|
||||
if (hwnd != 0)
|
||||
{
|
||||
SetWindowLongPtr (hwnd, GWLP_USERDATA, (LONG_PTR) 0);
|
||||
DestroyWindow (hwnd);
|
||||
}
|
||||
}
|
||||
|
||||
void setWindowPosition (Rectangle<int> newBounds)
|
||||
{
|
||||
SetWindowPos (hwnd, 0, newBounds.getX(), newBounds.getY(),
|
||||
newBounds.getWidth(), newBounds.getHeight(),
|
||||
SWP_NOACTIVATE | SWP_NOZORDER | SWP_NOOWNERZORDER);
|
||||
}
|
||||
|
||||
void showWindow (bool shouldBeVisible)
|
||||
{
|
||||
ShowWindow (hwnd, shouldBeVisible ? SW_SHOWNA : SW_HIDE);
|
||||
}
|
||||
|
||||
HWND hwnd = {};
|
||||
HDC hdc = {};
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (NativeWindow)
|
||||
};
|
||||
|
||||
std::unique_ptr<NativeWindow> nativeWindow;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DirectShowContext)
|
||||
};
|
||||
|
||||
std::unique_ptr<DirectShowContext> context;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl)
|
||||
};
|
||||
|
||||
JUCE_IMPLEMENT_SINGLETON (VideoComponent::Pimpl::DirectShowContext::NativeWindowClass)
|
123
modules/juce_video/playback/juce_VideoComponent.cpp
Normal file
123
modules/juce_video/playback/juce_VideoComponent.cpp
Normal file
@ -0,0 +1,123 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2015 - ROLI Ltd.
|
||||
|
||||
Permission is granted to use this software under the terms of either:
|
||||
a) the GPL v2 (or any later version)
|
||||
b) the Affero GPL v3
|
||||
|
||||
Details of these licenses can be found at: www.gnu.org/licenses
|
||||
|
||||
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
To release a closed-source product which uses JUCE, commercial licenses are
|
||||
available: visit www.juce.com for more information.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
#if JUCE_MAC || JUCE_IOS || JUCE_MSVC
|
||||
|
||||
#if JUCE_MAC || JUCE_IOS
|
||||
#include "../native/juce_mac_Video.h"
|
||||
#elif JUCE_WINDOWS
|
||||
#include "../native/juce_win32_Video.h"
|
||||
#endif
|
||||
|
||||
//==============================================================================
|
||||
VideoComponent::VideoComponent() : pimpl (new Pimpl())
|
||||
{
|
||||
addAndMakeVisible (pimpl.get());
|
||||
}
|
||||
|
||||
VideoComponent::~VideoComponent()
|
||||
{
|
||||
pimpl.reset();
|
||||
}
|
||||
|
||||
Result VideoComponent::load (const File& file)
|
||||
{
|
||||
auto r = pimpl->load (file);
|
||||
resized();
|
||||
return r;
|
||||
}
|
||||
|
||||
Result VideoComponent::load (const URL& url)
|
||||
{
|
||||
auto r = pimpl->load (url);
|
||||
resized();
|
||||
return r;
|
||||
}
|
||||
|
||||
void VideoComponent::closeVideo()
|
||||
{
|
||||
pimpl->close();
|
||||
resized();
|
||||
}
|
||||
|
||||
bool VideoComponent::isVideoOpen() const { return pimpl->isOpen(); }
|
||||
|
||||
File VideoComponent::getCurrentVideoFile() const { return pimpl->currentFile; }
|
||||
URL VideoComponent::getCurrentVideoURL() const { return pimpl->currentURL; }
|
||||
|
||||
double VideoComponent::getVideoDuration() const { return pimpl->getDuration(); }
|
||||
Rectangle<int> VideoComponent::getVideoNativeSize() const { return pimpl->getNativeSize(); }
|
||||
|
||||
void VideoComponent::play() { pimpl->play(); }
|
||||
void VideoComponent::stop() { pimpl->stop(); }
|
||||
|
||||
bool VideoComponent::isPlaying() const { return pimpl->isPlaying(); }
|
||||
|
||||
void VideoComponent::setPlayPosition (double newPos) { pimpl->setPosition (newPos); }
|
||||
double VideoComponent::getPlayPosition() const { return pimpl->getPosition(); }
|
||||
|
||||
void VideoComponent::setPlaySpeed (double newSpeed) { pimpl->setSpeed (newSpeed); }
|
||||
void VideoComponent::setAudioVolume (float newVolume) { pimpl->setVolume (newVolume); }
|
||||
float VideoComponent::getAudioVolume() const { return pimpl->getVolume(); }
|
||||
|
||||
void VideoComponent::resized()
|
||||
{
|
||||
auto r = getLocalBounds();
|
||||
|
||||
if (isVideoOpen() && ! r.isEmpty())
|
||||
{
|
||||
auto nativeSize = getVideoNativeSize();
|
||||
|
||||
if (nativeSize.isEmpty())
|
||||
{
|
||||
// if we've just opened the file and are still waiting for it to
|
||||
// figure out the size, start our timer..
|
||||
if (! isTimerRunning())
|
||||
startTimer (50);
|
||||
}
|
||||
else
|
||||
{
|
||||
r = RectanglePlacement (RectanglePlacement::centred).appliedTo (nativeSize, r);
|
||||
stopTimer();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
stopTimer();
|
||||
}
|
||||
|
||||
pimpl->setBounds (r);
|
||||
}
|
||||
|
||||
void VideoComponent::timerCallback()
|
||||
{
|
||||
resized();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce
|
138
modules/juce_video/playback/juce_VideoComponent.h
Normal file
138
modules/juce_video/playback/juce_VideoComponent.h
Normal file
@ -0,0 +1,138 @@
|
||||
/*
|
||||
==============================================================================
|
||||
|
||||
This file is part of the JUCE library.
|
||||
Copyright (c) 2015 - ROLI Ltd.
|
||||
|
||||
Permission is granted to use this software under the terms of either:
|
||||
a) the GPL v2 (or any later version)
|
||||
b) the Affero GPL v3
|
||||
|
||||
Details of these licenses can be found at: www.gnu.org/licenses
|
||||
|
||||
JUCE is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
|
||||
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
To release a closed-source product which uses JUCE, commercial licenses are
|
||||
available: visit www.juce.com for more information.
|
||||
|
||||
==============================================================================
|
||||
*/
|
||||
|
||||
#ifndef JUCE_VIDEOCOMPONENT_H_INCLUDED
|
||||
#define JUCE_VIDEOCOMPONENT_H_INCLUDED
|
||||
|
||||
namespace juce
|
||||
{
|
||||
|
||||
//==============================================================================
|
||||
/**
|
||||
A component that can play a movie.
|
||||
|
||||
Use the load() method to open a video once you've added this component to
|
||||
a parent (or put it on the desktop).
|
||||
|
||||
@tags{Video}
|
||||
*/
|
||||
class JUCE_API VideoComponent : public Component,
|
||||
private Timer
|
||||
{
|
||||
public:
|
||||
//==============================================================================
|
||||
/** Creates an empty VideoComponent.
|
||||
|
||||
Use the load() method to open a video once you've added this component to
|
||||
a parent (or put it on the desktop).
|
||||
*/
|
||||
VideoComponent();
|
||||
|
||||
/** Destructor. */
|
||||
~VideoComponent();
|
||||
|
||||
//==============================================================================
|
||||
/** Tries to load a video from a local file.
|
||||
@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 an error if the file failed to be loaded correctly
|
||||
*/
|
||||
Result load (const URL& url);
|
||||
|
||||
/** Closes the video and resets the component. */
|
||||
void closeVideo();
|
||||
|
||||
/** Returns true if a video is currently open. */
|
||||
bool isVideoOpen() const;
|
||||
|
||||
/** Returns the last file that was loaded.
|
||||
If nothing is open, or if it was a URL rather than a file, this will return File().
|
||||
*/
|
||||
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 URL().
|
||||
*/
|
||||
URL getCurrentVideoURL() const;
|
||||
|
||||
//==============================================================================
|
||||
/** Returns the length of the video, in seconds. */
|
||||
double getVideoDuration() const;
|
||||
|
||||
/** Returns the video's natural size, in pixels.
|
||||
If no video is loaded, an empty rectangle will be returned.
|
||||
*/
|
||||
Rectangle<int> getVideoNativeSize() const;
|
||||
|
||||
/** Starts the video playing. */
|
||||
void play();
|
||||
|
||||
/** Stops the video playing. */
|
||||
void stop();
|
||||
|
||||
/** Returns true if the video is currently playing. */
|
||||
bool isPlaying() const;
|
||||
|
||||
/** Sets the video's position to a given time. */
|
||||
void setPlayPosition (double newPositionSeconds);
|
||||
|
||||
/** Returns the current play position of the video. */
|
||||
double getPlayPosition() const;
|
||||
|
||||
/** Changes the video playback rate.
|
||||
A value of 1.0 is normal speed, greater values will play faster, smaller
|
||||
values play more slowly.
|
||||
*/
|
||||
void setPlaySpeed (double newSpeed);
|
||||
|
||||
/** Changes the video's playback volume.
|
||||
@param newVolume the volume in the range 0 (silent) to 1.0 (full)
|
||||
*/
|
||||
void setAudioVolume (float newVolume);
|
||||
|
||||
/** Returns the video's playback volume.
|
||||
@returns the volume in the range 0 (silent) to 1.0 (full)
|
||||
*/
|
||||
float getAudioVolume() const;
|
||||
|
||||
private:
|
||||
//==============================================================================
|
||||
struct Pimpl;
|
||||
friend struct Pimpl;
|
||||
friend struct ContainerDeletePolicy<Pimpl>;
|
||||
std::unique_ptr<Pimpl> pimpl;
|
||||
|
||||
void resized() override;
|
||||
void timerCallback() override;
|
||||
|
||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (VideoComponent)
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace juce
|
Reference in New Issue
Block a user