""" 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 # Linux 真实实现导入 try: from .._linux_impl import IS_LINUX, LinuxAudio, HAS_AMIXER _USE_LINUX = IS_LINUX except ImportError: _USE_LINUX = False # ═══════════════════════════════════════════════════════════════ # 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: 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" 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", ]