Files
Android-pyApi/android_pyapi/android/media.py
T
shuiyu1123 690022cea3 feat: Linux 真实实现层 — 从存根库变为可用生产力工具
新增 _linux_impl.py (800+行):
- LinuxNetwork: 真实IP/接口/DNS/网关/MAC (ip命令+/proc/net)
- LinuxBluetooth: 真实蓝牙状态/扫描/配对/连接 (bluetoothctl)
- LinuxSensors: 真实温度/CPU频率/风扇 (/sys/class/hwmon)
- LinuxAudio: 真实音量控制/静音 (amixer)
- LinuxClipboard: 真实剪贴板读写 (xclip+tkinter回退)
- LinuxPower: 真实CPU/内存/电池 (psutil+/sys/class/power_supply)
- LinuxLocation: 真实IP地理定位 (ip-api.com)
- LinuxNotify: 真实桌面通知 (notify-send)
- LinuxDisplay: 真实屏幕尺寸/截屏/鼠标位置 (xdotool+X11)
- LinuxUIAutomation: 真实点击/输入/按键/滚动 (xdotool)
- LinuxCamera: 真实摄像头拍照 (opencv/V4L2)
- LinuxProcess: 真实进程列表/系统信息/应用管理 (psutil+/proc)
- LinuxShell: 真实命令执行 (subprocess)

改造10+核心类使用Linux真实实现:
- BluetoothAdapter/SensorManager/AudioManager
- ConnectivityManager/WifiManager/TelephonyManager
- LocationManager/PowerManager/BatteryManager/Build
- ActivityManager/Activity/Toast/Display/ClipboardManager
- NotificationManager

15项真实行为测试全部通过:
- 真实IP: 10.60.218.244
- 真实位置: 北京 39.9042, 116.407
- 真实主机名: work-41-vci-g427c
- 真实内核: 5.15.120.byteatom-ck.14
- 真实内存: 1.8GB/3.8GB
- 真实进程列表
2026-06-24 15:40:49 +00:00

2078 lines
66 KiB
Python

"""
Android Media 组件 — MediaPlayer, MediaRecorder, SoundPool, AudioManager 等
方法签名完全对齐 Android SDK
"""
import io
import math
import os
import time
import struct
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
from .._core import Stub, AndroidOnlyError, android_context, HAS_PIL, HAS_PYMUPDF, resolve_android_path
from .graphics import PointF
# 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",
]