diff options
Diffstat (limited to 'src/android')
13 files changed, 1128 insertions, 493 deletions
diff --git a/src/android/CMakeLists.txt b/src/android/CMakeLists.txt index d437e8f0a..7fb5207ba 100644 --- a/src/android/CMakeLists.txt +++ b/src/android/CMakeLists.txt @@ -1,3 +1,6 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + # Generated from android.pro. add_subdirectory(jar) diff --git a/src/android/jar/CMakeLists.txt b/src/android/jar/CMakeLists.txt index ed51a1e20..01e0e5a08 100644 --- a/src/android/jar/CMakeLists.txt +++ b/src/android/jar/CMakeLists.txt @@ -1,9 +1,15 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + # Generated from jar.pro. set(java_sources src/org/qtproject/qt/android/multimedia/QtAndroidMediaPlayer.java src/org/qtproject/qt/android/multimedia/QtCameraListener.java + src/org/qtproject/qt/android/multimedia/QtCamera2.java + src/org/qtproject/qt/android/multimedia/QtExifDataHandler.java src/org/qtproject/qt/android/multimedia/QtAudioDeviceManager.java + src/org/qtproject/qt/android/multimedia/QtVideoDeviceManager.java src/org/qtproject/qt/android/multimedia/QtMediaRecorderListener.java src/org/qtproject/qt/android/multimedia/QtMultimediaUtils.java src/org/qtproject/qt/android/multimedia/QtSurfaceHolderCallback.java @@ -17,8 +23,10 @@ qt_internal_add_jar(Qt${QtMultimedia_VERSION_MAJOR}AndroidMultimedia OUTPUT_DIR "${QT_BUILD_DIR}/jar" ) +qt_path_join(destination ${INSTALL_DATADIR} "jar") + install_jar(Qt${QtMultimedia_VERSION_MAJOR}AndroidMultimedia - DESTINATION jar + DESTINATION ${destination} COMPONENT Devel ) diff --git a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtAndroidMediaPlayer.java b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtAndroidMediaPlayer.java index ce5dd5008..9bfad0aa4 100644 --- a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtAndroidMediaPlayer.java +++ b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtAndroidMediaPlayer.java @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtMultimedia of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only package org.qtproject.qt.android.multimedia; @@ -48,6 +12,7 @@ import java.io.FileInputStream; import android.content.Context; import android.media.MediaPlayer; import android.media.MediaFormat; +import android.media.PlaybackParams; import android.media.AudioAttributes; import android.media.TimedText; import android.net.Uri; @@ -62,19 +27,19 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -public class QtAndroidMediaPlayer +class QtAndroidMediaPlayer { // Native callback functions for MediaPlayer - native public void onErrorNative(int what, int extra, long id); - native public void onBufferingUpdateNative(int percent, long id); - native public void onProgressUpdateNative(int progress, long id); - native public void onDurationChangedNative(int duration, long id); - native public void onInfoNative(int what, int extra, long id); - native public void onVideoSizeChangedNative(int width, int height, long id); - native public void onStateChangedNative(int state, long id); + native void onErrorNative(int what, int extra, long id); + native void onBufferingUpdateNative(int percent, long id); + native void onProgressUpdateNative(int progress, long id); + native void onDurationChangedNative(int duration, long id); + native void onInfoNative(int what, int extra, long id); + native void onVideoSizeChangedNative(int width, int height, long id); + native void onStateChangedNative(int state, long id); - native public void onTrackInfoChangedNative(long id); - native public void onTimedTextChangedNative(String text, int time, long id); + native void onTrackInfoChangedNative(long id); + native void onTimedTextChangedNative(String text, int time, long id); private MediaPlayer mMediaPlayer = null; private AudioAttributes mAudioAttributes = null; @@ -102,7 +67,7 @@ public class QtAndroidMediaPlayer final static int Error = 0x200; } - public class TrackInfo + class TrackInfo { private int type; private String mime, language; @@ -241,19 +206,20 @@ public class QtAndroidMediaPlayer private class MediaPlayerTimedTextListener implements MediaPlayer.OnTimedTextListener { - @Override public void onTimedText(MediaPlayer mp, TimedText text) + @Override + public void onTimedText(MediaPlayer mp, TimedText text) { onTimedTextChangedNative(text.getText(), mp.getCurrentPosition(), mID); } } - public QtAndroidMediaPlayer(final Context context, final long id) + QtAndroidMediaPlayer(final Context context, final long id) { mID = id; mContext = context; } - public MediaPlayer getMediaPlayerHandle() + MediaPlayer getMediaPlayerHandle() { return mMediaPlayer; } @@ -291,7 +257,7 @@ public class QtAndroidMediaPlayer mProgressScheduler = Executors.newScheduledThreadPool(1); } - public void startProgressWatcher() + void startProgressWatcher() { // if it was shutdown, request new thread if (mProgressScheduler.isTerminated() || mProgressScheduler == null) @@ -306,13 +272,13 @@ public class QtAndroidMediaPlayer }, 10, 100, TimeUnit.MILLISECONDS); } - public void cancelProgressWatcher() + void cancelProgressWatcher() { if (mProgressScheduler != null) mProgressScheduler.shutdown(); } - public void start() + void start() { if ((mState & (State.Prepared | State.Started @@ -330,7 +296,7 @@ public class QtAndroidMediaPlayer } } - public void pause() + void pause() { if ((mState & (State.Started | State.Paused | State.PlaybackCompleted)) == 0) return; @@ -344,7 +310,7 @@ public class QtAndroidMediaPlayer } - public void stop() + void stop() { if ((mState & (State.Prepared | State.Started @@ -364,7 +330,7 @@ public class QtAndroidMediaPlayer } - public void seekTo(final int msec) + void seekTo(final int msec) { if ((mState & (State.Prepared | State.Started @@ -386,7 +352,7 @@ public class QtAndroidMediaPlayer } } - public boolean isPlaying() + boolean isPlaying() { boolean playing = false; if ((mState & (State.Idle @@ -408,7 +374,7 @@ public class QtAndroidMediaPlayer return playing; } - public void prepareAsync() + void prepareAsync() { if ((mState & (State.Initialized | State.Stopped)) == 0) return; @@ -421,17 +387,17 @@ public class QtAndroidMediaPlayer } } - public void initHeaders() + void initHeaders() { mHeaders = new HashMap<String, String>(); } - public void setHeader(final String header, final String value) + void setHeader(final String header, final String value) { mHeaders.put(header, value); } - public void setDataSource(final String path) + void setDataSource(final String path) { if (mState == State.Uninitialized) init(); @@ -497,7 +463,7 @@ public class QtAndroidMediaPlayer return ((mState & preparedState) != 0); } - public TrackInfo[] getAllTrackInfo() + TrackInfo[] getAllTrackInfo() { if (!isMediaPlayerPrepared()) { Log.w(TAG, "Trying to get track info of a media player that is not prepared!"); @@ -546,7 +512,7 @@ public class QtAndroidMediaPlayer return mimeType; } - public void selectTrack(int index) + void selectTrack(int index) { if (!isMediaPlayerPrepared()) { Log.d(TAG, "Trying to select a track of a media player that is not prepared!"); @@ -560,7 +526,7 @@ public class QtAndroidMediaPlayer } } - public void deselectTrack(int index) + void deselectTrack(int index) { if (!isMediaPlayerPrepared()) { Log.d(TAG, "Trying to deselect track of a media player that is not prepared!"); @@ -575,7 +541,7 @@ public class QtAndroidMediaPlayer } } - public int getSelectedTrack(int type) + int getSelectedTrack(int type) { int InvalidTrack = -1; @@ -606,7 +572,7 @@ public class QtAndroidMediaPlayer return InvalidTrack; } - public int getCurrentPosition() + int getCurrentPosition() { int currentPosition = 0; if ((mState & (State.Idle @@ -629,7 +595,7 @@ public class QtAndroidMediaPlayer } - public int getDuration() + int getDuration() { int duration = 0; if ((mState & (State.Prepared @@ -649,7 +615,7 @@ public class QtAndroidMediaPlayer return duration; } - public void setVolume(int volume) + void setVolume(int volume) { if (volume < 0) volume = 0; @@ -683,12 +649,12 @@ public class QtAndroidMediaPlayer } } - public SurfaceHolder display() + SurfaceHolder display() { return mSurfaceHolder; } - public void setDisplay(SurfaceHolder sh) + void setDisplay(SurfaceHolder sh) { mSurfaceHolder = sh; @@ -699,23 +665,23 @@ public class QtAndroidMediaPlayer } - public int getVolume() + int getVolume() { return mVolume; } - public void mute(final boolean mute) + void mute(final boolean mute) { mMuted = mute; setVolumeHelper(mute ? 0 : mVolume); } - public boolean isMuted() + boolean isMuted() { return mMuted; } - public void reset() + void reset() { if (mState == State.Uninitialized) { return; @@ -726,7 +692,7 @@ public class QtAndroidMediaPlayer cancelProgressWatcher(); } - public void release() + void release() { if (mMediaPlayer != null) { mMediaPlayer.reset(); @@ -738,7 +704,7 @@ public class QtAndroidMediaPlayer cancelProgressWatcher(); } - public void setAudioAttributes(int type, int usage) + void setAudioAttributes(int type, int usage) { mAudioAttributes = new AudioAttributes.Builder() .setUsage(usage) @@ -759,4 +725,28 @@ public class QtAndroidMediaPlayer Log.w(TAG, exception); } } + + boolean setPlaybackRate(float rate) + { + PlaybackParams playbackParams = mMediaPlayer.getPlaybackParams(); + playbackParams.setSpeed(rate); + // According to discussion under the patch from QTBUG-61115: At least with DirectShow + // and GStreamer, it changes both speed and pitch. (...) need to be consistent + if (rate != 0.0) + playbackParams.setPitch(Math.abs(rate)); + + try { + mMediaPlayer.setPlaybackParams(playbackParams); + } catch (IllegalStateException | IllegalArgumentException e) { + Log.e(TAG, "Cannot set playback rate " + rate + " :" + e.toString()); + return false; + } + + if ((mState & State.Started) == 0 && mMediaPlayer.isPlaying()) { + setState(State.Started); + startProgressWatcher(); + } + + return true; + } } diff --git a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtAudioDeviceManager.java b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtAudioDeviceManager.java index 245376480..c79e9da31 100644 --- a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtAudioDeviceManager.java +++ b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtAudioDeviceManager.java @@ -1,93 +1,74 @@ -/**************************************************************************** -** -** Copyright (C) 2021 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtMultimedia of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2021 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only package org.qtproject.qt.android.multimedia; import java.util.ArrayList; -import android.bluetooth.BluetoothA2dp; -import android.bluetooth.BluetoothAdapter; -import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothHeadset; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; +import android.media.AudioDeviceCallback; import android.media.AudioDeviceInfo; +import android.media.AudioFormat; import android.media.AudioManager; +import android.media.AudioRecord; +import android.media.AudioTrack; import android.media.MediaRecorder; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; -public class QtAudioDeviceManager +class QtAudioDeviceManager { + private static final String TAG = "QtAudioDeviceManager"; static private AudioManager m_audioManager = null; static private final AudioDevicesReceiver m_audioDevicesReceiver = new AudioDevicesReceiver(); + static private Handler handler = new Handler(Looper.getMainLooper()); + static private AudioRecord m_recorder = null; + static private AudioTrack m_streamPlayer = null; + static private Thread m_streamingThread = null; + static private boolean m_isStreaming = false; + static private boolean m_useSpeaker = false; + static private final int m_sampleRate = 8000; + static private final int m_channels = AudioFormat.CHANNEL_CONFIGURATION_MONO; + static private final int m_audioFormat = AudioFormat.ENCODING_PCM_16BIT; + static private final int m_bufferSize = AudioRecord.getMinBufferSize(m_sampleRate, m_channels, m_audioFormat); - public static native void onAudioInputDevicesUpdated(); - public static native void onAudioOutputDevicesUpdated(); + static native void onAudioInputDevicesUpdated(); + static native void onAudioOutputDevicesUpdated(); - static private class AudioDevicesReceiver extends BroadcastReceiver - { + static private void updateDeviceList() { + onAudioInputDevicesUpdated(); + onAudioOutputDevicesUpdated(); + if (m_useSpeaker) { + final AudioDeviceInfo[] audioDevices = + m_audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); + setAudioOutput(getModeForSpeaker(audioDevices), false, true); + } + } + + private static class AudioDevicesReceiver extends AudioDeviceCallback { @Override - public void onReceive(Context context, Intent intent) { - onAudioInputDevicesUpdated(); - onAudioOutputDevicesUpdated(); + public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) { + updateDeviceList(); } + + @Override + public void onAudioDevicesRemoved(AudioDeviceInfo[] removedDevices) { + updateDeviceList(); + } + } + + + static void registerAudioHeadsetStateReceiver() + { + m_audioManager.registerAudioDeviceCallback(m_audioDevicesReceiver, handler); } - public static void registerAudioHeadsetStateReceiver(Context context) + static void unregisterAudioHeadsetStateReceiver() { - IntentFilter audioDevicesFilter = new IntentFilter(); - audioDevicesFilter.addAction(AudioManager.ACTION_HEADSET_PLUG); - audioDevicesFilter.addAction(AudioManager.ACTION_HDMI_AUDIO_PLUG); - audioDevicesFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); - audioDevicesFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED); - audioDevicesFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECT_REQUESTED); - audioDevicesFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); - audioDevicesFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); - audioDevicesFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED); - audioDevicesFilter.addAction(AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED); - audioDevicesFilter.addAction(BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED); - audioDevicesFilter.addAction(BluetoothA2dp.ACTION_PLAYING_STATE_CHANGED); - - context.registerReceiver(m_audioDevicesReceiver, audioDevicesFilter); + m_audioManager.unregisterAudioDeviceCallback(m_audioDevicesReceiver); } - static public void setContext(Context context) + static void setContext(Context context) { m_audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE); } @@ -150,6 +131,7 @@ public class QtAudioDeviceManager private static String audioDeviceTypeToString(int type) { + // API <= 23 types switch (type) { case AudioDeviceInfo.TYPE_AUX_LINE: @@ -187,11 +169,16 @@ public class QtAudioDeviceManager return "Wired headphones"; case AudioDeviceInfo.TYPE_WIRED_HEADSET: return "Wired headset"; - case AudioDeviceInfo.TYPE_TELEPHONY: - case AudioDeviceInfo.TYPE_UNKNOWN: - default: - return "Unknown-Type"; } + + // API 24 + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { + if (type == AudioDeviceInfo.TYPE_BUS) + return "Bus"; + } + + return "Unknown-Type"; + } private static String[] getAudioDevices(int type) @@ -238,8 +225,27 @@ public class QtAudioDeviceManager return ret; } + private static int getModeForSpeaker(AudioDeviceInfo[] audioDevices) + { + // If we want to force device to use speaker when Bluetooth or Wiread headset is connected, + // we need to use MODE_IN_COMMUNICATION. Otherwise the MODE_NORMAL can be used. + for (AudioDeviceInfo deviceInfo : audioDevices) { + switch (deviceInfo.getType()) { + case AudioDeviceInfo.TYPE_BLUETOOTH_A2DP: + case AudioDeviceInfo.TYPE_BLUETOOTH_SCO: + case AudioDeviceInfo.TYPE_WIRED_HEADSET: + case AudioDeviceInfo.TYPE_WIRED_HEADPHONES: + return AudioManager.MODE_IN_COMMUNICATION; + default: break; + } + } + return AudioManager.MODE_NORMAL; + } + + private static boolean setAudioOutput(int id) { + m_useSpeaker = false; final AudioDeviceInfo[] audioDevices = m_audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); for (AudioDeviceInfo deviceInfo : audioDevices) { @@ -251,7 +257,8 @@ public class QtAudioDeviceManager setAudioOutput(AudioManager.MODE_IN_COMMUNICATION, true, false); return true; case AudioDeviceInfo.TYPE_BUILTIN_SPEAKER: - setAudioOutput(AudioManager.MODE_NORMAL, false, true); + m_useSpeaker = true; + setAudioOutput(getModeForSpeaker(audioDevices), false, true); return true; case AudioDeviceInfo.TYPE_WIRED_HEADSET: case AudioDeviceInfo.TYPE_WIRED_HEADPHONES: @@ -261,8 +268,15 @@ public class QtAudioDeviceManager // It doesn't work when WIRED HEADPHONES are connected // Earpiece has the lowest priority and setWiredHeadsetOn(boolean) // method to force it is deprecated + Log.w(TAG, "Built in Earpiece may not work when " + + "Wired Headphones are connected"); setAudioOutput(AudioManager.MODE_IN_CALL, false, false); return true; + case AudioDeviceInfo.TYPE_HDMI: + case AudioDeviceInfo.TYPE_HDMI_ARC: + case AudioDeviceInfo.TYPE_HDMI_EARC: + setAudioOutput(AudioManager.MODE_NORMAL, false, false); + return true; default: return false; } @@ -283,4 +297,66 @@ public class QtAudioDeviceManager m_audioManager.setSpeakerphoneOn(speakerOn); } + + private static void streamSound() + { + byte data[] = new byte[m_bufferSize]; + while (m_isStreaming) { + m_recorder.read(data, 0, m_bufferSize); + m_streamPlayer.play(); + m_streamPlayer.write(data, 0, m_bufferSize); + m_streamPlayer.stop(); + } + } + + private static void startSoundStreaming(int inputId, int outputId) + { + if (m_isStreaming) + stopSoundStreaming(); + + m_recorder = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, m_sampleRate, m_channels, + m_audioFormat, m_bufferSize); + m_streamPlayer = new AudioTrack(AudioManager.STREAM_MUSIC, m_sampleRate, m_channels, + m_audioFormat, m_bufferSize, AudioTrack.MODE_STREAM); + + final AudioDeviceInfo[] devices = m_audioManager.getDevices(AudioManager.GET_DEVICES_ALL); + for (AudioDeviceInfo deviceInfo : devices) { + if (deviceInfo.getId() == outputId) { + m_streamPlayer.setPreferredDevice(deviceInfo); + } else if (deviceInfo.getId() == inputId) { + m_recorder.setPreferredDevice(deviceInfo); + } + } + + m_recorder.startRecording(); + m_isStreaming = true; + + m_streamingThread = new Thread(new Runnable() { + @Override + public void run() { + streamSound(); + } + }); + + m_streamingThread.start(); + } + + private static void stopSoundStreaming() + { + if (!m_isStreaming) + return; + + m_isStreaming = false; + try { + m_streamingThread.join(); + m_streamingThread = null; + } catch (InterruptedException e) { + e.printStackTrace(); + } + m_recorder.stop(); + m_recorder.release(); + m_streamPlayer.release(); + m_streamPlayer = null; + m_recorder = null; + } } diff --git a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtCamera2.java b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtCamera2.java new file mode 100644 index 000000000..f74fc1e6e --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtCamera2.java @@ -0,0 +1,520 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +package org.qtproject.qt.android.multimedia; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Rect; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CaptureFailure; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.TotalCaptureResult; +import android.media.Image; +import android.media.ImageReader; +import android.os.Handler; +import android.os.HandlerThread; +import android.util.Log; +import android.util.Range; +import android.view.Surface; +import java.lang.Thread; +import java.util.ArrayList; +import java.util.List; + +@TargetApi(23) +class QtCamera2 { + + CameraDevice mCameraDevice = null; + QtVideoDeviceManager mVideoDeviceManager = null; + HandlerThread mBackgroundThread; + Handler mBackgroundHandler; + ImageReader mImageReader = null; + ImageReader mCapturedPhotoReader = null; + CameraManager mCameraManager; + CameraCaptureSession mCaptureSession; + CaptureRequest.Builder mPreviewRequestBuilder; + CaptureRequest mPreviewRequest; + String mCameraId; + List<Surface> mTargetSurfaces = new ArrayList<>(); + + private static final int STATE_PREVIEW = 0; + private static final int STATE_WAITING_LOCK = 1; + private static final int STATE_WAITING_PRECAPTURE = 2; + private static final int STATE_WAITING_NON_PRECAPTURE = 3; + private static final int STATE_PICTURE_TAKEN = 4; + + private int mState = STATE_PREVIEW; + private Object mStartMutex = new Object(); + private boolean mIsStarted = false; + private static int MaxNumberFrames = 12; + private int mFlashMode = CaptureRequest.CONTROL_AE_MODE_ON; + private int mTorchMode = CameraMetadata.FLASH_MODE_OFF; + private int mAFMode = CaptureRequest.CONTROL_AF_MODE_OFF; + private float mZoomFactor = 1.0f; + private Range<Integer> mFpsRange = null; + private QtExifDataHandler mExifDataHandler = null; + + native void onCameraOpened(String cameraId); + native void onCameraDisconnect(String cameraId); + native void onCameraError(String cameraId, int error); + CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() { + @Override + public void onOpened(CameraDevice cameraDevice) { + if (mCameraDevice != null) + mCameraDevice.close(); + mCameraDevice = cameraDevice; + onCameraOpened(mCameraId); + } + @Override + public void onDisconnected(CameraDevice cameraDevice) { + cameraDevice.close(); + if (mCameraDevice == cameraDevice) + mCameraDevice = null; + onCameraDisconnect(mCameraId); + } + @Override + public void onError(CameraDevice cameraDevice, int error) { + cameraDevice.close(); + if (mCameraDevice == cameraDevice) + mCameraDevice = null; + onCameraError(mCameraId, error); + } + }; + + native void onCaptureSessionConfigured(String cameraId); + native void onCaptureSessionConfigureFailed(String cameraId); + CameraCaptureSession.StateCallback mCaptureStateCallback = new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(CameraCaptureSession cameraCaptureSession) { + mCaptureSession = cameraCaptureSession; + onCaptureSessionConfigured(mCameraId); + } + + @Override + public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) { + onCaptureSessionConfigureFailed(mCameraId); + } + + @Override + public void onActive(CameraCaptureSession cameraCaptureSession) { + super.onActive(cameraCaptureSession); + onSessionActive(mCameraId); + } + + @Override + public void onClosed(CameraCaptureSession cameraCaptureSession) { + super.onClosed(cameraCaptureSession); + onSessionClosed(mCameraId); + } + }; + + native void onSessionActive(String cameraId); + native void onSessionClosed(String cameraId); + native void onCaptureSessionFailed(String cameraId, int reason, long frameNumber); + CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) { + super.onCaptureFailed(session, request, failure); + onCaptureSessionFailed(mCameraId, failure.getReason(), failure.getFrameNumber()); + } + + private void process(CaptureResult result) { + switch (mState) { + case STATE_WAITING_LOCK: { + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + if (afState == null) { + capturePhoto(); + } else if (CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED == afState || + CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED == afState) { + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + if (aeState == null || + aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + mState = STATE_PICTURE_TAKEN; + capturePhoto(); + } else { + try { + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + mState = STATE_WAITING_PRECAPTURE; + mCaptureSession.capture(mPreviewRequestBuilder.build(), + mCaptureCallback, + mBackgroundHandler); + } catch (CameraAccessException e) { + Log.w("QtCamera2", "Cannot get access to the camera: " + e); + } + } + } + break; + } + case STATE_WAITING_PRECAPTURE: { + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + mState = STATE_WAITING_NON_PRECAPTURE; + } + break; + } + case STATE_WAITING_NON_PRECAPTURE: { + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + mState = STATE_PICTURE_TAKEN; + capturePhoto(); + } + break; + } + default: + break; + } + } + + @Override + public void onCaptureProgressed(CameraCaptureSession s, CaptureRequest r, CaptureResult partialResult) { + process(partialResult); + } + + @Override + public void onCaptureCompleted(CameraCaptureSession s, CaptureRequest r, TotalCaptureResult result) { + process(result); + } + }; + + QtCamera2(Context context) { + mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); + mVideoDeviceManager = new QtVideoDeviceManager(context); + startBackgroundThread(); + } + + void startBackgroundThread() { + mBackgroundThread = new HandlerThread("CameraBackground"); + mBackgroundThread.start(); + mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); + } + + void stopBackgroundThread() { + mBackgroundThread.quitSafely(); + try { + mBackgroundThread.join(); + mBackgroundThread = null; + mBackgroundHandler = null; + } catch (Exception e) { + e.printStackTrace(); + } + } + + @SuppressLint("MissingPermission") + boolean open(String cameraId) { + try { + mCameraId = cameraId; + mCameraManager.openCamera(cameraId,mStateCallback,mBackgroundHandler); + return true; + } catch (Exception e){ + Log.w("QtCamera2", "Failed to open camera:" + e); + } + + return false; + } + + native void onPhotoAvailable(String cameraId, Image frame); + + ImageReader.OnImageAvailableListener mOnPhotoAvailableListener = new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(ImageReader reader) { + QtCamera2.this.onPhotoAvailable(mCameraId, reader.acquireLatestImage()); + } + }; + + native void onFrameAvailable(String cameraId, Image frame); + + ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() { + @Override + public void onImageAvailable(ImageReader reader) { + try { + Image img = reader.acquireLatestImage(); + if (img != null) + QtCamera2.this.onFrameAvailable(mCameraId, img); + } catch (IllegalStateException e) { + // It seems that ffmpeg is processing images for too long (and does not close it) + // Give it a little more time. Restarting the camera session if it doesn't help + Log.e("QtCamera2", "Image processing taking too long. Let's wait 0,5s more " + e); + try { + Thread.sleep(500); + QtCamera2.this.onFrameAvailable(mCameraId, reader.acquireLatestImage()); + } catch (IllegalStateException | InterruptedException e2) { + Log.e("QtCamera2", "Will not wait anymore. Restart camera session. " + e2); + // Remember current used camera ID, because stopAndClose will clear the value + String cameraId = mCameraId; + stopAndClose(); + addImageReader(mImageReader.getWidth(), mImageReader.getHeight(), + mImageReader.getImageFormat()); + open(cameraId); + } + } + } + }; + + + void prepareCamera(int width, int height, int format, int minFps, int maxFps) { + + addImageReader(width, height, format); + setFrameRate(minFps, maxFps); + } + + private void addImageReader(int width, int height, int format) { + + if (mImageReader != null) + removeSurface(mImageReader.getSurface()); + + if (mCapturedPhotoReader != null) + removeSurface(mCapturedPhotoReader.getSurface()); + + mImageReader = ImageReader.newInstance(width, height, format, MaxNumberFrames); + mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); + addSurface(mImageReader.getSurface()); + + mCapturedPhotoReader = ImageReader.newInstance(width, height, format, MaxNumberFrames); + mCapturedPhotoReader.setOnImageAvailableListener(mOnPhotoAvailableListener, mBackgroundHandler); + addSurface(mCapturedPhotoReader.getSurface()); + } + + private void setFrameRate(int minFrameRate, int maxFrameRate) { + + if (minFrameRate <= 0 || maxFrameRate <= 0) + mFpsRange = null; + else + mFpsRange = new Range<>(minFrameRate, maxFrameRate); + } + + boolean addSurface(Surface surface) { + if (mTargetSurfaces.contains(surface)) + return true; + + return mTargetSurfaces.add(surface); + } + + boolean removeSurface(Surface surface) { + return mTargetSurfaces.remove(surface); + } + + void clearSurfaces() { + mTargetSurfaces.clear(); + } + + boolean createSession() { + if (mCameraDevice == null) + return false; + + try { + mCameraDevice.createCaptureSession(mTargetSurfaces, mCaptureStateCallback, mBackgroundHandler); + return true; + } catch (Exception exception) { + Log.w("QtCamera2", "Failed to create a capture session:" + exception); + } + return false; + } + + boolean start(int template) { + + if (mCameraDevice == null) + return false; + + if (mCaptureSession == null) + return false; + + synchronized (mStartMutex) { + try { + mPreviewRequestBuilder = mCameraDevice.createCaptureRequest(template); + mPreviewRequestBuilder.addTarget(mImageReader.getSurface()); + mAFMode = CaptureRequest.CONTROL_AF_MODE_OFF; + for (int mode : mVideoDeviceManager.getSupportedAfModes(mCameraId)) { + if (mode == CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) { + mAFMode = mode; + break; + } + } + + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, mFlashMode); + mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE, mTorchMode); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, mAFMode); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT, CameraMetadata.CONTROL_CAPTURE_INTENT_VIDEO_RECORD); + if (mZoomFactor != 1.0f) + mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, getScalerCropRegion()); + if (mFpsRange != null) + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, mFpsRange); + mPreviewRequest = mPreviewRequestBuilder.build(); + mCaptureSession.setRepeatingRequest(mPreviewRequest, mCaptureCallback, mBackgroundHandler); + mIsStarted = true; + return true; + + } catch (Exception exception) { + Log.w("QtCamera2", "Failed to start preview:" + exception); + } + return false; + } + } + + void stopAndClose() { + synchronized (mStartMutex) { + try { + if (null != mCaptureSession) { + mCaptureSession.close(); + mCaptureSession = null; + } + if (null != mCameraDevice) { + mCameraDevice.close(); + mCameraDevice = null; + } + mCameraId = ""; + mTargetSurfaces.clear(); + } catch (Exception exception) { + Log.w("QtCamera2", "Failed to stop and close:" + exception); + } + mIsStarted = false; + } + } + + private void capturePhoto() { + try { + final CaptureRequest.Builder captureBuilder = + mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + captureBuilder.addTarget(mCapturedPhotoReader.getSurface()); + captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, mFlashMode); + if (mZoomFactor != 1.0f) + captureBuilder.set(CaptureRequest.SCALER_CROP_REGION, getScalerCropRegion()); + + CameraCaptureSession.CaptureCallback captureCallback + = new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, + TotalCaptureResult result) { + try { + mExifDataHandler = new QtExifDataHandler(result); + // Reset the focus/flash and go back to the normal state of preview. + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, + CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + mPreviewRequest = mPreviewRequestBuilder.build(); + mState = STATE_PREVIEW; + mCaptureSession.setRepeatingRequest(mPreviewRequest, + mCaptureCallback, + mBackgroundHandler); + } catch (CameraAccessException e) { + e.printStackTrace(); + } + } + }; + + mCaptureSession.capture(captureBuilder.build(), captureCallback, mBackgroundHandler); + } catch (CameraAccessException e) { + e.printStackTrace(); + } + } + + void takePhoto() { + try { + if (mAFMode == CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE) { + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START); + mState = STATE_WAITING_LOCK; + mCaptureSession.capture(mPreviewRequestBuilder.build(), mCaptureCallback, mBackgroundHandler); + } else { + capturePhoto(); + } + } catch (CameraAccessException e) { + Log.w("QtCamera2", "Cannot get access to the camera: " + e); + } + } + + void saveExifToFile(String path) + { + if (mExifDataHandler != null) + mExifDataHandler.save(path); + else + Log.e("QtCamera2", "No Exif data that could be saved to " + path); + } + + private Rect getScalerCropRegion() + { + Rect activePixels = mVideoDeviceManager.getActiveArraySize(mCameraId); + float zoomRatio = 1.0f; + if (mZoomFactor != 0.0f) + zoomRatio = 1.0f/mZoomFactor; + int croppedWidth = activePixels.width() - (int)(activePixels.width() * zoomRatio); + int croppedHeight = activePixels.height() - (int)(activePixels.height() * zoomRatio); + return new Rect(croppedWidth/2, croppedHeight/2, activePixels.width() - croppedWidth/2, + activePixels.height() - croppedHeight/2); + } + + void zoomTo(float factor) + { + synchronized (mStartMutex) { + mZoomFactor = factor; + + if (!mIsStarted) { + Log.w("QtCamera2", "Cannot set zoom on invalid camera"); + return; + } + + mPreviewRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, getScalerCropRegion()); + mPreviewRequest = mPreviewRequestBuilder.build(); + + try { + mCaptureSession.setRepeatingRequest(mPreviewRequest, mCaptureCallback, mBackgroundHandler); + } catch (Exception exception) { + Log.w("QtCamera2", "Failed to set zoom:" + exception); + } + } + } + void setFlashMode(String flashMode) + { + synchronized (mStartMutex) { + + int flashModeValue = mVideoDeviceManager.stringToControlAEMode(flashMode); + if (flashModeValue < 0) { + Log.w("QtCamera2", "Unknown flash mode"); + return; + } + mFlashMode = flashModeValue; + + if (!mIsStarted) + return; + + mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, mFlashMode); + mPreviewRequest = mPreviewRequestBuilder.build(); + + try { + mCaptureSession.setRepeatingRequest(mPreviewRequest, mCaptureCallback, mBackgroundHandler); + } catch (Exception exception) { + Log.w("QtCamera2", "Failed to set flash mode:" + exception); + } + } + } + + private int getTorchModeValue(boolean mode) + { + return mode ? CameraMetadata.FLASH_MODE_TORCH : CameraMetadata.FLASH_MODE_OFF; + } + + void setTorchMode(boolean torchMode) + { + synchronized (mStartMutex) { + mTorchMode = getTorchModeValue(torchMode); + + if (mIsStarted) { + mPreviewRequestBuilder.set(CaptureRequest.FLASH_MODE, mTorchMode); + mPreviewRequest = mPreviewRequestBuilder.build(); + + try { + mCaptureSession.setRepeatingRequest(mPreviewRequest, mCaptureCallback, mBackgroundHandler); + } catch (Exception exception) { + Log.w("QtCamera2", "Failed to set flash mode:" + exception); + } + } + } + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtCameraListener.java b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtCameraListener.java index 7f5361e77..23e9a3580 100644 --- a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtCameraListener.java +++ b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtCameraListener.java @@ -1,61 +1,21 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtMultimedia of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only package org.qtproject.qt.android.multimedia; import android.hardware.Camera; import android.hardware.Camera.CameraInfo; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.ImageFormat; +import android.graphics.Matrix; import android.graphics.SurfaceTexture; import android.util.Log; -import java.io.File; -import java.io.FileOutputStream; import java.lang.Math; -import android.media.ExifInterface; import java.io.ByteArrayOutputStream; -import java.lang.String; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.Matrix; -public class QtCameraListener implements Camera.ShutterCallback, +class QtCameraListener implements Camera.ShutterCallback, Camera.PictureCallback, Camera.AutoFocusCallback, Camera.PreviewCallback @@ -73,28 +33,29 @@ public class QtCameraListener implements Camera.ShutterCallback, private Camera.Size m_previewSize = null; private int m_previewFormat = ImageFormat.NV21; // Default preview format on all devices private int m_previewBytesPerLine = -1; + private int m_rotation = 0; private QtCameraListener(int id) { m_cameraId = id; } - public void notifyNewFrames(boolean notify) + void notifyNewFrames(boolean notify) { m_notifyNewFrames = notify; } - public void notifyWhenFrameAvailable(boolean notify) + void notifyWhenFrameAvailable(boolean notify) { m_notifyWhenFrameAvailable = notify; } - public byte[] lastPreviewBuffer() + byte[] lastPreviewBuffer() { return m_lastPreviewBuffer; } - public int previewWidth() + int previewWidth() { if (m_previewSize == null) return -1; @@ -102,7 +63,7 @@ public class QtCameraListener implements Camera.ShutterCallback, return m_previewSize.width; } - public int previewHeight() + int previewHeight() { if (m_previewSize == null) return -1; @@ -110,22 +71,27 @@ public class QtCameraListener implements Camera.ShutterCallback, return m_previewSize.height; } - public int previewFormat() + int previewFormat() { return m_previewFormat; } - public int previewBytesPerLine() + int previewBytesPerLine() { return m_previewBytesPerLine; } - public void clearPreviewCallback(Camera camera) + void clearPreviewCallback(Camera camera) { camera.setPreviewCallbackWithBuffer(null); } - public void setupPreviewCallback(Camera camera) + void setPhotoRotation(int rotation) + { + m_rotation = rotation; + } + + void setupPreviewCallback(Camera camera) { // Clear previous callback (also clears added buffers) clearPreviewCallback(camera); @@ -208,69 +174,22 @@ public class QtCameraListener implements Camera.ShutterCallback, @Override public void onPictureTaken(byte[] data, Camera camera) { - File outputFile = null; - try { - outputFile = File.createTempFile("pic_", ".jpg", QtMultimediaUtils.getCacheDirectory()); - FileOutputStream out = new FileOutputStream(outputFile); - - // we just want to read the exif... - BitmapFactory.decodeByteArray(data, 0, data.length) - .compress(Bitmap.CompressFormat.JPEG, 10, out); - - ExifInterface exif = new ExifInterface(outputFile.getAbsolutePath()); - int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, - ExifInterface.ORIENTATION_UNDEFINED); - - int degree = 0; - - switch (orientation) { - case ExifInterface.ORIENTATION_ROTATE_90: - degree = 90; - break; - case ExifInterface.ORIENTATION_ROTATE_180: - degree = 180; - break; - case ExifInterface.ORIENTATION_ROTATE_270: - degree = 270; - break; - } - - Camera.CameraInfo info = new Camera.CameraInfo(); - Camera.getCameraInfo(m_cameraId, info); - - int rotation = (info.orientation - degree + 360) % 360; - - Bitmap source = BitmapFactory.decodeByteArray(data, 0, data.length); - Matrix matrix = new Matrix(); - matrix.postRotate(rotation); - - if (info.facing == CameraInfo.CAMERA_FACING_FRONT) { - matrix.postScale(-1, 1, source.getWidth() / 2.0f, source.getHeight() / 2.0f); - } - - Bitmap rotatedBitmap = Bitmap.createBitmap(source, 0, 0, source.getWidth(), - source.getHeight(), matrix, true); - - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - rotatedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); - byte[] byteArray = outputStream.toByteArray(); - - rotatedBitmap.recycle(); - source.recycle(); - - notifyPictureCaptured(m_cameraId, byteArray); - - return; - - } catch (Exception e) { - Log.w(TAG, "Error fixing bitmap orientation."); - e.printStackTrace(); - } finally { - if (outputFile != null && outputFile.exists()) - outputFile.delete(); + Camera.CameraInfo info = new Camera.CameraInfo(); + Camera.getCameraInfo(m_cameraId, info); + Bitmap source = BitmapFactory.decodeByteArray(data, 0, data.length); + Matrix matrix = new Matrix(); + matrix.postRotate(m_rotation); + if (info.facing == CameraInfo.CAMERA_FACING_FRONT) { + matrix.postScale(-1, 1, source.getWidth() / 2.0f, source.getHeight() / 2.0f); } - - notifyPictureCaptured(m_cameraId, data); + Bitmap rotatedBitmap = Bitmap.createBitmap(source, 0, 0, source.getWidth(), + source.getHeight(), matrix, true); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + rotatedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream); + byte[] byteArray = outputStream.toByteArray(); + rotatedBitmap.recycle(); + source.recycle(); + notifyPictureCaptured(m_cameraId, byteArray); } @Override diff --git a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtExifDataHandler.java b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtExifDataHandler.java new file mode 100644 index 000000000..a27d98967 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtExifDataHandler.java @@ -0,0 +1,51 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +package org.qtproject.qt.android.multimedia; + +import android.hardware.camera2.CaptureResult; +import android.media.ExifInterface; +import android.os.Build; +import android.util.Log; + +import java.io.IOException; + +class QtExifDataHandler { + + private int mFlashFired = 0; + private long mExposureTime = 0L; + private float mFocalLength = 0; + private static String mModel = Build.MANUFACTURER + " " + Build.MODEL; + + QtExifDataHandler(CaptureResult r) + { + Integer flash = r.get(CaptureResult.FLASH_STATE); + if (flash != null && flash == CaptureResult.FLASH_STATE_FIRED) + mFlashFired = 1; + + Long exposureTime = r.get(CaptureResult.SENSOR_EXPOSURE_TIME); + if (exposureTime != null) + mExposureTime = exposureTime/1000000000; + mFocalLength = r.get(CaptureResult.LENS_FOCAL_LENGTH); + } + + void save(String path) + { + ExifInterface exif; + try { + exif = new ExifInterface(path); + } catch ( IOException e ) { + Log.e("QtExifDataHandler", "Cannot open file: " + path + "\n" + e); + return; + } + exif.setAttribute(ExifInterface.TAG_FLASH, String.valueOf(mFlashFired)); + exif.setAttribute(ExifInterface.TAG_EXPOSURE_TIME, String.valueOf(mExposureTime)); + exif.setAttribute(ExifInterface.TAG_FOCAL_LENGTH, String.valueOf(mFocalLength)); + exif.setAttribute(ExifInterface.TAG_MODEL, mModel); + + try { + exif.saveAttributes(); + } catch ( IOException e ) { + Log.e("QtExifDataHandler", "Cannot save file: " + path + "\n" + e); + } + } +} diff --git a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtMediaRecorderListener.java b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtMediaRecorderListener.java index bf1763dee..3cf77c323 100644 --- a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtMediaRecorderListener.java +++ b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtMediaRecorderListener.java @@ -1,51 +1,15 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtMultimedia of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only package org.qtproject.qt.android.multimedia; import android.media.MediaRecorder; -public class QtMediaRecorderListener implements MediaRecorder.OnErrorListener, MediaRecorder.OnInfoListener +class QtMediaRecorderListener implements MediaRecorder.OnErrorListener, MediaRecorder.OnInfoListener { private long m_id = -1; - public QtMediaRecorderListener(long id) + QtMediaRecorderListener(long id) { m_id = id; } diff --git a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtMultimediaUtils.java b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtMultimediaUtils.java index f2c6113b4..21a3989a6 100644 --- a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtMultimediaUtils.java +++ b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtMultimediaUtils.java @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtMultimedia of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only package org.qtproject.qt.android.multimedia; @@ -53,13 +17,13 @@ import java.lang.String; import java.io.File; import android.util.Log; -public class QtMultimediaUtils +class QtMultimediaUtils { static private class OrientationListener extends OrientationEventListener { - static public int deviceOrientation = 0; + static int deviceOrientation = 0; - public OrientationListener(Context context) + OrientationListener(Context context) { super(context); } @@ -78,17 +42,17 @@ public class QtMultimediaUtils static private OrientationListener m_orientationListener = null; private static final String QtTAG = "Qt QtMultimediaUtils"; - static public void setActivity(Activity qtMainActivity, Object qtActivityDelegate) + static void setActivity(Activity qtMainActivity, Object qtActivityDelegate) { } - static public void setContext(Context context) + static void setContext(Context context) { m_context = context; m_orientationListener = new OrientationListener(context); } - public QtMultimediaUtils() + QtMultimediaUtils() { } @@ -102,7 +66,7 @@ public class QtMultimediaUtils static int getDeviceOrientation() { - return m_orientationListener.deviceOrientation; + return OrientationListener.deviceOrientation; } static String getDefaultMediaDirectory(int type) @@ -161,24 +125,25 @@ public class QtMultimediaUtils return codecs; } -public static String getMimeType(Context context, String url) -{ - Uri parsedUri = Uri.parse(url); - String type = null; - - try { - String scheme = parsedUri.getScheme(); - if (scheme != null && scheme.contains("content")) { - ContentResolver cR = context.getContentResolver(); - type = cR.getType(parsedUri); - } else { - String extension = MimeTypeMap.getFileExtensionFromUrl(url); - if (extension != null) + static String getMimeType(Context context, String url) + { + Uri parsedUri = Uri.parse(url); + String type = null; + + try { + String scheme = parsedUri.getScheme(); + if (scheme != null && scheme.contains("content")) { + ContentResolver cR = context.getContentResolver(); + type = cR.getType(parsedUri); + } else { + String extension = MimeTypeMap.getFileExtensionFromUrl(url); + if (extension != null) type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - } - } catch (Exception e) { - Log.e(QtTAG, "getMimeType(): " + e.toString()); + } + } catch (Exception e) { + Log.e(QtTAG, "getMimeType(): " + e.toString()); + } + return type; } - return type; -} } + diff --git a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtSurfaceHolderCallback.java b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtSurfaceHolderCallback.java index 62000716b..bbaa0d5b9 100644 --- a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtSurfaceHolderCallback.java +++ b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtSurfaceHolderCallback.java @@ -1,51 +1,15 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtMultimedia of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only package org.qtproject.qt.android.multimedia; import android.view.SurfaceHolder; -public class QtSurfaceHolderCallback implements SurfaceHolder.Callback +class QtSurfaceHolderCallback implements SurfaceHolder.Callback { private long m_id = -1; - public QtSurfaceHolderCallback(long id) + QtSurfaceHolderCallback(long id) { m_id = id; } diff --git a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtSurfaceTextureHolder.java b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtSurfaceTextureHolder.java index ea7a41505..345c313e2 100644 --- a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtSurfaceTextureHolder.java +++ b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtSurfaceTextureHolder.java @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the (whatever) of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only package org.qtproject.qt.android.multimedia; @@ -44,11 +8,11 @@ import android.view.Surface; import android.graphics.Rect; import android.graphics.Canvas; -public class QtSurfaceTextureHolder implements SurfaceHolder +class QtSurfaceTextureHolder implements SurfaceHolder { private Surface surfaceTexture; - public QtSurfaceTextureHolder(Surface surface) + QtSurfaceTextureHolder(Surface surface) { surfaceTexture = surface; } diff --git a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtSurfaceTextureListener.java b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtSurfaceTextureListener.java index 4d929c6ad..9b4180b5d 100644 --- a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtSurfaceTextureListener.java +++ b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtSurfaceTextureListener.java @@ -1,51 +1,15 @@ -/**************************************************************************** -** -** Copyright (C) 2016 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtMultimedia of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2016 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only package org.qtproject.qt.android.multimedia; import android.graphics.SurfaceTexture; -public class QtSurfaceTextureListener implements SurfaceTexture.OnFrameAvailableListener +class QtSurfaceTextureListener implements SurfaceTexture.OnFrameAvailableListener { private final long m_id; - public QtSurfaceTextureListener(long id) + QtSurfaceTextureListener(long id) { m_id = id; } diff --git a/src/android/jar/src/org/qtproject/qt/android/multimedia/QtVideoDeviceManager.java b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtVideoDeviceManager.java new file mode 100644 index 000000000..6ec2073d8 --- /dev/null +++ b/src/android/jar/src/org/qtproject/qt/android/multimedia/QtVideoDeviceManager.java @@ -0,0 +1,247 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +package org.qtproject.qt.android.multimedia; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.ImageFormat; +import android.graphics.Rect; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.MediaCodecList; +import android.media.MediaCodecInfo; +import android.os.Build; +import android.util.Range; +import android.util.Size; +import android.util.Log; + +import java.lang.String; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +class QtVideoDeviceManager { + + CameraManager mCameraManager; + Map<String, CameraCharacteristics> cache; + + QtVideoDeviceManager(Context context) { + mCameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); + cache = new WeakHashMap<String, CameraCharacteristics>(); + } + + CameraCharacteristics getCameraCharacteristics(String cameraId) { + + if (cache.containsKey(cameraId)) + return cache.get(cameraId); + + try { + CameraCharacteristics cameraCharacteristics = mCameraManager.getCameraCharacteristics(cameraId); + cache.put(cameraId, cameraCharacteristics); + return cameraCharacteristics; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + static private boolean isSoftwareCodec(String longCodecName) { + longCodecName = longCodecName.toLowerCase(); + return longCodecName.startsWith("omx.google.") || longCodecName.startsWith("c2.android.") + || !(longCodecName.startsWith("omx.") || longCodecName.startsWith("c2.")); + } + + private enum CODEC { + DECODER, + ENCODER + } + static private String[] getHWVideoCodecs(CODEC expectedType) { + MediaCodecList mediaCodecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + MediaCodecInfo[] mediaCodecInfo = mediaCodecList.getCodecInfos(); + Set<String> codecs = new HashSet<String>(); + + for (MediaCodecInfo codecInfo : mediaCodecInfo) { + CODEC currentType = codecInfo.isEncoder() ? CODEC.ENCODER : CODEC.DECODER; + if (currentType == expectedType && !isSoftwareCodec(codecInfo.getName())) { + String[] supportedTypes = codecInfo.getSupportedTypes(); + for (String type : supportedTypes) { + if (type.startsWith("video/")) + codecs.add(type.substring(6)); + } + } + } + return codecs.toArray(new String[codecs.size()]); + } + + static String[] getHWVideoDecoders() { return getHWVideoCodecs(CODEC.DECODER); } + static String[] getHWVideoEncoders() { return getHWVideoCodecs(CODEC.ENCODER); } + + String[] getCameraIdList() { + try { + return mCameraManager.getCameraIdList(); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + int getSensorOrientation(String cameraId) { + CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); + if (characteristics == null) + return 0; + return characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + } + + int getLensFacing(String cameraId) { + CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); + if (characteristics == null) + return 0; + return characteristics.get(CameraCharacteristics.LENS_FACING); + } + + String[] getFpsRange(String cameraId) { + + CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); + if (characteristics == null) + return new String[0]; + + Range<Integer>[] ranges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + + String[] fps = new String[ranges.length]; + + for (int index = 0; index < ranges.length; index++) { + fps[index] = ranges[index].toString(); + } + + return fps; + } + + float getMaxZoom(String cameraId) { + + float maxZoom = 1.0f; + final CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); + if (characteristics != null) + maxZoom = characteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); + return maxZoom; + } + + Rect getActiveArraySize(String cameraId) { + Rect activeArraySize = new Rect(); + final CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); + if (characteristics != null) + activeArraySize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + return activeArraySize; + } + + static final int maxResolution = 3840*2160; // 4k resolution + String[] getStreamConfigurationsSizes(String cameraId, int imageFormat) { + + CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); + if (characteristics == null) + return new String[0]; + + StreamConfigurationMap map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); + Size[] sizes = map.getOutputSizes(imageFormat); + if (sizes == null) + return new String[0]; + + ArrayList<String> stream = new ArrayList<>(); + + for (int index = 0; index < sizes.length; index++) { + if (sizes[index].getWidth() * sizes[index].getHeight() <= maxResolution) + stream.add(sizes[index].toString()); + } + + return stream.toArray(new String[0]); + } + + int stringToControlAEMode(String mode) { + switch (mode) { + case "off": + return CaptureRequest.CONTROL_AE_MODE_ON; + case "auto": + return CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH; + case "on": + return CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH; + case "redeye": + return CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE; + case "external": + return CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH; + default: + return -1; + } + } + + String controlAEModeToString(int mode) { + switch (mode) { + case CaptureRequest.CONTROL_AE_MODE_ON: + return "off"; + case CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH: + return "auto"; + case CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH: + return "on"; + case CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE: + return "redeye"; + case CaptureRequest.CONTROL_AE_MODE_ON_EXTERNAL_FLASH: + return "external"; + case CaptureRequest.CONTROL_AE_MODE_OFF: + default: + return "unknown"; + } + } + + int[] getSupportedAfModes(String cameraId) { + + CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); + if (characteristics == null) + return new int[0]; + + return characteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + } + + String[] getSupportedFlashModes(String cameraId) { + + CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); + if (characteristics == null) + return new String[0]; + + int supportedFlashModes[] = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_MODES); + ArrayList<String> supportedFlashModesList = new ArrayList<String>(); + for (int index = 0; index < supportedFlashModes.length; index++) { + supportedFlashModesList.add(controlAEModeToString(supportedFlashModes[index])); + } + + String[] ret = new String[ supportedFlashModesList.size() ]; + return supportedFlashModesList.toArray(ret); + } + + static boolean isEmulator() + { + return ((Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) + || Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.startsWith("unknown") + || Build.HARDWARE.contains("goldfish") + || Build.HARDWARE.contains("ranchu") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + || Build.MANUFACTURER.contains("Genymotion") + || Build.PRODUCT.contains("sdk") + || Build.PRODUCT.contains("vbox86p") + || Build.PRODUCT.contains("emulator") + || Build.PRODUCT.contains("simulator")); + } + + boolean isTorchModeSupported(String cameraId) { + boolean ret = false; + final CameraCharacteristics characteristics = getCameraCharacteristics(cameraId); + if (characteristics != null) + ret = characteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + return ret; + } +} |