2049 lines
65 KiB
Python
2049 lines
65 KiB
Python
"""
|
|
Android Media 组件 — MediaPlayer, MediaRecorder, SoundPool, AudioManager 等
|
|
方法签名完全对齐 Android SDK
|
|
"""
|
|
|
|
import io
|
|
import math
|
|
import os
|
|
import time
|
|
import struct
|
|
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
|
|
|
from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL, HAS_PYMUPDF, resolve_android_path
|
|
from .graphics import PointF
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# AudioAttributes
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class AudioAttributes(Stub):
|
|
"""AudioAttributes — 音频属性"""
|
|
|
|
CONTENT_TYPE_UNKNOWN = 0
|
|
CONTENT_TYPE_SPEECH = 1
|
|
CONTENT_TYPE_MUSIC = 2
|
|
CONTENT_TYPE_MOVIE = 3
|
|
CONTENT_TYPE_SONIFICATION = 4
|
|
|
|
USAGE_UNKNOWN = 0
|
|
USAGE_MEDIA = 1
|
|
USAGE_VOICE_COMMUNICATION = 2
|
|
USAGE_VOICE_COMMUNICATION_SIGNALLING = 3
|
|
USAGE_ALARM = 4
|
|
USAGE_NOTIFICATION = 5
|
|
USAGE_NOTIFICATION_RINGTONE = 6
|
|
USAGE_GAME = 14
|
|
USAGE_ASSISTANCE_ACCESSIBILITY = 11
|
|
USAGE_ASSISTANCE_NAVIGATION_GUIDANCE = 12
|
|
USAGE_ASSISTANCE_SONIFICATION = 13
|
|
|
|
FLAG_AUDIBILITY_ENFORCED = 1
|
|
FLAG_SECURE = 2
|
|
FLAG_SCO = 4
|
|
FLAG_BEACON = 8
|
|
FLAG_BYPASS_INTERRUPTION_POLICY = 16
|
|
FLAG_BYPASS_MUTE = 32
|
|
FLAG_LOW_LATENCY = 64
|
|
FLAG_DEEP_BUFFER = 128
|
|
|
|
class Builder(Stub):
|
|
"""AudioAttributes.Builder — 音频属性构建器"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._content_type = AudioAttributes.CONTENT_TYPE_UNKNOWN
|
|
self._usage = AudioAttributes.USAGE_UNKNOWN
|
|
self._flags = 0
|
|
self._legacy_stream_type = -1
|
|
|
|
def setContentType(self, contentType: int) -> "AudioAttributes.Builder":
|
|
self._content_type = contentType
|
|
return self
|
|
|
|
def setUsage(self, usage: int) -> "AudioAttributes.Builder":
|
|
self._usage = usage
|
|
return self
|
|
|
|
def setFlags(self, flags: int) -> "AudioAttributes.Builder":
|
|
self._flags = flags
|
|
return self
|
|
|
|
def setLegacyStreamType(self, streamType: int) -> "AudioAttributes.Builder":
|
|
self._legacy_stream_type = streamType
|
|
return self
|
|
|
|
def build(self) -> "AudioAttributes":
|
|
attrs = AudioAttributes()
|
|
attrs._content_type = self._content_type
|
|
attrs._usage = self._usage
|
|
attrs._flags = self._flags
|
|
attrs._legacy_stream_type = self._legacy_stream_type
|
|
return attrs
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._content_type = AudioAttributes.CONTENT_TYPE_UNKNOWN
|
|
self._usage = AudioAttributes.USAGE_UNKNOWN
|
|
self._flags = 0
|
|
self._legacy_stream_type = -1
|
|
|
|
def getContentType(self) -> int:
|
|
return self._content_type
|
|
|
|
def getUsage(self) -> int:
|
|
return self._usage
|
|
|
|
def getFlags(self) -> int:
|
|
return self._flags
|
|
|
|
def getLegacyStreamType(self) -> int:
|
|
if self._legacy_stream_type >= 0:
|
|
return self._legacy_stream_type
|
|
usage_map = {
|
|
AudioAttributes.USAGE_MEDIA: 3,
|
|
AudioAttributes.USAGE_ALARM: 4,
|
|
AudioAttributes.USAGE_NOTIFICATION: 5,
|
|
AudioAttributes.USAGE_NOTIFICATION_RINGTONE: 2,
|
|
AudioAttributes.USAGE_VOICE_COMMUNICATION: 0,
|
|
AudioAttributes.USAGE_GAME: 3,
|
|
}
|
|
return usage_map.get(self._usage, 3)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# AudioFormat
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class AudioFormat(Stub):
|
|
"""AudioFormat — 音频格式"""
|
|
|
|
ENCODING_INVALID = 0
|
|
ENCODING_DEFAULT = 1
|
|
ENCODING_PCM_16BIT = 2
|
|
ENCODING_PCM_8BIT = 3
|
|
ENCODING_PCM_FLOAT = 4
|
|
ENCODING_AC3 = 5
|
|
ENCODING_E_AC3 = 6
|
|
ENCODING_DTS = 7
|
|
ENCODING_DTS_HD = 8
|
|
ENCODING_MP3 = 9
|
|
ENCODING_AAC = 10
|
|
ENCODING_AAC_LC = 11
|
|
ENCODING_AAC_HE_V1 = 12
|
|
ENCODING_AAC_HE_V2 = 13
|
|
ENCODING_AAC_ELD = 14
|
|
ENCODING_AAC_XHE = 15
|
|
ENCODING_AMRNB = 16
|
|
ENCODING_AMRWB = 17
|
|
ENCODING_OPUS = 18
|
|
|
|
CHANNEL_OUT_DEFAULT = 0x000
|
|
CHANNEL_OUT_MONO = 0x004
|
|
CHANNEL_OUT_STEREO = 0x00C
|
|
CHANNEL_OUT_QUAD = 0x01C
|
|
CHANNEL_OUT_SURROUND = 0x03C
|
|
CHANNEL_OUT_5POINT1 = 0x0FC
|
|
CHANNEL_OUT_7POINT1 = 0x3FC
|
|
|
|
CHANNEL_IN_DEFAULT = 0x000
|
|
CHANNEL_IN_MONO = 0x010
|
|
CHANNEL_IN_STEREO = 0x020
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._sample_rate = 44100
|
|
self._encoding = AudioFormat.ENCODING_PCM_16BIT
|
|
self._channel_mask = AudioFormat.CHANNEL_OUT_STEREO
|
|
self._channel_count = 2
|
|
|
|
def getSampleRate(self) -> int:
|
|
return self._sample_rate
|
|
|
|
def getEncoding(self) -> int:
|
|
return self._encoding
|
|
|
|
def getChannelMask(self) -> int:
|
|
return self._channel_mask
|
|
|
|
def getChannelCount(self) -> int:
|
|
return self._channel_count
|
|
|
|
@staticmethod
|
|
def toChannelMask(channelCount: int) -> int:
|
|
return AudioFormat.CHANNEL_OUT_MONO if channelCount == 1 else AudioFormat.CHANNEL_OUT_STEREO
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaPlayer
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaPlayer(Stub):
|
|
"""MediaPlayer — 媒体播放器"""
|
|
|
|
MEDIA_INFO_UNKNOWN = 1
|
|
MEDIA_INFO_VIDEO_TRACK_LAGGING = 700
|
|
MEDIA_INFO_VIDEO_RENDERING_START = 3
|
|
MEDIA_INFO_BUFFERING_START = 701
|
|
MEDIA_INFO_BUFFERING_END = 702
|
|
MEDIA_INFO_BAD_INTERLEAVING = 800
|
|
MEDIA_INFO_NOT_SEEKABLE = 801
|
|
MEDIA_INFO_METADATA_UPDATE = 802
|
|
MEDIA_INFO_AUDIO_NOT_PLAYING = 804
|
|
MEDIA_INFO_VIDEO_NOT_PLAYING = 805
|
|
MEDIA_INFO_SUBTITLE_TIMED_OUT = 902
|
|
MEDIA_INFO_UNSUPPORTED_SUBTITLE = 901
|
|
|
|
MEDIA_ERROR_UNKNOWN = 1
|
|
MEDIA_ERROR_SERVER_DIED = 100
|
|
MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200
|
|
MEDIA_ERROR_TIMED_OUT = -110
|
|
MEDIA_ERROR_IO = -1004
|
|
MEDIA_ERROR_MALFORMED = -1007
|
|
MEDIA_ERROR_UNSUPPORTED = -1010
|
|
|
|
# OnPreparedListener
|
|
class OnPreparedListener(Stub):
|
|
def onPrepared(self, mp: "MediaPlayer"):
|
|
pass
|
|
|
|
# OnCompletionListener
|
|
class OnCompletionListener(Stub):
|
|
def onCompletion(self, mp: "MediaPlayer"):
|
|
pass
|
|
|
|
# OnErrorListener
|
|
class OnErrorListener(Stub):
|
|
def onError(self, mp: "MediaPlayer", what: int, extra: int) -> bool:
|
|
return False
|
|
|
|
# OnInfoListener
|
|
class OnInfoListener(Stub):
|
|
def onInfo(self, mp: "MediaPlayer", what: int, extra: int) -> bool:
|
|
return False
|
|
|
|
# OnSeekCompleteListener
|
|
class OnSeekCompleteListener(Stub):
|
|
def onSeekComplete(self, mp: "MediaPlayer"):
|
|
pass
|
|
|
|
# OnVideoSizeChangedListener
|
|
class OnVideoSizeChangedListener(Stub):
|
|
def onVideoSizeChanged(self, mp: "MediaPlayer", width: int, height: int):
|
|
pass
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._data_source = None
|
|
self._prepared = False
|
|
self._playing = False
|
|
self._looping = False
|
|
self._volume_left = 1.0
|
|
self._volume_right = 1.0
|
|
self._current_position = 0
|
|
self._duration = 0
|
|
self._video_width = 0
|
|
self._video_height = 0
|
|
self._screen_on = False
|
|
self._wake_mode = 0
|
|
self._audio_attributes = None
|
|
self._display = None
|
|
self._surface = None
|
|
|
|
# Listeners
|
|
self._on_prepared_listener = None
|
|
self._on_completion_listener = None
|
|
self._on_error_listener = None
|
|
self._on_info_listener = None
|
|
self._on_seek_complete_listener = None
|
|
self._on_video_size_changed_listener = None
|
|
|
|
def setDataSource(self, path: str = None, uri=None, headers: Dict = None):
|
|
self._data_source = path or str(uri)
|
|
|
|
def prepare(self):
|
|
self._prepared = True
|
|
|
|
def prepareAsync(self):
|
|
self._prepared = True
|
|
if self._on_prepared_listener:
|
|
self._on_prepared_listener.onPrepared(self)
|
|
|
|
def start(self):
|
|
self._playing = True
|
|
|
|
def stop(self):
|
|
self._playing = False
|
|
|
|
def pause(self):
|
|
self._playing = False
|
|
|
|
def reset(self):
|
|
self._playing = False
|
|
self._prepared = False
|
|
self._current_position = 0
|
|
|
|
def release(self):
|
|
self._playing = False
|
|
self._prepared = False
|
|
|
|
def seekTo(self, msec: int):
|
|
self._current_position = max(0, min(msec, self._duration))
|
|
if self._on_seek_complete_listener:
|
|
self._on_seek_complete_listener.onSeekComplete(self)
|
|
|
|
def getCurrentPosition(self) -> int:
|
|
return self._current_position
|
|
|
|
def getDuration(self) -> int:
|
|
return self._duration
|
|
|
|
def isPlaying(self) -> bool:
|
|
return self._playing
|
|
|
|
def isLooping(self) -> bool:
|
|
return self._looping
|
|
|
|
def setLooping(self, looping: bool):
|
|
self._looping = looping
|
|
|
|
def setVolume(self, leftVolume: float, rightVolume: float):
|
|
self._volume_left = leftVolume
|
|
self._volume_right = rightVolume
|
|
|
|
def getVolume(self) -> Tuple[float, float]:
|
|
return (self._volume_left, self._volume_right)
|
|
|
|
def setScreenOnWhilePlaying(self, screenOn: bool):
|
|
self._screen_on = screenOn
|
|
|
|
def setWakeMode(self, context, mode: int):
|
|
self._wake_mode = mode
|
|
|
|
def setAudioAttributes(self, attributes: AudioAttributes):
|
|
self._audio_attributes = attributes
|
|
|
|
def setDisplay(self, surface_holder):
|
|
self._display = surface_holder
|
|
|
|
def getVideoWidth(self) -> int:
|
|
return self._video_width
|
|
|
|
def getVideoHeight(self) -> int:
|
|
return self._video_height
|
|
|
|
def setOnPreparedListener(self, listener: "MediaPlayer.OnPreparedListener"):
|
|
self._on_prepared_listener = listener
|
|
|
|
def setOnCompletionListener(self, listener: "MediaPlayer.OnCompletionListener"):
|
|
self._on_completion_listener = listener
|
|
|
|
def setOnErrorListener(self, listener: "MediaPlayer.OnErrorListener"):
|
|
self._on_error_listener = listener
|
|
|
|
def setOnInfoListener(self, listener: "MediaPlayer.OnInfoListener"):
|
|
self._on_info_listener = listener
|
|
|
|
def setOnSeekCompleteListener(self, listener: "MediaPlayer.OnSeekCompleteListener"):
|
|
self._on_seek_complete_listener = listener
|
|
|
|
def setOnVideoSizeChangedListener(self, listener: "MediaPlayer.OnVideoSizeChangedListener"):
|
|
self._on_video_size_changed_listener = listener
|
|
|
|
@staticmethod
|
|
def create(context, resid: int = 0) -> "MediaPlayer":
|
|
mp = MediaPlayer()
|
|
mp._prepared = True
|
|
return mp
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaRecorder
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaRecorder(Stub):
|
|
"""MediaRecorder — 媒体录制器"""
|
|
|
|
class AudioSource:
|
|
DEFAULT = 0
|
|
MIC = 1
|
|
VOICE_UPLINK = 2
|
|
VOICE_DOWNLINK = 3
|
|
VOICE_CALL = 4
|
|
CAMCORDER = 5
|
|
VOICE_RECOGNITION = 6
|
|
VOICE_COMMUNICATION = 7
|
|
REMOTE_SUBMIX = 8
|
|
UNPROCESSED = 9
|
|
|
|
class VideoSource:
|
|
DEFAULT = 0
|
|
CAMERA = 1
|
|
SURFACE = 2
|
|
|
|
class OutputFormat:
|
|
DEFAULT = 0
|
|
THREE_GPP = 1
|
|
MPEG_4 = 2
|
|
AMR_NB = 3
|
|
AMR_WB = 4
|
|
AAC_ADIF = 5
|
|
AAC_ADTS = 6
|
|
OUTPUT_FORMAT_RTP_AVP = 7
|
|
MPEG_2_TS = 8
|
|
WEBM = 9
|
|
HEIF = 10
|
|
OGG = 11
|
|
|
|
class AudioEncoder:
|
|
DEFAULT = 0
|
|
AMR_NB = 1
|
|
AMR_WB = 2
|
|
AAC = 3
|
|
HE_AAC = 4
|
|
AAC_ELD = 5
|
|
VORBIS = 6
|
|
OPUS = 7
|
|
LPCM = 8
|
|
|
|
class VideoEncoder:
|
|
DEFAULT = 0
|
|
H_263 = 1
|
|
H_264 = 2
|
|
MPEG_4_SP = 3
|
|
VP8 = 4
|
|
HEVC = 5
|
|
VP9 = 6
|
|
AV1 = 7
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._audio_source = 0
|
|
self._video_source = 0
|
|
self._output_format = 0
|
|
self._audio_encoder = 0
|
|
self._video_encoder = 0
|
|
self._output_file = None
|
|
self._preview_display = None
|
|
self._recording = False
|
|
|
|
def setAudioSource(self, audio_source: int):
|
|
self._audio_source = audio_source
|
|
|
|
def setVideoSource(self, video_source: int):
|
|
self._video_source = video_source
|
|
|
|
def setOutputFormat(self, output_format: int):
|
|
self._output_format = output_format
|
|
|
|
def setAudioEncoder(self, audio_encoder: int):
|
|
self._audio_encoder = audio_encoder
|
|
|
|
def setVideoEncoder(self, video_encoder: int):
|
|
self._video_encoder = video_encoder
|
|
|
|
def setOutputFile(self, path: str):
|
|
self._output_file = path
|
|
|
|
def setPreviewDisplay(self, surface_holder):
|
|
self._preview_display = surface_holder
|
|
|
|
def prepare(self):
|
|
pass
|
|
|
|
def start(self):
|
|
self._recording = True
|
|
|
|
def stop(self):
|
|
self._recording = False
|
|
|
|
def reset(self):
|
|
self._recording = False
|
|
|
|
def release(self):
|
|
self._recording = False
|
|
|
|
def getMaxAmplitude(self) -> int:
|
|
return 0
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# SoundPool
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class SoundPool(Stub):
|
|
"""SoundPool — 音效池"""
|
|
|
|
def __init__(self, maxStreams: int = 1, streamType: int = 3, srcQuality: int = 0):
|
|
super().__init__()
|
|
self._max_streams = maxStreams
|
|
self._stream_type = streamType
|
|
self._src_quality = srcQuality
|
|
self._sounds: Dict[int, str] = {}
|
|
self._playing: Dict[int, float] = {}
|
|
self._on_load_complete_listener = None
|
|
self._next_id = 1
|
|
|
|
def load(self, path: str, priority: int = 1) -> int:
|
|
sound_id = self._next_id
|
|
self._next_id += 1
|
|
self._sounds[sound_id] = path
|
|
if self._on_load_complete_listener:
|
|
self._on_load_complete_listener.onLoadComplete(self, sound_id, 0)
|
|
return sound_id
|
|
|
|
def load(self, context, resId: int, priority: int) -> int:
|
|
return self.load(str(resId), priority)
|
|
|
|
def play(self, soundID: int, leftVolume: float, rightVolume: float,
|
|
priority: int, loop: int, rate: float) -> int:
|
|
stream_id = self._next_id
|
|
self._next_id += 1
|
|
self._playing[stream_id] = 0.0
|
|
return stream_id
|
|
|
|
def pause(self, streamID: int):
|
|
pass
|
|
|
|
def resume(self, streamID: int):
|
|
pass
|
|
|
|
def autoPause(self):
|
|
pass
|
|
|
|
def autoResume(self):
|
|
pass
|
|
|
|
def release(self):
|
|
self._sounds.clear()
|
|
self._playing.clear()
|
|
|
|
def setVolume(self, streamID: int, leftVolume: float, rightVolume: float):
|
|
pass
|
|
|
|
def setRate(self, streamID: int, rate: float):
|
|
pass
|
|
|
|
def setPriority(self, streamID: int, priority: int):
|
|
pass
|
|
|
|
def setLoop(self, streamID: int, loop: int):
|
|
pass
|
|
|
|
def setOnLoadCompleteListener(self, listener):
|
|
self._on_load_complete_listener = listener
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# AudioManager
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class AudioManager(Stub):
|
|
"""AudioManager — 音频管理器"""
|
|
|
|
MODE_NORMAL = 0
|
|
MODE_RINGTONE = 1
|
|
MODE_IN_CALL = 2
|
|
MODE_IN_COMMUNICATION = 3
|
|
|
|
STREAM_VOICE_CALL = 0
|
|
STREAM_SYSTEM = 1
|
|
STREAM_RING = 2
|
|
STREAM_MUSIC = 3
|
|
STREAM_ALARM = 4
|
|
STREAM_NOTIFICATION = 5
|
|
STREAM_BLUETOOTH_SCO = 6
|
|
STREAM_DTMF = 8
|
|
STREAM_TTS = 9
|
|
STREAM_ACCESSIBILITY = 10
|
|
|
|
ADJUST_RAISE = 1
|
|
ADJUST_LOWER = -1
|
|
ADJUST_SAME = 0
|
|
ADJUST_MUTE = -100
|
|
ADJUST_UNMUTE = 100
|
|
ADJUST_TOGGLE_MUTE = 101
|
|
|
|
FLAG_PLAY_SOUND = 1
|
|
FLAG_SHOW_UI = 2
|
|
FLAG_REMOVE_SOUND_AND_VIBRATE = 4
|
|
FLAG_ALLOW_RINGER_MODES = 8
|
|
FLAG_VIBRATE = 16
|
|
FLAG_BYPASS_INTERRUPTION_POLICY = 32
|
|
FLAG_BYPASS_MUTE = 64
|
|
FLAG_LOW_LATENCY = 128
|
|
|
|
RINGER_MODE_NORMAL = 2
|
|
RINGER_MODE_SILENT = 0
|
|
RINGER_MODE_VIBRATE = 1
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._mode = AudioManager.MODE_NORMAL
|
|
self._ringer_mode = AudioManager.RINGER_MODE_NORMAL
|
|
self._speakerphone_on = False
|
|
self._bluetooth_sco_on = False
|
|
self._wired_headset_on = False
|
|
self._stream_volumes = {
|
|
AudioManager.STREAM_VOICE_CALL: 4,
|
|
AudioManager.STREAM_SYSTEM: 7,
|
|
AudioManager.STREAM_RING: 5,
|
|
AudioManager.STREAM_MUSIC: 10,
|
|
AudioManager.STREAM_ALARM: 7,
|
|
AudioManager.STREAM_NOTIFICATION: 5,
|
|
}
|
|
self._parameters: Dict[str, str] = {}
|
|
|
|
def getStreamVolume(self, streamType: int) -> int:
|
|
return self._stream_volumes.get(streamType, 0)
|
|
|
|
def getStreamMaxVolume(self, streamType: int) -> int:
|
|
return 15
|
|
|
|
def setStreamVolume(self, streamType: int, index: int, flags: int = 0):
|
|
if streamType in self._stream_volumes:
|
|
self._stream_volumes[streamType] = max(0, min(15, index))
|
|
|
|
def adjustVolume(self, direction: int, flags: int = 0):
|
|
pass
|
|
|
|
def adjustStreamVolume(self, streamType: int, direction: int, flags: int = 0):
|
|
current = self.getStreamVolume(streamType)
|
|
if direction == AudioManager.ADJUST_RAISE:
|
|
self.setStreamVolume(streamType, current + 1)
|
|
elif direction == AudioManager.ADJUST_LOWER:
|
|
self.setStreamVolume(streamType, current - 1)
|
|
|
|
def getMode(self) -> int:
|
|
return self._mode
|
|
|
|
def setMode(self, mode: int):
|
|
self._mode = mode
|
|
|
|
def setSpeakerphoneOn(self, on: bool):
|
|
self._speakerphone_on = on
|
|
|
|
def isSpeakerphoneOn(self) -> bool:
|
|
return self._speakerphone_on
|
|
|
|
def isWiredHeadsetOn(self) -> bool:
|
|
return self._wired_headset_on
|
|
|
|
def isBluetoothScoOn(self) -> bool:
|
|
return self._bluetooth_sco_on
|
|
|
|
def setBluetoothScoOn(self, on: bool):
|
|
self._bluetooth_sco_on = on
|
|
|
|
def setRingerMode(self, ringerMode: int):
|
|
self._ringer_mode = ringerMode
|
|
|
|
def getRingerMode(self) -> int:
|
|
return self._ringer_mode
|
|
|
|
def loadSoundEffects(self):
|
|
pass
|
|
|
|
def unloadSoundEffects(self):
|
|
pass
|
|
|
|
def getParameters(self, keys: str) -> str:
|
|
return ",".join(f"{k}={v}" for k, v in self._parameters.items() if k in keys)
|
|
|
|
def setParameters(self, keyValuePairs: str):
|
|
for pair in keyValuePairs.split(";"):
|
|
if "=" in pair:
|
|
k, v = pair.split("=", 1)
|
|
self._parameters[k.strip()] = v.strip()
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# AudioTrack
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class AudioTrack(Stub):
|
|
"""AudioTrack — 音频轨道"""
|
|
|
|
STATE_UNINITIALIZED = 0
|
|
STATE_INITIALIZED = 1
|
|
STATE_NO_STATIC_DATA = 2
|
|
MODE_STATIC = 0
|
|
MODE_STREAM = 1
|
|
WRITE_BLOCKING = -1
|
|
WRITE_NON_BLOCKING = 0
|
|
SUCCESS = 0
|
|
ERROR = -1
|
|
ERROR_BAD_VALUE = -2
|
|
ERROR_INVALID_OPERATION = -3
|
|
ERROR_DEAD_OBJECT = -6
|
|
ERROR_WAS_BLOCKED = -8
|
|
|
|
PERFORMANCE_MODE_NONE = 0
|
|
PERFORMANCE_MODE_LOW_LATENCY = 1
|
|
PERFORMANCE_MODE_POWER_SAVING = 2
|
|
|
|
def __init__(self, streamType: int, sampleRateInHz: int, channelConfig: int,
|
|
audioFormat: int, bufferSizeInBytes: int, mode: int):
|
|
super().__init__()
|
|
self._state = AudioTrack.STATE_INITIALIZED
|
|
self._playing = False
|
|
self._sample_rate = sampleRateInHz
|
|
self._channel_config = channelConfig
|
|
self._audio_format = audioFormat
|
|
self._buffer_size = bufferSizeInBytes
|
|
self._mode = mode
|
|
self._stream_type = streamType
|
|
self._volume = 1.0
|
|
self._playback_rate = sampleRateInHz
|
|
self._buffer = bytearray(bufferSizeInBytes)
|
|
|
|
def play(self):
|
|
self._playing = True
|
|
|
|
def pause(self):
|
|
self._playing = False
|
|
|
|
def stop(self):
|
|
self._playing = False
|
|
|
|
def flush(self):
|
|
pass
|
|
|
|
def release(self):
|
|
self._state = AudioTrack.STATE_UNINITIALIZED
|
|
self._playing = False
|
|
|
|
def write(self, audioData, offsetInBytes: int = 0, sizeInBytes: int = -1,
|
|
writeMode: int = WRITE_BLOCKING) -> int:
|
|
if sizeInBytes < 0:
|
|
sizeInBytes = len(audioData) - offsetInBytes
|
|
if self._buffer and offsetInBytes + sizeInBytes <= len(self._buffer):
|
|
self._buffer[offsetInBytes:offsetInBytes + sizeInBytes] = audioData[offsetInBytes:offsetInBytes + sizeInBytes]
|
|
return sizeInBytes
|
|
|
|
def getPlayState(self) -> int:
|
|
return 3 if self._playing else 1
|
|
|
|
def getState(self) -> int:
|
|
return self._state
|
|
|
|
def getSampleRate(self) -> int:
|
|
return self._sample_rate
|
|
|
|
def getPlaybackRate(self) -> int:
|
|
return self._playback_rate
|
|
|
|
def setPlaybackRate(self, sampleRateInHz: int):
|
|
self._playback_rate = sampleRateInHz
|
|
|
|
def setVolume(self, volume: float):
|
|
self._volume = max(0.0, min(1.0, volume))
|
|
|
|
def getVolume(self) -> float:
|
|
return self._volume
|
|
|
|
def getAudioFormat(self) -> int:
|
|
return self._audio_format
|
|
|
|
def getChannelConfiguration(self) -> int:
|
|
return self._channel_config
|
|
|
|
def getChannelCount(self) -> int:
|
|
return 2 if self._channel_config == AudioFormat.CHANNEL_OUT_STEREO else 1
|
|
|
|
def getNotificationMarkerPosition(self) -> int:
|
|
return 0
|
|
|
|
def setNotificationMarkerPosition(self, marker: int):
|
|
pass
|
|
|
|
def getPositionNotificationPeriod(self) -> int:
|
|
return 0
|
|
|
|
def setPositionNotificationPeriod(self, period: int):
|
|
pass
|
|
|
|
@staticmethod
|
|
def getMinBufferSize(sampleRateInHz: int, channelConfig: int, audioFormat: int) -> int:
|
|
return sampleRateInHz * 2 * 2 # Conservative estimate
|
|
|
|
@staticmethod
|
|
def getNativeOutputSampleRate(streamType: int) -> int:
|
|
return 44100
|
|
|
|
@staticmethod
|
|
def getMaxVolume() -> float:
|
|
return 1.0
|
|
|
|
@staticmethod
|
|
def getMinVolume() -> float:
|
|
return 0.0
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# AudioRecord
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class AudioRecord(Stub):
|
|
"""AudioRecord — 音频录制器"""
|
|
|
|
STATE_UNINITIALIZED = 0
|
|
STATE_INITIALIZED = 1
|
|
SUCCESS = 0
|
|
ERROR = -1
|
|
ERROR_BAD_VALUE = -2
|
|
ERROR_INVALID_OPERATION = -3
|
|
ERROR_DEAD_OBJECT = -6
|
|
|
|
def __init__(self, audioSource: int, sampleRateInHz: int, channelConfig: int,
|
|
audioFormat: int, bufferSizeInBytes: int):
|
|
super().__init__()
|
|
self._state = AudioRecord.STATE_INITIALIZED
|
|
self._recording = False
|
|
self._audio_source = audioSource
|
|
self._sample_rate = sampleRateInHz
|
|
self._channel_config = channelConfig
|
|
self._audio_format = audioFormat
|
|
self._buffer_size = bufferSizeInBytes
|
|
self._buffer = bytearray(bufferSizeInBytes)
|
|
|
|
def startRecording(self):
|
|
self._recording = True
|
|
|
|
def stop(self):
|
|
self._recording = False
|
|
|
|
def release(self):
|
|
self._state = AudioRecord.STATE_UNINITIALIZED
|
|
self._recording = False
|
|
|
|
def read(self, audioData, offsetInBytes: int, sizeInBytes: int) -> int:
|
|
return min(sizeInBytes, len(audioData) - offsetInBytes)
|
|
|
|
def getState(self) -> int:
|
|
return self._state
|
|
|
|
def getSampleRate(self) -> int:
|
|
return self._sample_rate
|
|
|
|
def getRecordingState(self) -> int:
|
|
return 3 if self._recording else 1
|
|
|
|
def getAudioFormat(self) -> int:
|
|
return self._audio_format
|
|
|
|
def getChannelConfiguration(self) -> int:
|
|
return self._channel_config
|
|
|
|
def getChannelCount(self) -> int:
|
|
return 2 if self._channel_config == AudioFormat.CHANNEL_IN_STEREO else 1
|
|
|
|
@staticmethod
|
|
def getMinBufferSize(sampleRateInHz: int, channelConfig: int, audioFormat: int) -> int:
|
|
return sampleRateInHz * 2 * 2
|
|
|
|
def getAudioSource(self) -> int:
|
|
return self._audio_source
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# Ringtone
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class Ringtone(Stub):
|
|
"""Ringtone — 铃声"""
|
|
|
|
def __init__(self, context=None):
|
|
super().__init__()
|
|
self._title = ""
|
|
self._stream_type = AudioManager.STREAM_RING
|
|
self._playing = False
|
|
self._volume = 1.0
|
|
|
|
def setStreamType(self, streamType: int):
|
|
self._stream_type = streamType
|
|
|
|
def getStreamType(self) -> int:
|
|
return self._stream_type
|
|
|
|
def setTitle(self, title: str):
|
|
self._title = title
|
|
|
|
def getTitle(self, context=None) -> str:
|
|
return self._title
|
|
|
|
def play(self):
|
|
self._playing = True
|
|
|
|
def stop(self):
|
|
self._playing = False
|
|
|
|
def isPlaying(self) -> bool:
|
|
return self._playing
|
|
|
|
def setVolume(self, volume: float):
|
|
self._volume = max(0.0, min(1.0, volume))
|
|
|
|
def getVolume(self) -> float:
|
|
return self._volume
|
|
|
|
def getAudioAttributes(self) -> AudioAttributes:
|
|
attrs = AudioAttributes()
|
|
if self._stream_type == AudioManager.STREAM_RING:
|
|
attrs._usage = AudioAttributes.USAGE_NOTIFICATION_RINGTONE
|
|
elif self._stream_type == AudioManager.STREAM_ALARM:
|
|
attrs._usage = AudioAttributes.USAGE_ALARM
|
|
else:
|
|
attrs._usage = AudioAttributes.USAGE_NOTIFICATION
|
|
return attrs
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# RingtoneManager
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class RingtoneManager(Stub):
|
|
"""RingtoneManager — 铃声管理器"""
|
|
|
|
TYPE_RINGTONE = 1
|
|
TYPE_NOTIFICATION = 2
|
|
TYPE_ALARM = 4
|
|
TYPE_ALL = 7
|
|
|
|
ACTION_RINGTONE_PICKER = "android.intent.action.RINGTONE_PICKER"
|
|
EXTRA_RINGTONE_TYPE = "android.intent.extra.ringtone.TYPE"
|
|
EXTRA_RINGTONE_TITLE = "android.intent.extra.ringtone.TITLE"
|
|
EXTRA_RINGTONE_EXISTING_URI = "android.intent.extra.ringtone.EXISTING_URI"
|
|
EXTRA_RINGTONE_DEFAULT_URI = "android.intent.extra.ringtone.DEFAULT_URI"
|
|
EXTRA_RINGTONE_SHOW_DEFAULT = "android.intent.extra.ringtone.SHOW_DEFAULT"
|
|
EXTRA_RINGTONE_INCLUDE_DRM = "android.intent.extra.ringtone.INCLUDE_DRM"
|
|
EXTRA_RINGTONE_PICKED_URI = "android.intent.extra.ringtone.PICKED_URI"
|
|
|
|
def __init__(self, context):
|
|
super().__init__()
|
|
self._context = context
|
|
self._type = RingtoneManager.TYPE_RINGTONE
|
|
self._cursor = []
|
|
self._cursor_position = -1
|
|
|
|
def setType(self, type_: int):
|
|
self._type = type_
|
|
|
|
def getCursor(self):
|
|
return self._cursor
|
|
|
|
def getRingtoneUri(self, position: int):
|
|
return None
|
|
|
|
def getRingtonePosition(self, uri) -> int:
|
|
return 0
|
|
|
|
@staticmethod
|
|
def getRingtone(context, uri) -> Ringtone:
|
|
return Ringtone(context)
|
|
|
|
@staticmethod
|
|
def getActualDefaultRingtoneUri(context, type_: int):
|
|
return None
|
|
|
|
@staticmethod
|
|
def setActualDefaultRingtoneUri(context, type_: int, ringtoneUri):
|
|
pass
|
|
|
|
@staticmethod
|
|
def getDefaultUri(type_: int):
|
|
return None
|
|
|
|
@staticmethod
|
|
def getValidRingtoneUri(context):
|
|
return None
|
|
|
|
@staticmethod
|
|
def inferStreamType(type_: int) -> int:
|
|
if type_ == RingtoneManager.TYPE_RINGTONE:
|
|
return AudioManager.STREAM_RING
|
|
elif type_ == RingtoneManager.TYPE_NOTIFICATION:
|
|
return AudioManager.STREAM_NOTIFICATION
|
|
elif type_ == RingtoneManager.TYPE_ALARM:
|
|
return AudioManager.STREAM_ALARM
|
|
return AudioManager.STREAM_RING
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaMetadataRetriever
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaMetadataRetriever(Stub):
|
|
"""MediaMetadataRetriever — 媒体元数据检索器"""
|
|
|
|
METADATA_KEY_ALBUM = "album"
|
|
METADATA_KEY_ARTIST = "artist"
|
|
METADATA_KEY_TITLE = "title"
|
|
METADATA_KEY_DURATION = "duration"
|
|
METADATA_KEY_CD_TRACK_NUMBER = "cd_track_number"
|
|
METADATA_KEY_COMPOSER = "composer"
|
|
METADATA_KEY_DATE = "date"
|
|
METADATA_KEY_GENRE = "genre"
|
|
METADATA_KEY_MIMETYPE = "mimetype"
|
|
METADATA_KEY_NUM_TRACKS = "num_tracks"
|
|
METADATA_KEY_WRITER = "writer"
|
|
METADATA_KEY_YEAR = "year"
|
|
METADATA_KEY_VIDEO_WIDTH = "video_width"
|
|
METADATA_KEY_VIDEO_HEIGHT = "video_height"
|
|
METADATA_KEY_HAS_AUDIO = "has_audio"
|
|
METADATA_KEY_HAS_VIDEO = "has_video"
|
|
METADATA_KEY_BITRATE = "bitrate"
|
|
METADATA_KEY_LOCATION = "location"
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._metadata: Dict[str, str] = {}
|
|
|
|
def setDataSource(self, path: str):
|
|
resolved = resolve_android_path(path)
|
|
if os.path.exists(resolved):
|
|
size = os.path.getsize(resolved)
|
|
self._metadata = {
|
|
MediaMetadataRetriever.METADATA_KEY_DURATION: "30000",
|
|
MediaMetadataRetriever.METADATA_KEY_TITLE: os.path.basename(resolved),
|
|
MediaMetadataRetriever.METADATA_KEY_MIMETYPE: "video/mp4",
|
|
MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH: "1920",
|
|
MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT: "1080",
|
|
MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO: "yes",
|
|
MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO: "yes",
|
|
MediaMetadataRetriever.METADATA_KEY_BITRATE: str(size * 8 // 30),
|
|
}
|
|
|
|
def setDataSource(self, path: str, headers: Dict[str, str]):
|
|
self.setDataSource(path)
|
|
|
|
def extractMetadata(self, keyCode: str) -> Optional[str]:
|
|
return self._metadata.get(keyCode)
|
|
|
|
def getFrameAtTime(self, timeUs: int = -1, option: int = 0) -> Optional["Bitmap"]:
|
|
if HAS_PIL:
|
|
from PIL import Image
|
|
from .graphics import Bitmap
|
|
img = Image.new("RGB", (1920, 1080), (64, 64, 64))
|
|
bm = Bitmap(1920, 1080, Bitmap.Config.ARGB_8888)
|
|
bm._image = img
|
|
return bm
|
|
return None
|
|
|
|
def getScaledFrameAtTime(self, timeUs: int, option: int, width: int, height: int) -> Optional["Bitmap"]:
|
|
if HAS_PIL:
|
|
from PIL import Image
|
|
from .graphics import Bitmap
|
|
img = Image.new("RGB", (width, height), (64, 64, 64))
|
|
bm = Bitmap(width, height, Bitmap.Config.ARGB_8888)
|
|
bm._image = img
|
|
return bm
|
|
return None
|
|
|
|
def release(self):
|
|
self._metadata.clear()
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaCodec
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaCodec(Stub):
|
|
"""MediaCodec — 媒体编解码器"""
|
|
|
|
BUFFER_FLAG_KEY_FRAME = 1
|
|
BUFFER_FLAG_CODEC_CONFIG = 2
|
|
BUFFER_FLAG_END_OF_STREAM = 4
|
|
BUFFER_FLAG_PARTIAL_FRAME = 8
|
|
BUFFER_FLAG_MUXER_DATA = 16
|
|
|
|
CONFIGURE_FLAG_ENCODE = 1
|
|
CONFIGURE_FLAG_DECODE = 0
|
|
|
|
CRYPTO_MODE_UNENCRYPTED = 0
|
|
CRYPTO_MODE_AES_CTR = 1
|
|
CRYPTO_MODE_AES_WV = 2
|
|
CRYPTO_MODE_AES_CBC = 3
|
|
|
|
INFO_TRY_AGAIN_LATER = -1
|
|
INFO_OUTPUT_FORMAT_CHANGED = -2
|
|
INFO_OUTPUT_BUFFERS_CHANGED = -3
|
|
|
|
MEDIA_CODEC_UNKNOWN = 0
|
|
MEDIA_CODEC_DECODER = 1
|
|
MEDIA_CODEC_ENCODER = 2
|
|
|
|
PARAMETER_KEY_SUSPEND = "suspend"
|
|
PARAMETER_KEY_VIDEO_BITRATE = "video-bitrate"
|
|
PARAMETER_KEY_REQUEST_SYNC_FRAME = "request-sync"
|
|
PARAMETER_KEY_DROP_BEFORE = "drop-before"
|
|
PARAMETER_KEY_DROP_AFTER = "drop-after"
|
|
PARAMETER_KEY_OFFSET_TIME = "offset-time"
|
|
PARAMETER_KEY_PRIORITY = "priority"
|
|
PARAMETER_KEY_OPERATING_RATE = "operating-rate"
|
|
PARAMETER_KEY_INTRA_REFRESH_PERIOD = "intra-refresh-period"
|
|
PARAMETER_KEY_LOW_LATENCY = "low-latency"
|
|
|
|
def __init__(self, name: str):
|
|
super().__init__()
|
|
self._name = name
|
|
self._configured = False
|
|
self._started = False
|
|
self._format = MediaFormat()
|
|
|
|
@staticmethod
|
|
def createDecoderByType(mimeType: str) -> "MediaCodec":
|
|
return MediaCodec(f"decoder.{mimeType}")
|
|
|
|
@staticmethod
|
|
def createEncoderByType(mimeType: str) -> "MediaCodec":
|
|
return MediaCodec(f"encoder.{mimeType}")
|
|
|
|
def configure(self, format: "MediaFormat", surface, crypto, flags: int):
|
|
self._format = format
|
|
self._configured = True
|
|
|
|
def start(self):
|
|
self._started = True
|
|
|
|
def stop(self):
|
|
self._started = False
|
|
|
|
def flush(self):
|
|
pass
|
|
|
|
def release(self):
|
|
self._started = False
|
|
self._configured = False
|
|
|
|
def getInputBuffers(self) -> List[Any]:
|
|
return [bytearray(4096)]
|
|
|
|
def getOutputBuffers(self) -> List[Any]:
|
|
return [bytearray(4096)]
|
|
|
|
def dequeueInputBuffer(self, timeoutUs: int) -> int:
|
|
return 0
|
|
|
|
def queueInputBuffer(self, index: int, offset: int, size: int,
|
|
presentationTimeUs: int, flags: int):
|
|
pass
|
|
|
|
def dequeueOutputBuffer(self, info: "MediaCodec.BufferInfo", timeoutUs: int) -> int:
|
|
return MediaCodec.INFO_TRY_AGAIN_LATER
|
|
|
|
def releaseOutputBuffer(self, index: int, render: bool):
|
|
pass
|
|
|
|
def getOutputFormat(self) -> "MediaFormat":
|
|
return self._format
|
|
|
|
def getInputImage(self, index: int):
|
|
return None
|
|
|
|
def getOutputImage(self, index: int):
|
|
return None
|
|
|
|
def getName(self) -> str:
|
|
return self._name
|
|
|
|
def getCodecInfo(self):
|
|
return None
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaCodecBufferInfo (nested class reference)
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaCodecBufferInfo(Stub):
|
|
"""MediaCodec.BufferInfo — 编解码器缓冲信息"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.offset = 0
|
|
self.size = 0
|
|
self.presentationTimeUs = 0
|
|
self.flags = 0
|
|
|
|
def set(self, offset: int, size: int, presentationTimeUs: int, flags: int):
|
|
self.offset = offset
|
|
self.size = size
|
|
self.presentationTimeUs = presentationTimeUs
|
|
self.flags = flags
|
|
|
|
def duplicate(self) -> "MediaCodecBufferInfo":
|
|
info = MediaCodecBufferInfo()
|
|
info.set(self.offset, self.size, self.presentationTimeUs, self.flags)
|
|
return info
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaFormat
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaFormat(Stub):
|
|
"""MediaFormat — 媒体格式"""
|
|
|
|
KEY_MIME = "mime"
|
|
KEY_WIDTH = "width"
|
|
KEY_HEIGHT = "height"
|
|
KEY_FRAME_RATE = "frame-rate"
|
|
KEY_BIT_RATE = "bitrate"
|
|
KEY_COLOR_FORMAT = "color-format"
|
|
KEY_I_FRAME_INTERVAL = "i-frame-interval"
|
|
KEY_CHANNEL_COUNT = "channel-count"
|
|
KEY_SAMPLE_RATE = "sample-rate"
|
|
KEY_DURATION = "durationUs"
|
|
KEY_MAX_INPUT_SIZE = "max-input-size"
|
|
KEY_AAC_PROFILE = "aac-profile"
|
|
KEY_LEVEL = "level"
|
|
KEY_PROFILE = "profile"
|
|
KEY_LANGUAGE = "language"
|
|
KEY_IS_AUTOSELECT = "is-autoselect"
|
|
KEY_IS_DEFAULT = "is-default"
|
|
KEY_IS_FORCED_SUBTITLE = "is-forced-subtitle"
|
|
KEY_PIXEL_ASPECT_RATIO_WIDTH = "pixel-aspect-ratio-width"
|
|
KEY_PIXEL_ASPECT_RATIO_HEIGHT = "pixel-aspect-ratio-height"
|
|
KEY_SLICE_HEIGHT = "slice-height"
|
|
KEY_STRIDE = "stride"
|
|
KEY_TEMPORAL_LAYERING = "temporal-layering"
|
|
KEY_REPEAT_PREVIOUS_FRAME_AFTER = "repeat-previous-frame-after"
|
|
KEY_PRIORITY = "priority"
|
|
KEY_OPERATING_RATE = "operating-rate"
|
|
KEY_INTRA_REFRESH_PERIOD = "intra-refresh-period"
|
|
KEY_LATENCY = "latency"
|
|
KEY_MAX_B_FRAMES = "max-bframes"
|
|
KEY_HDR10_PLUS_INFO = "hdr10-plus-info"
|
|
|
|
MIMETYPE_AUDIO_AAC = "audio/mp4a-latm"
|
|
MIMETYPE_AUDIO_AMR_NB = "audio/3gpp"
|
|
MIMETYPE_AUDIO_AMR_WB = "audio/amr-wb"
|
|
MIMETYPE_AUDIO_FLAC = "audio/flac"
|
|
MIMETYPE_AUDIO_MPEG = "audio/mpeg"
|
|
MIMETYPE_AUDIO_OPUS = "audio/opus"
|
|
MIMETYPE_AUDIO_RAW = "audio/raw"
|
|
MIMETYPE_AUDIO_VORBIS = "audio/vorbis"
|
|
MIMETYPE_VIDEO_AV1 = "video/av01"
|
|
MIMETYPE_VIDEO_AVC = "video/avc"
|
|
MIMETYPE_VIDEO_DOLBY_VISION = "video/dolby-vision"
|
|
MIMETYPE_VIDEO_H263 = "video/3gpp"
|
|
MIMETYPE_VIDEO_HEVC = "video/hevc"
|
|
MIMETYPE_VIDEO_MPEG2 = "video/mpeg2"
|
|
MIMETYPE_VIDEO_MPEG4 = "video/mp4v-es"
|
|
MIMETYPE_VIDEO_VP8 = "video/x-vnd.on2.vp8"
|
|
MIMETYPE_VIDEO_VP9 = "video/x-vnd.on2.vp9"
|
|
MIMETYPE_VIDEO_RAW = "video/raw"
|
|
MIMETYPE_TEXT_CEA_608 = "text/cea-608"
|
|
MIMETYPE_TEXT_CEA_708 = "text/cea-708"
|
|
MIMETYPE_TEXT_VTT = "text/vtt"
|
|
MIMETYPE_IMAGE_ANDROID_HEIC = "image/vnd.android.heic"
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._properties: Dict[str, Any] = {}
|
|
|
|
def setString(self, key: str, value: str):
|
|
self._properties[key] = value
|
|
|
|
def setInteger(self, key: str, value: int):
|
|
self._properties[key] = value
|
|
|
|
def setFloat(self, key: str, value: float):
|
|
self._properties[key] = value
|
|
|
|
def setByteBuffer(self, key: str, buf):
|
|
self._properties[key] = buf
|
|
|
|
def setFeatureEnabled(self, feature: str, enabled: bool):
|
|
self._properties[feature] = enabled
|
|
|
|
def getString(self, key: str) -> Optional[str]:
|
|
v = self._properties.get(key)
|
|
return str(v) if v is not None else None
|
|
|
|
def getInteger(self, key: str, defaultValue: int = 0) -> int:
|
|
v = self._properties.get(key)
|
|
return int(v) if v is not None else defaultValue
|
|
|
|
def getFloat(self, key: str, defaultValue: float = 0.0) -> float:
|
|
v = self._properties.get(key)
|
|
return float(v) if v is not None else defaultValue
|
|
|
|
def getByteBuffer(self, key: str):
|
|
return self._properties.get(key)
|
|
|
|
def containsKey(self, key: str) -> bool:
|
|
return key in self._properties
|
|
|
|
def keys(self) -> List[str]:
|
|
return list(self._properties.keys())
|
|
|
|
@staticmethod
|
|
def createVideoFormat(mime: str, width: int, height: int) -> "MediaFormat":
|
|
fmt = MediaFormat()
|
|
fmt.setString(MediaFormat.KEY_MIME, mime)
|
|
fmt.setInteger(MediaFormat.KEY_WIDTH, width)
|
|
fmt.setInteger(MediaFormat.KEY_HEIGHT, height)
|
|
return fmt
|
|
|
|
@staticmethod
|
|
def createAudioFormat(mime: str, sampleRate: int, channelCount: int) -> "MediaFormat":
|
|
fmt = MediaFormat()
|
|
fmt.setString(MediaFormat.KEY_MIME, mime)
|
|
fmt.setInteger(MediaFormat.KEY_SAMPLE_RATE, sampleRate)
|
|
fmt.setInteger(MediaFormat.KEY_CHANNEL_COUNT, channelCount)
|
|
return fmt
|
|
|
|
@staticmethod
|
|
def createSubtitleFormat(mime: str, language: str) -> "MediaFormat":
|
|
fmt = MediaFormat()
|
|
fmt.setString(MediaFormat.KEY_MIME, mime)
|
|
fmt.setString(MediaFormat.KEY_LANGUAGE, language)
|
|
return fmt
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaMuxer
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaMuxer(Stub):
|
|
"""MediaMuxer — 媒体混合器"""
|
|
|
|
OUTPUT_FORMAT_MPEG_4 = 0
|
|
OUTPUT_FORMAT_WEBM = 1
|
|
OUTPUT_FORMAT_THREE_GPP = 2
|
|
OUTPUT_FORMAT_HEIF = 3
|
|
OUTPUT_FORMAT_OGG = 4
|
|
|
|
def __init__(self, path: str, format: int):
|
|
super().__init__()
|
|
self._path = path
|
|
self._format = format
|
|
self._started = False
|
|
self._tracks: Dict[int, MediaFormat] = {}
|
|
self._next_track = 0
|
|
|
|
def addTrack(self, format: MediaFormat) -> int:
|
|
idx = self._next_track
|
|
self._next_track += 1
|
|
self._tracks[idx] = format
|
|
return idx
|
|
|
|
def start(self):
|
|
self._started = True
|
|
|
|
def stop(self):
|
|
self._started = False
|
|
|
|
def release(self):
|
|
self._started = False
|
|
self._tracks.clear()
|
|
|
|
def writeSampleData(self, trackIndex: int, byteBuf, bufferInfo: MediaCodecBufferInfo):
|
|
pass
|
|
|
|
def setOrientationHint(self, degrees: int):
|
|
pass
|
|
|
|
def setLocation(self, latitude: float, longitude: float):
|
|
pass
|
|
|
|
def getTrackFormat(self, index: int) -> Optional[MediaFormat]:
|
|
return self._tracks.get(index)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaExtractor
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaExtractor(Stub):
|
|
"""MediaExtractor — 媒体提取器"""
|
|
|
|
SEEK_TO_NEXT_SYNC = 0
|
|
SEEK_TO_PREVIOUS_SYNC = 1
|
|
SEEK_TO_CLOSEST_SYNC = 2
|
|
SEEK_TO_CLOSEST_SYNC = 2
|
|
SAMPLE_FLAG_SYNC = 1
|
|
SAMPLE_FLAG_ENCRYPTED = 2
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._tracks: List[MediaFormat] = []
|
|
self._selected_tracks: Set[int] = set()
|
|
self._current_track = 0
|
|
|
|
def setDataSource(self, path: str):
|
|
pass
|
|
|
|
def setDataSource(self, context, uri, headers: Dict[str, str] = None):
|
|
pass
|
|
|
|
def getTrackCount(self) -> int:
|
|
return len(self._tracks)
|
|
|
|
def getTrackFormat(self, index: int) -> Optional[MediaFormat]:
|
|
if 0 <= index < len(self._tracks):
|
|
return self._tracks[index]
|
|
return None
|
|
|
|
def selectTrack(self, index: int):
|
|
self._selected_tracks.add(index)
|
|
|
|
def unselectTrack(self, index: int):
|
|
self._selected_tracks.discard(index)
|
|
|
|
def readSampleData(self, byteBuf, offset: int) -> int:
|
|
return 0
|
|
|
|
def getSampleTrackIndex(self) -> int:
|
|
return 0
|
|
|
|
def getSampleTime(self) -> int:
|
|
return 0
|
|
|
|
def advance(self) -> bool:
|
|
return False
|
|
|
|
def release(self):
|
|
self._tracks.clear()
|
|
self._selected_tracks.clear()
|
|
|
|
def seekTo(self, timeUs: int, mode: int):
|
|
pass
|
|
|
|
def getSampleFlags(self) -> int:
|
|
return 0
|
|
|
|
def getCachedDuration(self) -> int:
|
|
return 0
|
|
|
|
def hasCachedReachedEndOfStream(self) -> bool:
|
|
return False
|
|
|
|
def getSampleSize(self) -> int:
|
|
return 0
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# FaceDetector / FaceDetectorFace
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class FaceDetectorFace(Stub):
|
|
"""FaceDetector.Face — 检测到的人脸"""
|
|
|
|
CONFIDENCE_THRESHOLD = 0.4
|
|
EULER_X = 0
|
|
EULER_Y = 1
|
|
EULER_Z = 2
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._confidence = 0.0
|
|
self._mid_point_x = 0.0
|
|
self._mid_point_y = 0.0
|
|
self._eyes_distance = 0.0
|
|
self._pose_euler_x = 0.0
|
|
self._pose_euler_y = 0.0
|
|
self._pose_euler_z = 0.0
|
|
|
|
def confidence(self) -> float:
|
|
return self._confidence
|
|
|
|
def getMidPoint(self, point: PointF):
|
|
point.set(self._mid_point_x, self._mid_point_y)
|
|
|
|
def eyesDistance(self) -> float:
|
|
return self._eyes_distance
|
|
|
|
def pose(self, euler: int) -> float:
|
|
if euler == FaceDetectorFace.EULER_X:
|
|
return self._pose_euler_x
|
|
elif euler == FaceDetectorFace.EULER_Y:
|
|
return self._pose_euler_y
|
|
elif euler == FaceDetectorFace.EULER_Z:
|
|
return self._pose_euler_z
|
|
return 0.0
|
|
|
|
|
|
class FaceDetector(Stub):
|
|
"""FaceDetector — 人脸检测器"""
|
|
|
|
def __init__(self, width: int, height: int, maxFaces: int):
|
|
super().__init__()
|
|
self._width = width
|
|
self._height = height
|
|
self._max_faces = maxFaces
|
|
|
|
def findFaces(self, bitmap: "Bitmap", faces: List[FaceDetectorFace]) -> int:
|
|
return 0
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# ThumbnailUtils
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class ThumbnailUtils(Stub):
|
|
"""ThumbnailUtils — 缩略图工具"""
|
|
|
|
OPTIONS_RECYCLE_INPUT = 256
|
|
|
|
@staticmethod
|
|
def createVideoThumbnail(filePath: str, kind: int) -> Optional["Bitmap"]:
|
|
if HAS_PIL:
|
|
from PIL import Image
|
|
from .graphics import Bitmap
|
|
img = Image.new("RGB", (320, 240), (32, 32, 32))
|
|
bm = Bitmap(320, 240, Bitmap.Config.ARGB_8888)
|
|
bm._image = img
|
|
return bm
|
|
return None
|
|
|
|
@staticmethod
|
|
def createImageThumbnail(filePath: str, kind: int) -> Optional["Bitmap"]:
|
|
resolved = resolve_android_path(filePath)
|
|
if not os.path.exists(resolved):
|
|
return None
|
|
if HAS_PIL:
|
|
from PIL import Image
|
|
from .graphics import Bitmap
|
|
img = Image.open(resolved)
|
|
img.thumbnail((320, 240), Image.LANCZOS)
|
|
bm = Bitmap(img.width, img.height, Bitmap.Config.ARGB_8888)
|
|
if img.mode != "RGBA":
|
|
img = img.convert("RGBA")
|
|
bm._image = img
|
|
return bm
|
|
return None
|
|
|
|
@staticmethod
|
|
def extractThumbnail(source: "Bitmap", width: int, height: int,
|
|
options: int = 0) -> Optional["Bitmap"]:
|
|
if HAS_PIL and hasattr(source, "_image") and source._image:
|
|
from PIL import Image
|
|
img = source._image.copy()
|
|
img.thumbnail((width, height), Image.LANCZOS)
|
|
bm = Bitmap(img.width, img.height, source._config)
|
|
bm._image = img
|
|
if options & ThumbnailUtils.OPTIONS_RECYCLE_INPUT:
|
|
source.recycle()
|
|
return bm
|
|
return Bitmap.createScaledBitmap(source, width, height)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaScannerConnection / MediaScannerConnectionClient
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaScannerConnectionClient(Stub):
|
|
"""MediaScannerConnection.MediaScannerConnectionClient — 扫描客户端接口"""
|
|
|
|
def onMediaScannerConnected(self):
|
|
pass
|
|
|
|
def onScanCompleted(self, path: str, uri):
|
|
pass
|
|
|
|
|
|
class MediaScannerConnection(Stub):
|
|
"""MediaScannerConnection — 媒体扫描连接"""
|
|
|
|
def __init__(self, context, client: MediaScannerConnectionClient = None):
|
|
super().__init__()
|
|
self._client = client
|
|
self._connected = False
|
|
|
|
def connect(self):
|
|
self._connected = True
|
|
if self._client:
|
|
self._client.onMediaScannerConnected()
|
|
|
|
def disconnect(self):
|
|
self._connected = False
|
|
|
|
def isConnected(self) -> bool:
|
|
return self._connected
|
|
|
|
def scanFile(self, path: str, mimeType: str):
|
|
if self._client:
|
|
self._client.onScanCompleted(path, None)
|
|
|
|
@staticmethod
|
|
def scanFile(context, paths: List[str], mimeTypes: List[str],
|
|
callback: MediaScannerConnectionClient):
|
|
for path in paths:
|
|
if callback:
|
|
callback.onScanCompleted(path, None)
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaActionSound
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaActionSound(Stub):
|
|
"""MediaActionSound — 媒体操作音效"""
|
|
|
|
SHUTTER_CLICK = 0
|
|
FOCUS_COMPLETE = 1
|
|
START_VIDEO_RECORDING = 2
|
|
STOP_VIDEO_RECORDING = 3
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
|
|
def play(self, soundName: int):
|
|
pass
|
|
|
|
def load(self, soundName: int):
|
|
pass
|
|
|
|
def release(self):
|
|
pass
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# ToneGenerator
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class ToneGenerator(Stub):
|
|
"""ToneGenerator — 音调发生器"""
|
|
|
|
TONE_DTMF_0 = 0
|
|
TONE_DTMF_1 = 1
|
|
TONE_DTMF_2 = 2
|
|
TONE_DTMF_3 = 3
|
|
TONE_DTMF_4 = 4
|
|
TONE_DTMF_5 = 5
|
|
TONE_DTMF_6 = 6
|
|
TONE_DTMF_7 = 7
|
|
TONE_DTMF_8 = 8
|
|
TONE_DTMF_9 = 9
|
|
TONE_DTMF_S = 10
|
|
TONE_DTMF_P = 11
|
|
TONE_DTMF_A = 12
|
|
TONE_DTMF_B = 13
|
|
TONE_DTMF_C = 14
|
|
TONE_DTMF_D = 15
|
|
TONE_SUP_CALL_WAITING = 16
|
|
TONE_SUP_RINGTONE = 17
|
|
TONE_PROP_BEEP = 24
|
|
TONE_PROP_ACK = 25
|
|
TONE_PROP_NACK = 26
|
|
TONE_PROP_PROMPT = 27
|
|
TONE_PROP_BEEP2 = 28
|
|
TONE_SUP_INTERCEPT = 29
|
|
TONE_SUP_INTERCEPT_ABBREV = 30
|
|
TONE_SUP_CONGESTION = 31
|
|
TONE_SUP_CONGESTION_ABBREV = 32
|
|
TONE_SUP_CONFIRM = 33
|
|
TONE_SUP_RADIO_ACK = 34
|
|
TONE_SUP_RADIO_NOTAVAIL = 35
|
|
TONE_SUP_ERROR = 36
|
|
TONE_SUP_CALL_WAITING = 37
|
|
TONE_SUP_RINGTONE = 38
|
|
TONE_PROP_BEEP = 39
|
|
TONE_PROP_ACK = 40
|
|
TONE_PROP_NACK = 41
|
|
TONE_PROP_PROMPT = 42
|
|
TONE_PROP_BEEP2 = 43
|
|
TONE_SUP_INTERCEPT = 44
|
|
TONE_PROP_END_EMERGENCY = 45
|
|
TONE_CDMA_PRESSHOLDKEY_LITE = 46
|
|
TONE_CDMA_ALERT_SIGNAL_RELEASE = 47
|
|
TONE_CDMA_NETWORK_USA_RINGBACK = 48
|
|
TONE_CDMA_INTERCEPT = 49
|
|
TONE_CDMA_ABBR_INTERCEPT = 50
|
|
TONE_CDMA_REORDER = 51
|
|
TONE_CDMA_ABBR_REORDER = 52
|
|
TONE_CDMA_NETWORK_BUSY = 53
|
|
TONE_CDMA_CONFIRM = 54
|
|
TONE_CDMA_ANSWER = 55
|
|
TONE_CDMA_NETWORK_CALLWAITING = 56
|
|
TONE_CDMA_PRESSHOLDKEY_LITE = 57
|
|
TONE_CDMA_ONE_MIN_BEEP = 58
|
|
TONE_CDMA_SIGNAL_OFF = 59
|
|
TONE_CDMA_SIGNAL_ON = 60
|
|
TONE_CDMA_HIGH_LITE = 61
|
|
TONE_CDMA_MED_LITE = 62
|
|
TONE_CDMA_LOW_LITE = 63
|
|
TONE_CDMA_LOW_PBX_LITE = 64
|
|
TONE_CDMA_HIGH_PBX_LITE = 65
|
|
TONE_CDMA_MED_PBX_LITE = 66
|
|
TONE_CDMA_PBX_OFF = 67
|
|
TONE_CDMA_PBX_ON = 68
|
|
TONE_CDMA_ALERT_CALL_GUARD = 69
|
|
TONE_CDMA_SOFT_ERROR_LITE = 70
|
|
TONE_CDMA_ABBR_ALERT = 71
|
|
TONE_CDMA_NETWORK_BUSY_ONE_SHOT = 72
|
|
TONE_CDMA_NETWORK_BUSY = 73
|
|
TONE_CDMA_NETWORK_BUSY_ONE_SHOT = 74
|
|
TONE_CDMA_ABBR_ALERT = 75
|
|
TONE_CDMA_SOFT_ERROR_LITE = 76
|
|
TONE_CDMA_ALERT_CALL_GUARD = 77
|
|
TONE_CDMA_LOW_PBX_LITE = 78
|
|
TONE_CDMA_MED_PBX_LITE = 79
|
|
TONE_CDMA_HIGH_PBX_LITE = 80
|
|
TONE_CDMA_PBX_OFF = 81
|
|
TONE_CDMA_PBX_ON = 82
|
|
TONE_CDMA_PRESSHOLDKEY_LITE = 83
|
|
TONE_CDMA_ONE_MIN_BEEP = 84
|
|
TONE_CDMA_SIGNAL_OFF = 85
|
|
TONE_CDMA_SIGNAL_ON = 86
|
|
TONE_CDMA_HIGH_LITE = 87
|
|
TONE_CDMA_MED_LITE = 88
|
|
TONE_CDMA_LOW_LITE = 89
|
|
TONE_CDMA_ALERT_AUTO_REDIAL = 90
|
|
TONE_CDMA_ALERT_CALL_GUARD = 91
|
|
TONE_PROP_BEEP = 92
|
|
TONE_PROP_ACK = 93
|
|
TONE_PROP_NACK = 94
|
|
TONE_PROP_PROMPT = 95
|
|
TONE_PROP_BEEP2 = 96
|
|
TONE_SUP_INTERCEPT = 97
|
|
TONE_SUP_INTERCEPT_ABBREV = 98
|
|
TONE_SUP_CONGESTION = 99
|
|
TONE_SUP_CONGESTION_ABBREV = 100
|
|
TONE_SUP_CONFIRM = 101
|
|
TONE_SUP_RADIO_ACK = 102
|
|
TONE_SUP_RADIO_NOTAVAIL = 103
|
|
TONE_SUP_ERROR = 104
|
|
TONE_SUP_CALL_WAITING = 105
|
|
TONE_SUP_RINGTONE = 106
|
|
|
|
MIN_VOLUME = 0
|
|
MAX_VOLUME = 100
|
|
|
|
def __init__(self, streamType: int, volume: int):
|
|
super().__init__()
|
|
self._stream_type = streamType
|
|
self._volume = max(0, min(100, volume))
|
|
|
|
def startTone(self, toneType: int, durationMs: int = -1):
|
|
pass
|
|
|
|
def stopTone(self):
|
|
pass
|
|
|
|
def release(self):
|
|
pass
|
|
|
|
def getAudioSessionId(self) -> int:
|
|
return 0
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# ExifInterface
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class ExifInterface(Stub):
|
|
"""ExifInterface — EXIF 接口"""
|
|
|
|
TAG_APERTURE = "FNumber"
|
|
TAG_DATETIME = "DateTime"
|
|
TAG_EXPOSURE_TIME = "ExposureTime"
|
|
TAG_FLASH = "Flash"
|
|
TAG_FOCAL_LENGTH = "FocalLength"
|
|
TAG_GPS_ALTITUDE = "GPSAltitude"
|
|
TAG_GPS_ALTITUDE_REF = "GPSAltitudeRef"
|
|
TAG_GPS_DATESTAMP = "GPSDateStamp"
|
|
TAG_GPS_LATITUDE = "GPSLatitude"
|
|
TAG_GPS_LATITUDE_REF = "GPSLatitudeRef"
|
|
TAG_GPS_LONGITUDE = "GPSLongitude"
|
|
TAG_GPS_LONGITUDE_REF = "GPSLongitudeRef"
|
|
TAG_GPS_PROCESSING_METHOD = "GPSProcessingMethod"
|
|
TAG_GPS_TIMESTAMP = "GPSTimeStamp"
|
|
TAG_IMAGE_LENGTH = "ImageLength"
|
|
TAG_IMAGE_WIDTH = "ImageWidth"
|
|
TAG_ISO = "ISOSpeedRatings"
|
|
TAG_MAKE = "Make"
|
|
TAG_MODEL = "Model"
|
|
TAG_ORIENTATION = "Orientation"
|
|
TAG_WHITE_BALANCE = "WhiteBalance"
|
|
|
|
ORIENTATION_NORMAL = 1
|
|
ORIENTATION_ROTATE_90 = 6
|
|
ORIENTATION_ROTATE_180 = 3
|
|
ORIENTATION_ROTATE_270 = 8
|
|
ORIENTATION_FLIP_HORIZONTAL = 2
|
|
ORIENTATION_FLIP_VERTICAL = 4
|
|
ORIENTATION_TRANSPOSE = 5
|
|
ORIENTATION_TRANSVERSE = 7
|
|
ORIENTATION_UNDEFINED = 0
|
|
|
|
WHITEBALANCE_AUTO = 0
|
|
WHITEBALANCE_MANUAL = 1
|
|
|
|
FLAG_NO_FLASH = 0
|
|
FLAG_FIRED = 1
|
|
FLAG_STROBE_RETURN_LIGHT_DETECTED = 2
|
|
FLAG_STROBE_RETURN_LIGHT_NOT_DETECTED = 4
|
|
FLAG_COMPULSORY_FIRE = 8
|
|
FLAG_COMPULSORY_SUPPRESSION = 16
|
|
FLAG_AUTO_MODE = 24
|
|
FLAG_AUTO_MODE_RED_EYE = 25
|
|
FLAG_NO_STROBE_FUNCTION = 32
|
|
FLAG_RED_EYE_SUPPORT = 64
|
|
|
|
def __init__(self, filename: str):
|
|
super().__init__()
|
|
self._filename = resolve_android_path(filename)
|
|
self._attributes: Dict[str, str] = {}
|
|
self._load_exif()
|
|
|
|
def _load_exif(self):
|
|
"""尝试从文件中读取 EXIF 数据"""
|
|
if not self._filename or not os.path.exists(self._filename):
|
|
return
|
|
if HAS_PIL:
|
|
from PIL import Image
|
|
from PIL import ExifTags
|
|
try:
|
|
img = Image.open(self._filename)
|
|
exif_data = img._getexif()
|
|
if exif_data:
|
|
for tag_id, value in exif_data.items():
|
|
tag_name = ExifTags.TAGS.get(tag_id, str(tag_id))
|
|
self._attributes[str(tag_name)] = str(value)
|
|
except Exception:
|
|
self._attributes[self.TAG_MAKE] = "Unknown"
|
|
self._attributes[self.TAG_MODEL] = "Android Device"
|
|
self._attributes[self.TAG_ORIENTATION] = str(self.ORIENTATION_NORMAL)
|
|
self._attributes[self.TAG_IMAGE_WIDTH] = "1920"
|
|
self._attributes[self.TAG_IMAGE_LENGTH] = "1080"
|
|
else:
|
|
self._attributes[self.TAG_MAKE] = "Unknown"
|
|
self._attributes[self.TAG_MODEL] = "Android Device"
|
|
self._attributes[self.TAG_ORIENTATION] = str(self.ORIENTATION_NORMAL)
|
|
|
|
def getAttribute(self, tag: str) -> Optional[str]:
|
|
return self._attributes.get(tag)
|
|
|
|
def getAttributeInt(self, tag: str, defaultValue: int) -> int:
|
|
v = self._attributes.get(tag)
|
|
if v is not None:
|
|
try:
|
|
return int(v)
|
|
except (ValueError, TypeError):
|
|
return defaultValue
|
|
return defaultValue
|
|
|
|
def getAttributeDouble(self, tag: str, defaultValue: float) -> float:
|
|
v = self._attributes.get(tag)
|
|
if v is not None:
|
|
try:
|
|
# Handle rational strings like "1/100"
|
|
if "/" in str(v):
|
|
num, den = str(v).split("/")
|
|
return float(num) / float(den)
|
|
return float(v)
|
|
except (ValueError, TypeError, ZeroDivisionError):
|
|
return defaultValue
|
|
return defaultValue
|
|
|
|
def setAttribute(self, tag: str, value: str):
|
|
self._attributes[tag] = value
|
|
|
|
def saveAttributes(self):
|
|
if not self._filename or not HAS_PIL:
|
|
return
|
|
from PIL import Image
|
|
try:
|
|
img = Image.open(self._filename)
|
|
exif_bytes = img.info.get("exif", b"")
|
|
img.save(self._filename, exif=exif_bytes)
|
|
except Exception:
|
|
pass
|
|
|
|
def hasThumbnail(self) -> bool:
|
|
return False
|
|
|
|
def getThumbnail(self) -> Optional[bytes]:
|
|
return None
|
|
|
|
def getThumbnailRange(self) -> Optional[Tuple[int, int]]:
|
|
return None
|
|
|
|
def getAttributeRange(self, tag: str) -> Optional[Tuple[int, int]]:
|
|
return None
|
|
|
|
def setIsUri(self, isUri: bool):
|
|
pass
|
|
|
|
def flipHorizontally(self):
|
|
pass
|
|
|
|
def flipVertically(self):
|
|
pass
|
|
|
|
def rotate(self, degree: int):
|
|
pass
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# CamcorderProfile
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class CamcorderProfile(Stub):
|
|
"""CamcorderProfile — 摄像机配置"""
|
|
|
|
QUALITY_LOW = 0
|
|
QUALITY_HIGH = 1
|
|
QUALITY_QCIF = 2
|
|
QUALITY_CIF = 3
|
|
QUALITY_480P = 4
|
|
QUALITY_720P = 5
|
|
QUALITY_1080P = 6
|
|
QUALITY_QVGA = 7
|
|
QUALITY_2160P = 8
|
|
QUALITY_VGA = 9
|
|
QUALITY_4KDCI = 10
|
|
QUALITY_QHD = 11
|
|
QUALITY_2K = 12
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.quality = CamcorderProfile.QUALITY_HIGH
|
|
self.fileFormat = MediaRecorder.OutputFormat.MPEG_4
|
|
self.duration = 30
|
|
self.videoCodec = MediaRecorder.VideoEncoder.H_264
|
|
self.videoBitRate = 20000000
|
|
self.videoFrameRate = 30
|
|
self.videoFrameWidth = 1920
|
|
self.videoFrameHeight = 1080
|
|
self.audioCodec = MediaRecorder.AudioEncoder.AAC
|
|
self.audioBitRate = 192000
|
|
self.audioSampleRate = 44100
|
|
self.audioChannels = 2
|
|
|
|
@staticmethod
|
|
def get(cameraId: int, quality: int) -> Optional["CamcorderProfile"]:
|
|
if quality in (CamcorderProfile.QUALITY_LOW, CamcorderProfile.QUALITY_HIGH):
|
|
profile = CamcorderProfile()
|
|
profile.quality = quality
|
|
if quality == CamcorderProfile.QUALITY_LOW:
|
|
profile.videoFrameWidth = 640
|
|
profile.videoFrameHeight = 480
|
|
profile.videoBitRate = 500000
|
|
profile.audioBitRate = 96000
|
|
return profile
|
|
return None
|
|
|
|
@staticmethod
|
|
def hasProfile(cameraId: int, quality: int) -> bool:
|
|
return True
|
|
|
|
@staticmethod
|
|
def getAll(cameraId: int) -> List[int]:
|
|
return [
|
|
CamcorderProfile.QUALITY_LOW,
|
|
CamcorderProfile.QUALITY_HIGH,
|
|
CamcorderProfile.QUALITY_480P,
|
|
CamcorderProfile.QUALITY_720P,
|
|
CamcorderProfile.QUALITY_1080P,
|
|
]
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# EncoderProfiles
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class EncoderProfiles(Stub):
|
|
"""EncoderProfiles — 编码器配置 (API 31+)"""
|
|
|
|
class VideoProfile(Stub):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.codec = ""
|
|
self.bitrate = 0
|
|
self.width = 0
|
|
self.height = 0
|
|
self.frameRate = 0
|
|
self.profile = 0
|
|
self.bitDepth = 8
|
|
self.chromaSubsampling = 420
|
|
self.hdrFormat = 0
|
|
|
|
class AudioProfile(Stub):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.codec = ""
|
|
self.bitrate = 0
|
|
self.sampleRate = 0
|
|
self.channels = 0
|
|
self.profile = 0
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._default_duration = 30
|
|
self._recommended_file_format = MediaRecorder.OutputFormat.MPEG_4
|
|
self._video_profiles: List[EncoderProfiles.VideoProfile] = []
|
|
self._audio_profiles: List[EncoderProfiles.AudioProfile] = []
|
|
|
|
def getDefaultDuration(self) -> int:
|
|
return self._default_duration
|
|
|
|
def getRecommendedFileFormat(self) -> int:
|
|
return self._recommended_file_format
|
|
|
|
def getVideoProfiles(self) -> List["VideoProfile"]:
|
|
return list(self._video_profiles)
|
|
|
|
def getAudioProfiles(self) -> List["AudioProfile"]:
|
|
return list(self._audio_profiles)
|
|
|
|
@staticmethod
|
|
def get(cameraId: int, quality: int) -> Optional["EncoderProfiles"]:
|
|
profiles = EncoderProfiles()
|
|
|
|
vp = EncoderProfiles.VideoProfile()
|
|
vp.codec = MediaFormat.MIMETYPE_VIDEO_AVC
|
|
vp.bitrate = 20000000
|
|
vp.width = 1920
|
|
vp.height = 1080
|
|
vp.frameRate = 30
|
|
vp.profile = 8 # High Profile
|
|
profiles._video_profiles.append(vp)
|
|
|
|
ap = EncoderProfiles.AudioProfile()
|
|
ap.codec = MediaFormat.MIMETYPE_AUDIO_AAC
|
|
ap.bitrate = 192000
|
|
ap.sampleRate = 44100
|
|
ap.channels = 2
|
|
ap.profile = 2 # AAC LC
|
|
profiles._audio_profiles.append(ap)
|
|
|
|
return profiles
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# __all__
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
__all__ = [
|
|
"MediaPlayer",
|
|
"MediaRecorder",
|
|
"SoundPool",
|
|
"AudioManager",
|
|
"AudioTrack",
|
|
"AudioRecord",
|
|
"Ringtone",
|
|
"RingtoneManager",
|
|
"MediaMetadataRetriever",
|
|
"MediaCodec",
|
|
"MediaCodecBufferInfo",
|
|
"MediaFormat",
|
|
"MediaMuxer",
|
|
"MediaExtractor",
|
|
"FaceDetector",
|
|
"FaceDetectorFace",
|
|
"ThumbnailUtils",
|
|
"MediaScannerConnection",
|
|
"MediaScannerConnectionClient",
|
|
"AudioAttributes",
|
|
"AudioFormat",
|
|
"MediaActionSound",
|
|
"ToneGenerator",
|
|
"ExifInterface",
|
|
"CamcorderProfile",
|
|
"EncoderProfiles",
|
|
] |