Files
Android-pyApi/android_pyapi/android/media.py
T
shuiyu1123 fbb8263b13 feat: 第三轮深度完善 — SQLite/JSON/网络/媒体/文本 真实实现
SQLite真实数据库:
- SQLiteDatabase: execSQL/insert/insertOrThrow/replace/update/delete/query 用sqlite3
- 修复getVersion/setVersion用PRAGMA user_version
- 修复getMaximumSize/getPageSize/setMaximumSize/setPageSize用PRAGMA
- SQLiteStatement: execute/simpleQueryForLong/simpleQueryForString/execQuery
- SQLiteOpenHelper: onCreate/onUpgrade/onDowngrade回调正确触发
- 修复SQLiteCursor.moveToPosition越界处理
- 修复ContentValues值对提取bug

JSON真实实现:
- JSONObject: isNull/append/putOnce/putOpt/getNames
- JSONArray: 完整put/get/opt/length/toString

网络真实实现:
- NetworkInterface: getByName/getNetworkInterfaces/getHardwareAddress 用/sys/class/net+fcntl.ioctl
- InterfaceAddress: 新增类
- ConnectivityManager: getActiveNetwork/getAllNetworks 用LinuxNetwork
- WifiManager: getScanResults用nmcli, setWifiEnabled用nmcli/rfkill
- BluetoothAdapter: getAddress/getName 用bluetoothctl

媒体真实实现:
- MediaPlayer: prepare检查文件, getCurrentPosition实时计算, getDuration/getVideoWidth用ffprobe
- MediaRecorder: setVideoSize/setVideoFrameRate/setVideoEncodingBitRate配置存储
- MediaMetadataRetriever: extractMetadata用ffprobe, getFrameAtTime用ffmpeg, getEmbeddedPicture用ffmpeg

文本真实实现:
- TextUtils: split用re.split, concat/indexOf/lastIndexOf/urlEncode/urlDecode
- SpannableString: setSpan/getSpans/getSpanFlags/nextSpanTransition 真实实现
- SpannableStringBuilder: 修复append重载bug

Provider增强:
- Settings: putBoolean/getBoolean
- MediaStore: getContentUri/VOLUME常量
- ContactsContract: 补全常量和CommonDataKinds

Util增强:
- Log: w/e/wtf支持Throwable参数, 修复wtf重复定义bug
- TypedValue: 修复complexToDimension/complexToFloat/getFloat实现

修复:
- __init__.py: 补全NetworkInterface/InetAddress/MediaMetadataRetriever/SpannableStringBuilder导出

统计: 221模块, 3666类, 31005方法, 183529行
2026-06-25 00:40:59 +00:00

2525 lines
83 KiB
Python

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