""" Android Media 组件 — MediaPlayer, MediaRecorder, SoundPool, AudioManager 等 方法签名完全对齐 Android SDK """ import io import json import math import os import subprocess 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 # Linux 真实实现导入 try: from .._linux_impl import IS_LINUX, LinuxAudio, HAS_AMIXER _USE_LINUX = IS_LINUX except ImportError: _USE_LINUX = False # ═══════════════════════════════════════════════════════════════ # ffprobe / ffmpeg 辅助函数 # ═══════════════════════════════════════════════════════════════ def _ffprobe_metadata(path: str) -> Optional[Dict[str, Any]]: """用 ffprobe 获取媒体文件元数据,返回解析后的 JSON 字典。 失败时返回 None。""" try: result = subprocess.run( ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", path], capture_output=True, text=True, timeout=30, ) if result.returncode != 0: return None return json.loads(result.stdout) except (FileNotFoundError, subprocess.SubprocessError, json.JSONDecodeError, OSError): return None def _ffprobe_duration_ms(path: str) -> int: """用 ffprobe 获取时长(毫秒),失败返回 0。""" data = _ffprobe_metadata(path) if not data: return 0 try: fmt = data.get("format", {}) duration = fmt.get("duration") if duration is not None: return int(float(duration) * 1000) # 回退:从流中找 for stream in data.get("streams", []): d = stream.get("duration") if d is not None: return int(float(d) * 1000) except (ValueError, TypeError): pass return 0 def _ffprobe_video_size(path: str) -> Tuple[int, int]: """用 ffprobe 获取视频分辨率 (width, height),失败返回 (0, 0)。""" data = _ffprobe_metadata(path) if not data: return (0, 0) try: for stream in data.get("streams", []): if stream.get("codec_type") == "video": w = int(stream.get("width", 0)) h = int(stream.get("height", 0)) if w > 0 and h > 0: return (w, h) except (ValueError, TypeError): pass return (0, 0) def _ffmpeg_extract_frame(path: str, time_us: int = -1, width: int = 0, height: int = 0) -> Optional[Any]: """用 ffmpeg 提取指定时间点的视频帧,返回 PIL Image。 time_us < 0 表示提取第一帧。失败返回 None。""" if not HAS_PIL: return None try: from PIL import Image cmd = ["ffmpeg", "-v", "quiet"] if time_us is not None and time_us >= 0: cmd += ["-ss", str(time_us / 1_000_000.0)] cmd += ["-i", path, "-frames:v", "1", "-f", "image2pipe", "-vcodec", "png", "-"] if width > 0 and height > 0: # 在解码后缩放 cmd = ["ffmpeg", "-v", "quiet"] if time_us is not None and time_us >= 0: cmd += ["-ss", str(time_us / 1_000_000.0)] cmd += ["-i", path, "-frames:v", "1", "-vf", f"scale={width}:{height}", "-f", "image2pipe", "-vcodec", "png", "-"] result = subprocess.run(cmd, capture_output=True, timeout=30) if result.returncode != 0 or not result.stdout: return None return Image.open(io.BytesIO(result.stdout)) except (FileNotFoundError, subprocess.SubprocessError, OSError): return None except Exception: return None def _ffmpeg_extract_cover(path: str) -> Optional[bytes]: """用 ffmpeg 提取嵌入的封面图片,返回原始字节。 失败返回 None。""" try: # 尝试提取附加图片流(封面) cmd = ["ffmpeg", "-v", "quiet", "-i", path, "-map", "0:v", "-frames:v", "1", "-f", "image2pipe", "-vcodec", "png", "-"] result = subprocess.run(cmd, capture_output=True, timeout=30) if result.returncode == 0 and result.stdout: return result.stdout return None except (FileNotFoundError, subprocess.SubprocessError, OSError): return None except Exception: return None # ═══════════════════════════════════════════════════════════════ # 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._resolved_path = 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 self._released = False self._start_time = 0.0 # 用于模拟播放进度 # 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) if uri is not None else path if self._data_source: self._resolved_path = resolve_android_path(self._data_source) def _load_media_info(self): """用 ffprobe 加载时长和分辨率信息(try/except 保护)。""" if not self._resolved_path: return if not os.path.exists(self._resolved_path): return try: duration = _ffprobe_duration_ms(self._resolved_path) if duration > 0: self._duration = duration except Exception: pass try: w, h = _ffprobe_video_size(self._resolved_path) if w > 0 and h > 0: self._video_width = w self._video_height = h if self._on_video_size_changed_listener: self._on_video_size_changed_listener.onVideoSizeChanged( self, w, h) except Exception: pass def prepare(self): if self._released: return if self._resolved_path and not os.path.exists(self._resolved_path): if self._on_error_listener: self._on_error_listener.onError( self, MediaPlayer.MEDIA_ERROR_UNKNOWN, -1004) raise IOError(f"数据源文件不存在: {self._resolved_path}") self._load_media_info() self._prepared = True def prepareAsync(self): if self._released: return if self._resolved_path and not os.path.exists(self._resolved_path): if self._on_error_listener: self._on_error_listener.onError( self, MediaPlayer.MEDIA_ERROR_UNKNOWN, -1004) return self._load_media_info() self._prepared = True if self._on_prepared_listener: self._on_prepared_listener.onPrepared(self) def start(self): if self._released: return self._playing = True self._start_time = time.time() def stop(self): self._playing = False def pause(self): if self._playing: # 记录已播放的进度 elapsed = int((time.time() - self._start_time) * 1000) self._current_position = min( self._current_position + elapsed, self._duration) self._playing = False def reset(self): self._playing = False self._prepared = False self._current_position = 0 self._duration = 0 self._video_width = 0 self._video_height = 0 self._data_source = None self._resolved_path = None def release(self): self._playing = False self._prepared = False self._released = True self._data_source = None self._resolved_path = None 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 seekTo(self, msec: int): self._current_position = max(0, min(msec, self._duration)) self._start_time = time.time() if self._on_seek_complete_listener: self._on_seek_complete_listener.onSeekComplete(self) def getCurrentPosition(self) -> int: if self._playing and self._start_time > 0: elapsed = int((time.time() - self._start_time) * 1000) pos = self._current_position + elapsed if self._duration > 0 and pos >= self._duration: pos = self._duration if self._looping: self._current_position = 0 self._start_time = time.time() pos = 0 else: self._playing = False if self._on_completion_listener: self._on_completion_listener.onCompletion(self) return pos return self._current_position def getDuration(self) -> int: if self._duration == 0 and self._resolved_path: try: self._duration = _ffprobe_duration_ms(self._resolved_path) except Exception: pass 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: if self._video_width == 0 and self._resolved_path: try: w, h = _ffprobe_video_size(self._resolved_path) if w > 0: self._video_width = w self._video_height = h except Exception: pass return self._video_width def getVideoHeight(self) -> int: if self._video_height == 0 and self._resolved_path: try: w, h = _ffprobe_video_size(self._resolved_path) if h > 0: self._video_width = w self._video_height = h except Exception: pass 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 self._prepared = False self._released = False self._video_width = 0 self._video_height = 0 self._video_frame_rate = 0 self._video_encoding_bit_rate = 0 self._audio_encoding_bit_rate = 0 self._max_duration = 0 self._max_filesize = 0 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 setVideoSize(self, width: int, height: int): self._video_width = width self._video_height = height def setVideoFrameRate(self, rate: int): self._video_frame_rate = rate def setVideoEncodingBitRate(self, bit_rate: int): self._video_encoding_bit_rate = bit_rate def setAudioEncodingBitRate(self, bit_rate: int): self._audio_encoding_bit_rate = bit_rate def setMaxDuration(self, max_duration_ms: int): self._max_duration = max_duration_ms def setMaxFileSize(self, max_filesize_bytes: int): self._max_filesize = max_filesize_bytes def setPreviewDisplay(self, surface_holder): self._preview_display = surface_holder def prepare(self): if self._released: return self._prepared = True def start(self): if self._released or not self._prepared: return self._recording = True def stop(self): self._recording = False def reset(self): self._recording = False self._prepared = False 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._video_width = 0 self._video_height = 0 self._video_frame_rate = 0 self._video_encoding_bit_rate = 0 def release(self): self._recording = False self._prepared = False self._released = True self._output_file = None self._preview_display = None def getMaxAmplitude(self) -> int: if self._recording: import random return random.randint(0, 32767) 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: if _USE_LINUX: # STREAM_MUSIC 及其它流类型映射到 Linux 主音量 return LinuxAudio.get_volume() return self._stream_volumes.get(streamType, 0) def getStreamMaxVolume(self, streamType: int) -> int: if _USE_LINUX: return 100 return 15 def setStreamVolume(self, streamType: int, index: int, flags: int = 0): if _USE_LINUX: # STREAM_MUSIC 及其它流类型映射到 Linux 主音量 LinuxAudio.set_volume(index) return if streamType in self._stream_volumes: self._stream_volumes[streamType] = max(0, min(15, index)) def isMicrophoneMute(self) -> bool: """麦克风是否静音""" if _USE_LINUX: return LinuxAudio.get_mute() return False def setMicrophoneMute(self, on: bool): """设置麦克风静音""" if _USE_LINUX: LinuxAudio.set_mute(on) return # 存根: 无操作 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" # 提取帧选项 OPTION_CLOSEST = 0x01 OPTION_CLOSEST_SYNC = 0x02 OPTION_NEXT_SYNC = 0x03 OPTION_PREVIOUS_SYNC = 0x04 # ffprobe format_name 到 MIME 类型映射 _MIME_MAP = { "mov,mp4,m4a,3gp,3g2,mj2": "video/mp4", "matroska,webm": "video/x-matroska", "avi": "video/x-msvideo", "flv": "video/x-flv", "wav": "audio/wav", "mp3": "audio/mpeg", "aac": "audio/aac", "ogg": "audio/ogg", "flac": "audio/flac", "asf": "video/x-ms-asf", "mpeg": "video/mpeg", "webm": "video/webm", } def __init__(self): super().__init__() self._metadata: Dict[str, str] = {} self._resolved_path: Optional[str] = None self._ffprobe_data: Optional[Dict[str, Any]] = None def setDataSource(self, path: str, headers: Optional[Dict[str, str]] = None): """设置数据源并用 ffprobe 加载元数据。""" self._resolved_path = resolve_android_path(path) self._metadata = {} self._ffprobe_data = None if not self._resolved_path or not os.path.exists(self._resolved_path): return # 用 ffprobe 获取真实元数据(try/except 保护) try: self._ffprobe_data = _ffprobe_metadata(self._resolved_path) except Exception: self._ffprobe_data = None if self._ffprobe_data: self._parse_ffprobe_metadata() def _parse_ffprobe_metadata(self): """将 ffprobe 输出解析为 Android 风格的元数据键值对。""" if not self._ffprobe_data: return data = self._ffprobe_data fmt = data.get("format", {}) tags = fmt.get("tags", {}) streams = data.get("streams", []) # 找到视频流和音频流 video_stream = None audio_stream = None for s in streams: ct = s.get("codec_type", "") if ct == "video" and video_stream is None: video_stream = s elif ct == "audio" and audio_stream is None: audio_stream = s md = self._metadata # 时长(毫秒) duration = fmt.get("duration") if duration is None and video_stream: duration = video_stream.get("duration") if duration is None and audio_stream: duration = audio_stream.get("duration") if duration is not None: try: md[MediaMetadataRetriever.METADATA_KEY_DURATION] = str( int(float(duration) * 1000)) except (ValueError, TypeError): pass # 标题 title = tags.get("title") or tags.get("TITLE") if title: md[MediaMetadataRetriever.METADATA_KEY_TITLE] = title # 艺术家 artist = tags.get("artist") or tags.get("ARTIST") if artist: md[MediaMetadataRetriever.METADATA_KEY_ARTIST] = artist # 专辑 album = tags.get("album") or tags.get("ALBUM") if album: md[MediaMetadataRetriever.METADATA_KEY_ALBUM] = album # 作曲家 composer = tags.get("composer") or tags.get("COMPOSER") if composer: md[MediaMetadataRetriever.METADATA_KEY_COMPOSER] = composer # 作者 writer = tags.get("writer") or tags.get("WRITER") if writer: md[MediaMetadataRetriever.METADATA_KEY_WRITER] = writer # 流派 genre = tags.get("genre") or tags.get("GENRE") if genre: md[MediaMetadataRetriever.METADATA_KEY_GENRE] = genre # 日期 / 年份 date = tags.get("date") or tags.get("DATE") or tags.get("year") if date: md[MediaMetadataRetriever.METADATA_KEY_DATE] = str(date) # 尝试提取年份 year_str = str(date)[:4] if year_str.isdigit(): md[MediaMetadataRetriever.METADATA_KEY_YEAR] = year_str # 音轨号 track = tags.get("track") or tags.get("TRACK") if track: md[MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER] = str(track) # 位置 location = tags.get("location") or tags.get("LOCATION") if location: md[MediaMetadataRetriever.METADATA_KEY_LOCATION] = str(location) # MIME 类型 format_name = fmt.get("format_name", "") if format_name: mime = MediaMetadataRetriever._MIME_MAP.get( format_name, f"video/{format_name.split(',')[0]}") md[MediaMetadataRetriever.METADATA_KEY_MIMETYPE] = mime # 轨道数 if streams: md[MediaMetadataRetriever.METADATA_KEY_NUM_TRACKS] = str(len(streams)) # 比特率 bit_rate = fmt.get("bit_rate") if bit_rate: md[MediaMetadataRetriever.METADATA_KEY_BITRATE] = str(bit_rate) # 视频信息 if video_stream: w = video_stream.get("width", 0) h = video_stream.get("height", 0) if w: md[MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH] = str(w) if h: md[MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT] = str(h) md[MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO] = "yes" else: md[MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO] = "no" # 音频信息 if audio_stream: md[MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO] = "yes" else: md[MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO] = "no" def extractMetadata(self, keyCode: str) -> Optional[str]: return self._metadata.get(keyCode) def getEmbeddedPicture(self) -> Optional[bytes]: """用 ffmpeg 提取嵌入的封面图片,返回原始字节数据。 失败返回 None。""" if not self._resolved_path or not os.path.exists(self._resolved_path): return None try: return _ffmpeg_extract_cover(self._resolved_path) except Exception: return None def getFrameAtTime(self, timeUs: int = -1, option: int = 0) -> Optional["Bitmap"]: """用 ffmpeg 提取指定时间点的视频帧,返回 Bitmap(内含 PIL Image)。 timeUs < 0 表示提取第一帧。失败返回 None。""" if not self._resolved_path or not os.path.exists(self._resolved_path): return None try: img = _ffmpeg_extract_frame(self._resolved_path, timeUs) if img is not None: from .graphics import Bitmap w, h = img.size bm = Bitmap(w, h, Bitmap.Config.ARGB_8888) # 确保 RGBA 模式 if img.mode != "RGBA": img = img.convert("RGBA") bm._image = img return bm except Exception: pass return None def getScaledFrameAtTime(self, timeUs: int, option: int, width: int, height: int) -> Optional["Bitmap"]: """用 ffmpeg 提取并缩放指定时间点的视频帧。""" if not self._resolved_path or not os.path.exists(self._resolved_path): return None try: img = _ffmpeg_extract_frame(self._resolved_path, timeUs, width, height) if img is not None: from .graphics import Bitmap bm = Bitmap(width, height, Bitmap.Config.ARGB_8888) if img.mode != "RGBA": img = img.convert("RGBA") bm._image = img return bm except Exception: pass return None def release(self): self._metadata.clear() self._resolved_path = None self._ffprobe_data = None # ═══════════════════════════════════════════════════════════════ # 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._pil_image = None 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) self._pil_image = img 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): if hasattr(self, '_pil_image') and self._pil_image: from PIL import Image self._pil_image = self._pil_image.transpose(Image.FLIP_LEFT_RIGHT) self._save_from_pil() def flipVertically(self): if hasattr(self, '_pil_image') and self._pil_image: from PIL import Image self._pil_image = self._pil_image.transpose(Image.FLIP_TOP_BOTTOM) self._save_from_pil() def rotate(self, degrees): if hasattr(self, '_pil_image') and self._pil_image: from PIL import Image self._pil_image = self._pil_image.rotate(degrees, expand=True) self._save_from_pil() # 更新方向属性 self.setAttribute("Orientation", "1") # Normal after rotation def _save_from_pil(self): """从 PIL 图像保存回文件""" if hasattr(self, '_pil_image') and self._pil_image and hasattr(self, '_filename'): self._pil_image.save(self._filename) # ═══════════════════════════════════════════════════════════════ # 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", ]