- android/ 167个模块 (app/content/os/view/widget/graphics/media/hardware/net/telephony/provider等) - java/ 35个模块 (lang/io/util/net/nio/security/crypto/sql/beans/rmi/awt等) - javax/ 4个模块 (net/crypto/security/sql) - org/ 5个模块 (json/w3c_dom/xml_sax/xml_pull/apache_http) - dalvik/ 1个模块 (system) - ~3053个类/接口,方法签名完全对齐Android SDK - 桌面跨平台运行 (Windows/macOS/Linux) - 修复所有导入错误和Python关键字冲突
1621 lines
53 KiB
Python
1621 lines
53 KiB
Python
"""
|
|
Android media 异步组件 — MediaCodec, MediaExtractor, MediaMuxer, MediaPlayer 扩展等
|
|
方法签名完全对齐 Android SDK
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import io
|
|
import math
|
|
import time
|
|
import struct
|
|
import threading
|
|
from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union
|
|
from enum import IntEnum
|
|
|
|
from .._core import Stub, AndroidOnlyError
|
|
from ..java.lang import RuntimeException
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaCodec 常量
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
BUFFER_FLAG_KEY_FRAME = 1
|
|
BUFFER_FLAG_CODEC_CONFIG = 2
|
|
BUFFER_FLAG_END_OF_STREAM = 4
|
|
BUFFER_FLAG_PARTIAL_FRAME = 8
|
|
BUFFER_FLAG_SYNC_FRAME = 1
|
|
|
|
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
|
|
|
|
COLOR_FormatUnsupported = 0x7FFFFFFF
|
|
COLOR_FormatYUV420Planar = 19
|
|
COLOR_FormatYUV420SemiPlanar = 21
|
|
COLOR_FormatYUV420PackedPlanar = 20
|
|
COLOR_FormatYUV420PackedSemiPlanar = 22
|
|
COLOR_FormatYUV422Planar = 16
|
|
COLOR_FormatYUV422SemiPlanar = 41
|
|
COLOR_FormatYUV422PackedPlanar = 17
|
|
COLOR_FormatYUV422PackedSemiPlanar = 18
|
|
COLOR_FormatYUV444Planar = 31
|
|
COLOR_FormatYUV444SemiPlanar = 42
|
|
COLOR_FormatYUV444PackedPlanar = 32
|
|
COLOR_FormatYUV444PackedSemiPlanar = 33
|
|
COLOR_TI_FormatYUV420PackedSemiPlanar = 0x7F000100
|
|
COLOR_FormatSurface = 0x7F000789
|
|
COLOR_FormatYUV420Flexible = 0x7F420888
|
|
COLOR_Format24BitBGR = 5
|
|
COLOR_Format32BitARGB8888 = 9
|
|
COLOR_Format32BitBGRA8888 = 10
|
|
|
|
BITRATE_MODE_VBR = 0
|
|
BITRATE_MODE_CBR = 1
|
|
BITRATE_MODE_CQ = 2
|
|
|
|
CODEC_LEVEL_UNSPECIFIED = 0
|
|
CODEC_PROFILE_UNSPECIFIED = 0
|
|
|
|
AUDIO_CHANNEL_OUT_MONO = 4
|
|
AUDIO_CHANNEL_OUT_STEREO = 12
|
|
AUDIO_CHANNEL_OUT_5POINT1 = 252
|
|
AUDIO_CHANNEL_OUT_7POINT1 = 6396
|
|
|
|
AUDIO_CHANNEL_IN_MONO = 16
|
|
AUDIO_CHANNEL_IN_STEREO = 12
|
|
|
|
PCM_ENCODING_PCM_16BIT = 2
|
|
PCM_ENCODING_PCM_8BIT = 3
|
|
PCM_ENCODING_PCM_FLOAT = 4
|
|
|
|
AUDIO_OUTPUT_FLAG_NONE = 0
|
|
AUDIO_OUTPUT_FLAG_DIRECT = 1
|
|
AUDIO_OUTPUT_FLAG_HW_AV_SYNC = 8
|
|
|
|
# MediaFormat KEY 常量
|
|
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_MAX_INPUT_SIZE = "max-input-size"
|
|
KEY_DURATION = "durationUs"
|
|
KEY_IS_ADTS = "is-adts"
|
|
KEY_CHANNEL_MASK = "channel-mask"
|
|
KEY_ENCODING = "encoding"
|
|
KEY_PCM_ENCODING = "pcm-encoding"
|
|
KEY_PROFILE = "profile"
|
|
KEY_LEVEL = "level"
|
|
KEY_LANGUAGE = "language"
|
|
KEY_PRIORITY = "priority"
|
|
KEY_OPERATING_RATE = "operating-rate"
|
|
KEY_INTRA_REFRESH_PERIOD = "intra-refresh-period"
|
|
KEY_LATENCY = "latency"
|
|
KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder"
|
|
KEY_MAX_PTS_GAP_TO_ENCODER = "max-pts-gap-to-encoder"
|
|
KEY_PREPEND_HEADER_TO_SYNC_FRAMES = "prepend-sps-pps-to-idr-frames"
|
|
KEY_REPEAT_PREVIOUS_FRAME_AFTER = "repeat-previous-frame-after"
|
|
KEY_TEMPORAL_LAYERING = "temporal-layering"
|
|
KEY_HDR10_PLUS_INFO = "hdr10-plus-info"
|
|
KEY_PIXEL_ASPECT_RATIO_WIDTH = "pixel-aspect-ratio-width"
|
|
KEY_PIXEL_ASPECT_RATIO_HEIGHT = "pixel-aspect-ratio-height"
|
|
KEY_AAC_DRC_BOOST_FACTOR = "aac-drc-boost-factor"
|
|
KEY_AAC_DRC_ATTENUATION_FACTOR = "aac-drc-attenuation-factor"
|
|
KEY_AAC_DRC_HEAVY_COMPRESSION = "aac-drc-heavy-compression"
|
|
KEY_AAC_DRC_TARGET_REFERENCE_LEVEL = "aac-target-ref-level"
|
|
KEY_AAC_ENCODED_TARGET_LEVEL = "aac-encoded-target-level"
|
|
KEY_AAC_MAX_OUTPUT_CHANNEL_COUNT = "aac-max-output-channel-count"
|
|
KEY_AAC_PROFILE = "aac-profile"
|
|
KEY_AAC_SBR_MODE = "aac-sbr-mode"
|
|
KEY_ENCODER_DELAY_FRAME_COUNT = "encoder-delay"
|
|
KEY_ENCODER_PADDING_FRAME_COUNT = "encoder-padding"
|
|
KEY_FLAC_COMPRESSION_LEVEL = "flac-compression-level"
|
|
KEY_MAX_BIT_RATE = "max-bitrate"
|
|
KEY_QUALITY = "quality"
|
|
KEY_SAMPLE_MIME_TYPE = "sample-mime-type"
|
|
KEY_TRACK_ID = "track-id"
|
|
KEY_IS_AUTOSELECT = "is-autoselect"
|
|
KEY_IS_DEFAULT = "is-default"
|
|
KEY_IS_FORCED_SUBTITLE = "is-forced-subtitle"
|
|
KEY_ROTATION = "rotation"
|
|
KEY_CAPTURE_RATE = "capture-rate"
|
|
KEY_FEATURE_ = "feature-"
|
|
KEY_HDR_STATIC_INFO = "hdr-static-info"
|
|
KEY_HDR10_PLUS_INFO = "hdr10-plus-info"
|
|
KEY_COLOR_TRANSFER_REQUEST = "color-transfer-request"
|
|
KEY_OFFSET_TIME = "offset-time"
|
|
KEY_PCM_ENCODING = "pcm-encoding"
|
|
KEY_SAMPLE_RATE = "sample-rate"
|
|
KEY_SUPPORTED_PCM_ENCODINGS = "supported-pcm-encodings"
|
|
KEY_SUPPORTED_PCM_USAGES = "supported-pcm-usages"
|
|
KEY_TRANSFER_TIMEOUT = "transfer-timeout"
|
|
KEY_VIDEO_QP_B_MAX = "video-qp-b-max"
|
|
KEY_VIDEO_QP_B_MIN = "video-qp-b-min"
|
|
KEY_VIDEO_QP_I_MAX = "video-qp-i-max"
|
|
KEY_VIDEO_QP_I_MIN = "video-qp-i-min"
|
|
KEY_VIDEO_QP_P_MAX = "video-qp-p-max"
|
|
KEY_VIDEO_QP_P_MIN = "video-qp-p-min"
|
|
|
|
# MIME 类型
|
|
MIMETYPE_VIDEO_AV1 = "video/av01"
|
|
MIMETYPE_VIDEO_VP9 = "video/x-vnd.on2.vp9"
|
|
MIMETYPE_VIDEO_VP8 = "video/x-vnd.on2.vp8"
|
|
MIMETYPE_VIDEO_H265 = "video/hevc"
|
|
MIMETYPE_VIDEO_H264 = "video/avc"
|
|
MIMETYPE_VIDEO_MPEG2 = "video/mpeg2"
|
|
MIMETYPE_VIDEO_MPEG4 = "video/mp4v-es"
|
|
MIMETYPE_VIDEO_VC1 = "video/wvc1"
|
|
MIMETYPE_VIDEO_DIVX = "video/divx"
|
|
MIMETYPE_VIDEO_DIVX3 = "video/divx3"
|
|
MIMETYPE_VIDEO_FLV = "video/flv1"
|
|
MIMETYPE_VIDEO_H263 = "video/3gpp"
|
|
MIMETYPE_VIDEO_RAW = "video/raw"
|
|
MIMETYPE_VIDEO_DOLBY_VISION = "video/dolby-vision"
|
|
MIMETYPE_VIDEO_SCRAMBLED = "video/scrambled"
|
|
MIMETYPE_AUDIO_AAC = "audio/mp4a-latm"
|
|
MIMETYPE_AUDIO_MP3 = "audio/mpeg"
|
|
MIMETYPE_AUDIO_MPEG = "audio/mpeg"
|
|
MIMETYPE_AUDIO_AMR_NB = "audio/3gpp"
|
|
MIMETYPE_AUDIO_AMR_WB = "audio/amr-wb"
|
|
MIMETYPE_AUDIO_FLAC = "audio/flac"
|
|
MIMETYPE_AUDIO_VORBIS = "audio/vorbis"
|
|
MIMETYPE_AUDIO_OPUS = "audio/opus"
|
|
MIMETYPE_AUDIO_RAW = "audio/raw"
|
|
MIMETYPE_AUDIO_ALAC = "audio/alac"
|
|
MIMETYPE_AUDIO_AC3 = "audio/ac3"
|
|
MIMETYPE_AUDIO_EAC3 = "audio/eac3"
|
|
MIMETYPE_AUDIO_WMA = "audio/x-ms-wma"
|
|
MIMETYPE_AUDIO_MSGSM = "audio/gsm"
|
|
MIMETYPE_TEXT_VTT = "text/vtt"
|
|
MIMETYPE_TEXT_CEA_608 = "text/cea-608"
|
|
MIMETYPE_TEXT_CEA_708 = "text/cea-708"
|
|
MIMETYPE_SUBTITLE_SRT = "application/x-subrip"
|
|
MIMETYPE_SUBTITLE_TTML = "application/ttml+xml"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaCodec.BufferInfo
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class CodecBufferInfo(Stub):
|
|
"""MediaCodec.BufferInfo — 编解码缓冲区信息"""
|
|
|
|
BUFFER_FLAG_KEY_FRAME = BUFFER_FLAG_KEY_FRAME
|
|
BUFFER_FLAG_CODEC_CONFIG = BUFFER_FLAG_CODEC_CONFIG
|
|
BUFFER_FLAG_END_OF_STREAM = BUFFER_FLAG_END_OF_STREAM
|
|
BUFFER_FLAG_PARTIAL_FRAME = BUFFER_FLAG_PARTIAL_FRAME
|
|
BUFFER_FLAG_SYNC_FRAME = BUFFER_FLAG_SYNC_FRAME
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.offset: int = 0
|
|
self.size: int = 0
|
|
self.presentationTimeUs: int = 0
|
|
self.flags: int = 0
|
|
|
|
def set(self, new_offset: int, new_size: int, new_time_us: int, new_flags: int) -> None:
|
|
self.offset = new_offset
|
|
self.size = new_size
|
|
self.presentationTimeUs = new_time_us
|
|
self.flags = new_flags
|
|
|
|
def duplicate(self) -> "CodecBufferInfo":
|
|
info = CodecBufferInfo()
|
|
info.offset = self.offset
|
|
info.size = self.size
|
|
info.presentationTimeUs = self.presentationTimeUs
|
|
info.flags = self.flags
|
|
return info
|
|
|
|
def toString(self) -> str:
|
|
return (f"BufferInfo(offset={self.offset}, size={self.size}, "
|
|
f"timeUs={self.presentationTimeUs}, flags={self.flags})")
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaCodec.CryptoInfo
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class CryptoInfo(Stub):
|
|
"""CryptoInfo — 加密信息 (MediaCodec)"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.numSubSamples = 0
|
|
self.numBytesOfClearData: List[int] = []
|
|
self.numBytesOfEncryptedData: List[int] = []
|
|
self.key: Optional[bytes] = None
|
|
self.iv: Optional[bytes] = None
|
|
self.mode: int = 0
|
|
|
|
class Pattern(Stub):
|
|
"""Pattern — 加密模式"""
|
|
|
|
def __init__(self, encrypt_blocks: int = 0, skip_blocks: int = 0):
|
|
super().__init__()
|
|
self.encryptBlocks = encrypt_blocks
|
|
self.skipBlocks = skip_blocks
|
|
|
|
def toString(self) -> str:
|
|
return f"Pattern(encrypt={self.encryptBlocks}, skip={self.skipBlocks})"
|
|
|
|
def hashCode(self) -> int:
|
|
return hash((self.encryptBlocks, self.skipBlocks)) & 0x7FFFFFFF
|
|
|
|
def equals(self, other) -> bool:
|
|
if not isinstance(other, CryptoInfo.Pattern):
|
|
return False
|
|
return (self.encryptBlocks == other.encryptBlocks
|
|
and self.skipBlocks == other.skipBlocks)
|
|
|
|
def setPattern(self, pattern: "CryptoInfo.Pattern"):
|
|
self._pattern = pattern
|
|
|
|
def getPattern(self) -> Optional["CryptoInfo.Pattern"]:
|
|
return getattr(self, '_pattern', None)
|
|
|
|
def toString(self) -> str:
|
|
return (f"CryptoInfo(subsamples={self.numSubSamples}, "
|
|
f"mode={self.mode})")
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaCodec.CodecException
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class CodecException(RuntimeException):
|
|
"""MediaCodec.CodecException — 编解码器异常"""
|
|
|
|
ERROR_RECOVERABLE = 1
|
|
ERROR_INSUFFICIENT_RESOURCE = 2
|
|
|
|
def __init__(self, error_code: int = 0, diagnostic_info: str = "",
|
|
is_recoverable: bool = False, is_transient: bool = False):
|
|
super().__init__(f"CodecException: {diagnostic_info}")
|
|
self._error_code = error_code
|
|
self._diagnostic_info = diagnostic_info
|
|
self._is_recoverable = is_recoverable
|
|
self._is_transient = is_transient
|
|
|
|
def getErrorCode(self) -> int:
|
|
return self._error_code
|
|
|
|
def getDiagnosticInfo(self) -> str:
|
|
return self._diagnostic_info
|
|
|
|
def isRecoverable(self) -> bool:
|
|
return self._is_recoverable
|
|
|
|
def isTransient(self) -> bool:
|
|
return self._is_transient
|
|
|
|
def toString(self) -> str:
|
|
return (f"CodecException(errorCode={self._error_code}, "
|
|
f"recoverable={self._is_recoverable}, "
|
|
f"transient={self._is_transient})")
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaCodec
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaCodec(Stub):
|
|
"""MediaCodec — 媒体编解码器"""
|
|
|
|
# 内部类引用
|
|
BufferInfo = CodecBufferInfo
|
|
CryptoInfo = CryptoInfo
|
|
CodecException = CodecException
|
|
|
|
BUFFER_FLAG_KEY_FRAME = BUFFER_FLAG_KEY_FRAME
|
|
BUFFER_FLAG_CODEC_CONFIG = BUFFER_FLAG_CODEC_CONFIG
|
|
BUFFER_FLAG_END_OF_STREAM = BUFFER_FLAG_END_OF_STREAM
|
|
BUFFER_FLAG_PARTIAL_FRAME = BUFFER_FLAG_PARTIAL_FRAME
|
|
BUFFER_FLAG_SYNC_FRAME = BUFFER_FLAG_SYNC_FRAME
|
|
|
|
CRYPTO_MODE_UNENCRYPTED = CRYPTO_MODE_UNENCRYPTED
|
|
CRYPTO_MODE_AES_CTR = CRYPTO_MODE_AES_CTR
|
|
CRYPTO_MODE_AES_WV = CRYPTO_MODE_AES_WV
|
|
CRYPTO_MODE_AES_CBC = CRYPTO_MODE_AES_CBC
|
|
|
|
INFO_TRY_AGAIN_LATER = INFO_TRY_AGAIN_LATER
|
|
INFO_OUTPUT_FORMAT_CHANGED = INFO_OUTPUT_FORMAT_CHANGED
|
|
INFO_OUTPUT_BUFFERS_CHANGED = INFO_OUTPUT_BUFFERS_CHANGED
|
|
|
|
CONFIGURE_FLAG_ENCODE = 1
|
|
|
|
def __init__(self, name: str = ""):
|
|
super().__init__()
|
|
self._name = name
|
|
self._configured = False
|
|
self._started = False
|
|
self._format: Optional["MediaFormat"] = None
|
|
self._input_buffers: Dict[int, Optional[bytes]] = {}
|
|
self._output_buffers: Dict[int, Optional[bytes]] = {}
|
|
self._input_buffer_count = 0
|
|
self._output_buffer_count = 0
|
|
self._input_lock = threading.Lock()
|
|
self._output_lock = threading.Lock()
|
|
self._callback: Optional[MediaCodec.Callback] = None
|
|
self._handler: Optional[Any] = None
|
|
|
|
@staticmethod
|
|
def createByCodecName(name: str) -> "MediaCodec":
|
|
return MediaCodec(name)
|
|
|
|
@staticmethod
|
|
def createDecoderByType(type_str: str) -> "MediaCodec":
|
|
return MediaCodec(f"decoder.{type_str}")
|
|
|
|
@staticmethod
|
|
def createEncoderByType(type_str: str) -> "MediaCodec":
|
|
return MediaCodec(f"encoder.{type_str}")
|
|
|
|
def configure(self, format: "MediaFormat", surface: Any = None,
|
|
crypto: Any = None, flags: int = 0) -> None:
|
|
self._format = format
|
|
self._configured = True
|
|
self._input_buffers.clear()
|
|
self._output_buffers.clear()
|
|
|
|
def start(self) -> None:
|
|
if not self._configured:
|
|
raise RuntimeException("MediaCodec 未配置")
|
|
self._started = True
|
|
self._input_buffer_count = 8
|
|
self._output_buffer_count = 8
|
|
|
|
def stop(self) -> None:
|
|
self._started = False
|
|
|
|
def flush(self) -> None:
|
|
if not self._started:
|
|
return
|
|
with self._input_lock:
|
|
self._input_buffers.clear()
|
|
with self._output_lock:
|
|
self._output_buffers.clear()
|
|
|
|
def release(self) -> None:
|
|
self.stop()
|
|
self._configured = False
|
|
self._format = None
|
|
|
|
def getInputBuffers(self) -> List[Optional[bytes]]:
|
|
return [self._input_buffers.get(i) for i in range(self._input_buffer_count)]
|
|
|
|
def getOutputBuffers(self) -> List[Optional[bytes]]:
|
|
return [self._output_buffers.get(i) for i in range(self._output_buffer_count)]
|
|
|
|
def getInputBuffer(self, idx: int) -> Optional[bytes]:
|
|
with self._input_lock:
|
|
if idx not in self._input_buffers:
|
|
self._input_buffers[idx] = bytearray(1024 * 1024)
|
|
return self._input_buffers[idx]
|
|
|
|
def getOutputBuffer(self, idx: int) -> Optional[bytes]:
|
|
with self._output_lock:
|
|
return self._output_buffers.get(idx)
|
|
|
|
def queueInputBuffer(self, index: int, offset: int, size: int,
|
|
presentation_time_us: int, flags: int) -> None:
|
|
pass
|
|
|
|
def queueSecureInputBuffer(self, index: int, offset: int,
|
|
info: CryptoInfo,
|
|
presentation_time_us: int,
|
|
flags: int) -> None:
|
|
pass
|
|
|
|
def dequeueInputBuffer(self, timeout_us: int) -> int:
|
|
return 0
|
|
|
|
def dequeueOutputBuffer(self, info: CodecBufferInfo, timeout_us: int) -> int:
|
|
return INFO_TRY_AGAIN_LATER
|
|
|
|
def releaseOutputBuffer(self, index: int, render: bool) -> None:
|
|
pass
|
|
|
|
def getOutputFormat(self) -> Optional["MediaFormat"]:
|
|
return self._format
|
|
|
|
def getCodecInfo(self) -> Optional["MediaCodecInfo"]:
|
|
return None
|
|
|
|
def getName(self) -> str:
|
|
return self._name
|
|
|
|
def setCallback(self, cb: Optional["MediaCodec.Callback"],
|
|
handler: Any = None) -> None:
|
|
self._callback = cb
|
|
self._handler = handler
|
|
|
|
def createInputSurface(self) -> Any:
|
|
raise AndroidOnlyError("createInputSurface() 仅在 Android 上可用")
|
|
|
|
def signalEndOfInputStream(self) -> None:
|
|
pass
|
|
|
|
def getMetrics(self) -> Dict[str, Any]:
|
|
return {}
|
|
|
|
def setVideoScalingMode(self, mode: int) -> None:
|
|
pass
|
|
|
|
def getCodecName(self) -> str:
|
|
return self._name
|
|
|
|
def isConfigured(self) -> bool:
|
|
return self._configured
|
|
|
|
def isStarted(self) -> bool:
|
|
return self._started
|
|
|
|
def toString(self) -> str:
|
|
return f"MediaCodec(name={self._name}, started={self._started})"
|
|
|
|
|
|
class MediaCodecCallback(Stub):
|
|
"""MediaCodec.Callback — 编解码器回调"""
|
|
|
|
def onInputBufferAvailable(self, codec: MediaCodec, index: int) -> None:
|
|
pass
|
|
|
|
def onOutputBufferAvailable(self, codec: MediaCodec, index: int,
|
|
info: CodecBufferInfo) -> None:
|
|
pass
|
|
|
|
def onError(self, codec: MediaCodec, e: CodecException) -> None:
|
|
pass
|
|
|
|
def onOutputFormatChanged(self, codec: MediaCodec, format: "MediaFormat") -> None:
|
|
pass
|
|
|
|
|
|
MediaCodec.Callback = MediaCodecCallback
|
|
|
|
|
|
class MediaCodecInfo(Stub):
|
|
"""MediaCodecInfo — 编解码器信息"""
|
|
|
|
class CodecCapabilities(Stub):
|
|
"""MediaCodecInfo.CodecCapabilities — 编解码能力"""
|
|
|
|
FEATURE_AdaptivePlayback = "adaptive-playback"
|
|
FEATURE_SecurePlayback = "secure-playback"
|
|
FEATURE_TunneledPlayback = "tunneled-playback"
|
|
FEATURE_PartialFrame = "partial-frame"
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.colorFormats: List[int] = []
|
|
self.profileLevels: List[Any] = []
|
|
self.profileEntries: List[Any] = []
|
|
self._flags: int = 0
|
|
self._max_instances: int = 0
|
|
self._bitrate_range: Optional[Tuple[int, int]] = None
|
|
self._width_range: Optional[Tuple[int, int]] = None
|
|
self._height_range: Optional[Tuple[int, int]] = None
|
|
self._frame_rate_range: Optional[Tuple[int, int]] = None
|
|
|
|
def getColorFormats(self) -> List[int]:
|
|
return list(self.colorFormats)
|
|
|
|
def getProfileLevels(self) -> List[Any]:
|
|
return list(self.profileLevels)
|
|
|
|
def getSupportedFeatures(self) -> List[str]:
|
|
features = []
|
|
return features
|
|
|
|
def isFeatureSupported(self, name: str) -> bool:
|
|
return False
|
|
|
|
def getVideoCapabilities(self) -> Optional[Any]:
|
|
return None
|
|
|
|
def getAudioCapabilities(self) -> Optional[Any]:
|
|
return None
|
|
|
|
def toString(self) -> str:
|
|
return (f"CodecCapabilities(formats={len(self.colorFormats)}, "
|
|
f"profiles={len(self.profileLevels)})")
|
|
|
|
class CodecProfileLevel(Stub):
|
|
"""MediaCodecInfo.CodecProfileLevel — 配置等级"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.profile: int = 0
|
|
self.level: int = 0
|
|
|
|
def __init__(self, name: str = "", canonical_name: str = ""):
|
|
super().__init__()
|
|
self._name = name
|
|
self._canonical_name = canonical_name or name
|
|
self._is_encoder: bool = False
|
|
self._is_vendor: bool = False
|
|
self._capabilities: Optional[MediaCodecInfo.CodecCapabilities] = None
|
|
self._supported_types: List[str] = []
|
|
|
|
def getName(self) -> str:
|
|
return self._name
|
|
|
|
def getCanonicalName(self) -> str:
|
|
return self._canonical_name
|
|
|
|
def isEncoder(self) -> bool:
|
|
return self._is_encoder
|
|
|
|
def setEncoder(self, is_enc: bool) -> None:
|
|
self._is_encoder = is_enc
|
|
|
|
def isVendor(self) -> bool:
|
|
return self._is_vendor
|
|
|
|
def getSupportedTypes(self) -> List[str]:
|
|
return list(self._supported_types)
|
|
|
|
def getCapabilitiesForType(self, type_str: str) -> Optional[CodecCapabilities]:
|
|
if type_str in self._supported_types:
|
|
return self._capabilities
|
|
return None
|
|
|
|
def toString(self) -> str:
|
|
return (f"MediaCodecInfo(name={self._name}, "
|
|
f"encoder={self._is_encoder}, "
|
|
f"types={len(self._supported_types)})")
|
|
|
|
|
|
class MediaCodecList(Stub):
|
|
"""MediaCodecList — 编解码器列表"""
|
|
|
|
REGULAR_CODECS = 0
|
|
ALL_CODECS = 1
|
|
|
|
def __init__(self, kind: int = REGULAR_CODECS):
|
|
super().__init__()
|
|
self._kind = kind
|
|
self._codecs: List[MediaCodecInfo] = []
|
|
|
|
def getCodecInfos(self) -> List[MediaCodecInfo]:
|
|
return list(self._codecs)
|
|
|
|
def findDecoderForFormat(self, format: "MediaFormat") -> Optional[str]:
|
|
return None
|
|
|
|
def findEncoderForFormat(self, format: "MediaFormat") -> Optional[str]:
|
|
return None
|
|
|
|
@staticmethod
|
|
def getCodecCount() -> int:
|
|
return 0
|
|
|
|
@staticmethod
|
|
def getCodecInfoAt(index: int) -> Optional[MediaCodecInfo]:
|
|
return None
|
|
|
|
def toString(self) -> str:
|
|
return f"MediaCodecList(codecs={len(self._codecs)})"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaExtractor
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaExtractor(Stub):
|
|
"""MediaExtractor — 媒体提取器"""
|
|
|
|
SEEK_TO_PREVIOUS_SYNC = 0
|
|
SEEK_TO_NEXT_SYNC = 1
|
|
SEEK_TO_CLOSEST_SYNC = 2
|
|
SAMPLE_FLAG_SYNC = 1
|
|
SAMPLE_FLAG_ENCRYPTED = 2
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._data_source: Optional[str] = None
|
|
self._fd: Optional[int] = None
|
|
self._track_count: int = 0
|
|
self._selected_tracks: Set[int] = set()
|
|
self._current_track: int = -1
|
|
self._sample_time: int = 0
|
|
self._sample_flags: int = 0
|
|
self._sample_size: int = 0
|
|
self._sample_track_index: int = -1
|
|
self._cached_info: Dict[int, "MediaFormat"] = {}
|
|
self._released = False
|
|
self._lock = threading.Lock()
|
|
|
|
def setDataSource(self, path: str) -> None:
|
|
self._data_source = path
|
|
self._fd = None
|
|
self._parse_source()
|
|
|
|
def setDataSource(self, path: str, headers: Dict[str, str] = None) -> None:
|
|
self._data_source = path
|
|
self._fd = None
|
|
self._headers = headers or {}
|
|
self._parse_source()
|
|
|
|
def setDataSource(self, fd: int) -> None:
|
|
self._fd = fd
|
|
self._data_source = None
|
|
if fd > 0:
|
|
self._track_count = 1
|
|
|
|
def _parse_source(self) -> None:
|
|
if self._data_source and os.path.exists(self._data_source):
|
|
self._track_count = 1
|
|
|
|
def getTrackCount(self) -> int:
|
|
return self._track_count
|
|
|
|
def getTrackFormat(self, index: int) -> Optional["MediaFormat"]:
|
|
return self._cached_info.get(index)
|
|
|
|
def selectTrack(self, index: int) -> None:
|
|
with self._lock:
|
|
if 0 <= index < self._track_count:
|
|
self._selected_tracks.add(index)
|
|
|
|
def unselectTrack(self, index: int) -> None:
|
|
with self._lock:
|
|
self._selected_tracks.discard(index)
|
|
|
|
def getSampleTrackIndex(self) -> int:
|
|
return self._sample_track_index
|
|
|
|
def readSampleData(self, byte_buf: Any, offset: int) -> int:
|
|
return -1
|
|
|
|
def getSampleTime(self) -> int:
|
|
return self._sample_time
|
|
|
|
def getSampleFlags(self) -> int:
|
|
return self._sample_flags
|
|
|
|
def getSampleSize(self) -> int:
|
|
return self._sample_size
|
|
|
|
def advance(self) -> bool:
|
|
return False
|
|
|
|
def seekTo(self, time_us: int, mode: int = SEEK_TO_CLOSEST_SYNC) -> None:
|
|
self._sample_time = time_us
|
|
|
|
def release(self) -> None:
|
|
self._released = True
|
|
self._cached_info.clear()
|
|
self._selected_tracks.clear()
|
|
|
|
def getCachedDuration(self) -> int:
|
|
return -1
|
|
|
|
def hasCacheReachedEndOfStream(self) -> bool:
|
|
return False
|
|
|
|
def getSampleCryptoInfo(self, info: CryptoInfo) -> bool:
|
|
return False
|
|
|
|
def toString(self) -> str:
|
|
return (f"MediaExtractor(source={self._data_source}, "
|
|
f"tracks={self._track_count})")
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaMuxer
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaMuxer(Stub):
|
|
"""MediaMuxer — 媒体复用器"""
|
|
|
|
OUTPUT_FORMAT_MPEG4 = 0
|
|
OUTPUT_FORMAT_WEBM = 1
|
|
OUTPUT_FORMAT_3GPP = 2
|
|
OUTPUT_FORMAT_HEIF = 3
|
|
OUTPUT_FORMAT_OGG = 4
|
|
|
|
def __init__(self, path: str, format: int = OUTPUT_FORMAT_MPEG4):
|
|
super().__init__()
|
|
self._path = path
|
|
self._format = format
|
|
self._started = False
|
|
self._track_count = 0
|
|
self._tracks: Dict[int, "MediaFormat"] = {}
|
|
self._next_track_index = 0
|
|
self._released = False
|
|
|
|
def addTrack(self, format: "MediaFormat") -> int:
|
|
idx = self._next_track_index
|
|
self._next_track_index += 1
|
|
self._tracks[idx] = format
|
|
self._track_count = len(self._tracks)
|
|
return idx
|
|
|
|
def start(self) -> None:
|
|
if self._started:
|
|
return
|
|
self._started = True
|
|
|
|
def stop(self) -> None:
|
|
self._started = False
|
|
|
|
def release(self) -> None:
|
|
self._released = True
|
|
self._tracks.clear()
|
|
self._started = False
|
|
|
|
def writeSampleData(self, track_index: int, byte_buf: Any,
|
|
offset: int, buffer_info: CodecBufferInfo) -> None:
|
|
pass
|
|
|
|
def setOrientationHint(self, degrees: int) -> None:
|
|
pass
|
|
|
|
def setLocation(self, latitude: float, longitude: float) -> None:
|
|
pass
|
|
|
|
def getTrackCount(self) -> int:
|
|
return self._track_count
|
|
|
|
def getOutputFormat(self) -> int:
|
|
return self._format
|
|
|
|
def getPath(self) -> str:
|
|
return self._path
|
|
|
|
def isStarted(self) -> bool:
|
|
return self._started
|
|
|
|
def toString(self) -> str:
|
|
return (f"MediaMuxer(path={self._path}, format={self._format}, "
|
|
f"tracks={self._track_count})")
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaFormat
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaFormat(Stub):
|
|
"""MediaFormat — 媒体格式"""
|
|
|
|
# MIME 类型
|
|
MIMETYPE_VIDEO_AV1 = MIMETYPE_VIDEO_AV1
|
|
MIMETYPE_VIDEO_VP9 = MIMETYPE_VIDEO_VP9
|
|
MIMETYPE_VIDEO_VP8 = MIMETYPE_VIDEO_VP8
|
|
MIMETYPE_VIDEO_H265 = MIMETYPE_VIDEO_H265
|
|
MIMETYPE_VIDEO_H264 = MIMETYPE_VIDEO_H264
|
|
MIMETYPE_VIDEO_MPEG2 = MIMETYPE_VIDEO_MPEG2
|
|
MIMETYPE_VIDEO_MPEG4 = MIMETYPE_VIDEO_MPEG4
|
|
MIMETYPE_VIDEO_VC1 = MIMETYPE_VIDEO_VC1
|
|
MIMETYPE_VIDEO_DIVX = MIMETYPE_VIDEO_DIVX
|
|
MIMETYPE_VIDEO_DIVX3 = MIMETYPE_VIDEO_DIVX3
|
|
MIMETYPE_VIDEO_FLV = MIMETYPE_VIDEO_FLV
|
|
MIMETYPE_VIDEO_H263 = MIMETYPE_VIDEO_H263
|
|
MIMETYPE_VIDEO_RAW = MIMETYPE_VIDEO_RAW
|
|
MIMETYPE_VIDEO_DOLBY_VISION = MIMETYPE_VIDEO_DOLBY_VISION
|
|
MIMETYPE_VIDEO_SCRAMBLED = MIMETYPE_VIDEO_SCRAMBLED
|
|
MIMETYPE_AUDIO_AAC = MIMETYPE_AUDIO_AAC
|
|
MIMETYPE_AUDIO_MP3 = MIMETYPE_AUDIO_MP3
|
|
MIMETYPE_AUDIO_AMR_NB = MIMETYPE_AUDIO_AMR_NB
|
|
MIMETYPE_AUDIO_AMR_WB = MIMETYPE_AUDIO_AMR_WB
|
|
MIMETYPE_AUDIO_FLAC = MIMETYPE_AUDIO_FLAC
|
|
MIMETYPE_AUDIO_VORBIS = MIMETYPE_AUDIO_VORBIS
|
|
MIMETYPE_AUDIO_OPUS = MIMETYPE_AUDIO_OPUS
|
|
MIMETYPE_AUDIO_RAW = MIMETYPE_AUDIO_RAW
|
|
MIMETYPE_AUDIO_ALAC = MIMETYPE_AUDIO_ALAC
|
|
MIMETYPE_AUDIO_AC3 = MIMETYPE_AUDIO_AC3
|
|
MIMETYPE_AUDIO_EAC3 = MIMETYPE_AUDIO_EAC3
|
|
MIMETYPE_TEXT_VTT = MIMETYPE_TEXT_VTT
|
|
MIMETYPE_TEXT_CEA_608 = MIMETYPE_TEXT_CEA_608
|
|
MIMETYPE_TEXT_CEA_708 = MIMETYPE_TEXT_CEA_708
|
|
MIMETYPE_SUBTITLE_SRT = MIMETYPE_SUBTITLE_SRT
|
|
MIMETYPE_SUBTITLE_TTML = MIMETYPE_SUBTITLE_TTML
|
|
|
|
# KEY 常量
|
|
KEY_MIME = KEY_MIME
|
|
KEY_WIDTH = KEY_WIDTH
|
|
KEY_HEIGHT = KEY_HEIGHT
|
|
KEY_FRAME_RATE = KEY_FRAME_RATE
|
|
KEY_BIT_RATE = KEY_BIT_RATE
|
|
KEY_COLOR_FORMAT = KEY_COLOR_FORMAT
|
|
KEY_I_FRAME_INTERVAL = KEY_I_FRAME_INTERVAL
|
|
KEY_CHANNEL_COUNT = KEY_CHANNEL_COUNT
|
|
KEY_SAMPLE_RATE = KEY_SAMPLE_RATE
|
|
KEY_MAX_INPUT_SIZE = KEY_MAX_INPUT_SIZE
|
|
KEY_DURATION = KEY_DURATION
|
|
KEY_IS_ADTS = KEY_IS_ADTS
|
|
KEY_CHANNEL_MASK = KEY_CHANNEL_MASK
|
|
KEY_ENCODING = KEY_ENCODING
|
|
KEY_PROFILE = KEY_PROFILE
|
|
KEY_LEVEL = KEY_LEVEL
|
|
KEY_LANGUAGE = KEY_LANGUAGE
|
|
KEY_PRIORITY = KEY_PRIORITY
|
|
KEY_OPERATING_RATE = KEY_OPERATING_RATE
|
|
KEY_INTRA_REFRESH_PERIOD = KEY_INTRA_REFRESH_PERIOD
|
|
KEY_LATENCY = KEY_LATENCY
|
|
KEY_ROTATION = KEY_ROTATION
|
|
KEY_CAPTURE_RATE = KEY_CAPTURE_RATE
|
|
KEY_QUALITY = KEY_QUALITY
|
|
KEY_MAX_BIT_RATE = KEY_MAX_BIT_RATE
|
|
KEY_FLAC_COMPRESSION_LEVEL = KEY_FLAC_COMPRESSION_LEVEL
|
|
KEY_TRACK_ID = KEY_TRACK_ID
|
|
KEY_IS_AUTOSELECT = KEY_IS_AUTOSELECT
|
|
KEY_IS_DEFAULT = KEY_IS_DEFAULT
|
|
KEY_IS_FORCED_SUBTITLE = KEY_IS_FORCED_SUBTITLE
|
|
|
|
# 色彩格式
|
|
COLOR_FormatUnsupported = COLOR_FormatUnsupported
|
|
COLOR_FormatYUV420Planar = COLOR_FormatYUV420Planar
|
|
COLOR_FormatYUV420SemiPlanar = COLOR_FormatYUV420SemiPlanar
|
|
COLOR_FormatYUV420Flexible = COLOR_FormatYUV420Flexible
|
|
COLOR_FormatSurface = COLOR_FormatSurface
|
|
COLOR_Format32BitARGB8888 = COLOR_Format32BitARGB8888
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._properties: Dict[str, Any] = {}
|
|
|
|
@staticmethod
|
|
def createVideoFormat(mime: str, width: int, height: int) -> "MediaFormat":
|
|
fmt = MediaFormat()
|
|
fmt.setString(KEY_MIME, mime)
|
|
fmt.setInteger(KEY_WIDTH, width)
|
|
fmt.setInteger(KEY_HEIGHT, height)
|
|
return fmt
|
|
|
|
@staticmethod
|
|
def createAudioFormat(mime: str, sample_rate: int, channel_count: int) -> "MediaFormat":
|
|
fmt = MediaFormat()
|
|
fmt.setString(KEY_MIME, mime)
|
|
fmt.setInteger(KEY_SAMPLE_RATE, sample_rate)
|
|
fmt.setInteger(KEY_CHANNEL_COUNT, channel_count)
|
|
return fmt
|
|
|
|
@staticmethod
|
|
def createSubtitleFormat(mime: str, language: str) -> "MediaFormat":
|
|
fmt = MediaFormat()
|
|
fmt.setString(KEY_MIME, mime)
|
|
fmt.setString(KEY_LANGUAGE, language)
|
|
return fmt
|
|
|
|
def containsKey(self, key: str) -> bool:
|
|
return key in self._properties
|
|
|
|
def getInteger(self, key: str, default_value: int = 0) -> int:
|
|
val = self._properties.get(key, default_value)
|
|
return int(val) if isinstance(val, (int, float)) else default_value
|
|
|
|
def getFloat(self, key: str, default_value: float = 0.0) -> float:
|
|
val = self._properties.get(key, default_value)
|
|
return float(val) if isinstance(val, (int, float)) else default_value
|
|
|
|
def getLong(self, key: str, default_value: int = 0) -> int:
|
|
return self.getInteger(key, default_value)
|
|
|
|
def getString(self, key: str, default_value: Optional[str] = None) -> Optional[str]:
|
|
val = self._properties.get(key, default_value)
|
|
return str(val) if val is not None else None
|
|
|
|
def getByteBuffer(self, key: str) -> Optional[bytes]:
|
|
val = self._properties.get(key)
|
|
if isinstance(val, (bytes, bytearray)):
|
|
return bytes(val)
|
|
return None
|
|
|
|
def setInteger(self, key: str, value: int) -> None:
|
|
self._properties[key] = value
|
|
|
|
def setFloat(self, key: str, value: float) -> None:
|
|
self._properties[key] = value
|
|
|
|
def setLong(self, key: str, value: int) -> None:
|
|
self._properties[key] = value
|
|
|
|
def setString(self, key: str, value: str) -> None:
|
|
self._properties[key] = value
|
|
|
|
def setByteBuffer(self, key: str, buf: bytes) -> None:
|
|
self._properties[key] = buf
|
|
|
|
def getMap(self) -> Dict[str, Any]:
|
|
return dict(self._properties)
|
|
|
|
def toString(self) -> str:
|
|
parts = [f"{k}={v}" for k, v in self._properties.items()]
|
|
return f"MediaFormat({', '.join(parts)})"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaCrypto
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaCrypto(Stub):
|
|
"""MediaCrypto — 媒体加密"""
|
|
|
|
def __init__(self, uuid: str = ""):
|
|
super().__init__()
|
|
self._uuid = uuid
|
|
self._init_data: Optional[bytes] = None
|
|
|
|
def requiresSecureDecoderComponent(self, mime: str) -> bool:
|
|
return False
|
|
|
|
def setMediaDrmSession(self, session_id: bytes) -> None:
|
|
pass
|
|
|
|
def getUUID(self) -> str:
|
|
return self._uuid
|
|
|
|
def toString(self) -> str:
|
|
return f"MediaCrypto(uuid={self._uuid})"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaDrm
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaDrm(Stub):
|
|
"""MediaDrm — 数字版权管理"""
|
|
|
|
KEY_TYPE_STREAMING = 1
|
|
KEY_TYPE_OFFLINE = 2
|
|
KEY_TYPE_RELEASE = 3
|
|
|
|
EVENT_PROVISION_REQUIRED = 1
|
|
EVENT_KEY_REQUIRED = 2
|
|
EVENT_KEY_EXPIRED = 3
|
|
EVENT_VENDOR_DEFINED = 4
|
|
EVENT_SESSION_RECLAIMED = 5
|
|
|
|
KEY_EXPIRED = 0
|
|
KEY_STATUS_OK = 1
|
|
KEY_STATUS_PENDING = 2
|
|
KEY_STATUS_USABLE = 3
|
|
KEY_STATUS_USABLE_IN_FUTURE = 4
|
|
|
|
def __init__(self, uuid: str = ""):
|
|
super().__init__()
|
|
self._uuid = uuid
|
|
self._sessions: Dict[str, bytes] = {}
|
|
self._event_listener: Optional[Callable] = None
|
|
self._expiration_update_listener: Optional[Callable] = None
|
|
self._key_status_listener: Optional[Callable] = None
|
|
|
|
def openSession(self) -> Optional[bytes]:
|
|
import os
|
|
session_id = os.urandom(16)
|
|
self._sessions[session_id.hex()] = session_id
|
|
return session_id
|
|
|
|
def closeSession(self, session_id: bytes) -> None:
|
|
key = session_id.hex() if isinstance(session_id, bytes) else session_id
|
|
self._sessions.pop(key, None)
|
|
|
|
def getKeyRequest(self, session_id: bytes, init_data: bytes,
|
|
mime_type: str, key_type: int,
|
|
optional_parameters: Optional[Dict[str, str]] = None) -> Any:
|
|
return None
|
|
|
|
def provideKeyResponse(self, session_id: bytes, response: bytes) -> Optional[bytes]:
|
|
return None
|
|
|
|
def restoreKeys(self, session_id: bytes, key_set_id: bytes) -> None:
|
|
pass
|
|
|
|
def removeKeys(self, session_id: bytes) -> None:
|
|
pass
|
|
|
|
def queryKeyStatus(self, session_id: bytes) -> Optional[Dict[str, str]]:
|
|
return {"status": "usable"}
|
|
|
|
def getProvisionRequest(self) -> Optional[Tuple[bytes, str]]:
|
|
return None
|
|
|
|
def provideProvisionResponse(self, response: bytes) -> None:
|
|
pass
|
|
|
|
def getSecureStops(self) -> List[bytes]:
|
|
return []
|
|
|
|
def getSecureStop(self, ssid: bytes) -> Optional[bytes]:
|
|
return None
|
|
|
|
def releaseSecureStops(self, ssid: bytes) -> None:
|
|
pass
|
|
|
|
def releaseAllSecureStops(self) -> None:
|
|
pass
|
|
|
|
def getPropertyString(self, name: str) -> str:
|
|
return ""
|
|
|
|
def setPropertyString(self, name: str, value: str) -> None:
|
|
pass
|
|
|
|
def getPropertyByteArray(self, name: str) -> Optional[bytes]:
|
|
return None
|
|
|
|
def setPropertyByteArray(self, name: str, value: bytes) -> None:
|
|
pass
|
|
|
|
def getMetrics(self) -> Dict[str, Any]:
|
|
return {}
|
|
|
|
def getMaxSessionCount(self) -> int:
|
|
return 128
|
|
|
|
def getOpenSessionCount(self) -> int:
|
|
return len(self._sessions)
|
|
|
|
def getTotalSessionCount(self) -> int:
|
|
return len(self._sessions)
|
|
|
|
def release(self) -> None:
|
|
self._sessions.clear()
|
|
|
|
@staticmethod
|
|
def isCryptoSchemeSupported(uuid: str) -> bool:
|
|
return False
|
|
|
|
def toString(self) -> str:
|
|
return f"MediaDrm(uuid={self._uuid})"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaDescription / MediaMetadata
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaDescription(Stub):
|
|
"""MediaDescription — 媒体描述"""
|
|
|
|
BT_FOLDER_TYPE_ALBUMS = 1
|
|
BT_FOLDER_TYPE_ARTISTS = 2
|
|
BT_FOLDER_TYPE_GENRES = 3
|
|
BT_FOLDER_TYPE_PLAYLISTS = 4
|
|
BT_FOLDER_TYPE_YEARS = 5
|
|
EXTRA_BT_FOLDER_TYPE = "android.media.extra.BT_FOLDER_TYPE"
|
|
EXTRA_DOWNLOAD_STATUS = "android.media.extra.DOWNLOAD_STATUS"
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._media_id: str = ""
|
|
self._title: Optional[str] = None
|
|
self._subtitle: Optional[str] = None
|
|
self._description: Optional[str] = None
|
|
self._icon_uri: Optional[str] = None
|
|
self._extras: Dict[str, Any] = {}
|
|
self._media_uri: Optional[str] = None
|
|
self._flags: int = 0
|
|
|
|
def getMediaId(self) -> Optional[str]:
|
|
return self._media_id
|
|
|
|
def getTitle(self) -> Optional[str]:
|
|
return self._title
|
|
|
|
def getSubtitle(self) -> Optional[str]:
|
|
return self._subtitle
|
|
|
|
def getDescription(self) -> Optional[str]:
|
|
return self._description
|
|
|
|
def getIconUri(self) -> Optional[str]:
|
|
return self._icon_uri
|
|
|
|
def getExtras(self) -> Optional[Dict[str, Any]]:
|
|
return dict(self._extras) if self._extras else None
|
|
|
|
def getMediaUri(self) -> Optional[str]:
|
|
return self._media_uri
|
|
|
|
def toString(self) -> str:
|
|
return (f"MediaDescription(id={self._media_id}, "
|
|
f"title={self._title})")
|
|
|
|
|
|
class MediaMetadata(Stub):
|
|
"""MediaMetadata — 媒体元数据"""
|
|
|
|
METADATA_KEY_ALBUM = "android.media.metadata.ALBUM"
|
|
METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART"
|
|
METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST"
|
|
METADATA_KEY_ART = "android.media.metadata.ART"
|
|
METADATA_KEY_ARTIST = "android.media.metadata.ARTIST"
|
|
METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR"
|
|
METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION"
|
|
METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER"
|
|
METADATA_KEY_DATE = "android.media.metadata.DATE"
|
|
METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER"
|
|
METADATA_KEY_DURATION = "android.media.metadata.DURATION"
|
|
METADATA_KEY_GENRE = "android.media.metadata.GENRE"
|
|
METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS"
|
|
METADATA_KEY_RATING = "android.media.metadata.RATING"
|
|
METADATA_KEY_TITLE = "android.media.metadata.TITLE"
|
|
METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER"
|
|
METADATA_KEY_WRITER = "android.media.metadata.WRITER"
|
|
METADATA_KEY_YEAR = "android.media.metadata.YEAR"
|
|
METADATA_KEY_MEDIA_ID = "android.media.metadata.MEDIA_ID"
|
|
METADATA_KEY_DISPLAY_TITLE = "android.media.metadata.DISPLAY_TITLE"
|
|
METADATA_KEY_DISPLAY_SUBTITLE = "android.media.metadata.DISPLAY_SUBTITLE"
|
|
METADATA_KEY_DISPLAY_DESCRIPTION = "android.media.metadata.DISPLAY_DESCRIPTION"
|
|
METADATA_KEY_DISPLAY_ICON = "android.media.metadata.DISPLAY_ICON"
|
|
METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING"
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._values: Dict[str, Any] = {}
|
|
|
|
def containsKey(self, key: str) -> bool:
|
|
return key in self._values
|
|
|
|
def getString(self, key: str) -> Optional[str]:
|
|
val = self._values.get(key)
|
|
return str(val) if val is not None else None
|
|
|
|
def getLong(self, key: str) -> int:
|
|
return int(self._values.get(key, 0))
|
|
|
|
def getFloat(self, key: str) -> float:
|
|
return float(self._values.get(key, 0.0))
|
|
|
|
def getBitmap(self, key: str) -> Optional[Any]:
|
|
return None
|
|
|
|
def putString(self, key: str, value: str) -> None:
|
|
self._values[key] = value
|
|
|
|
def putLong(self, key: str, value: int) -> None:
|
|
self._values[key] = value
|
|
|
|
def putFloat(self, key: str, value: float) -> None:
|
|
self._values[key] = value
|
|
|
|
def putBitmap(self, key: str, bitmap: Any) -> None:
|
|
pass
|
|
|
|
def size(self) -> int:
|
|
return len(self._values)
|
|
|
|
def keySet(self) -> Set[str]:
|
|
return set(self._values.keys())
|
|
|
|
def toString(self) -> str:
|
|
return f"MediaMetadata(entries={len(self._values)})"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaMetadataRetriever
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaMetadataRetriever(Stub):
|
|
"""MediaMetadataRetriever — 媒体元数据检索器"""
|
|
|
|
OPTION_PREVIOUS_SYNC = 0
|
|
OPTION_NEXT_SYNC = 1
|
|
OPTION_CLOSEST_SYNC = 2
|
|
OPTION_CLOSEST = 3
|
|
|
|
METADATA_KEY_ALBUM = 1
|
|
METADATA_KEY_ALBUM_ARTIST = 13
|
|
METADATA_KEY_ARTIST = 2
|
|
METADATA_KEY_AUTHOR = 3
|
|
METADATA_KEY_BITRATE = 20
|
|
METADATA_KEY_CD_TRACK_NUMBER = 0
|
|
METADATA_KEY_COMPILATION = 15
|
|
METADATA_KEY_COMPOSER = 4
|
|
METADATA_KEY_DATE = 5
|
|
METADATA_KEY_DISC_NUMBER = 14
|
|
METADATA_KEY_DURATION = 9
|
|
METADATA_KEY_GENRE = 6
|
|
METADATA_KEY_HAS_AUDIO = 16
|
|
METADATA_KEY_HAS_VIDEO = 17
|
|
METADATA_KEY_IS_DRM = 22
|
|
METADATA_KEY_LOCATION = 23
|
|
METADATA_KEY_MIMETYPE = 12
|
|
METADATA_KEY_NUM_TRACKS = 10
|
|
METADATA_KEY_TITLE = 7
|
|
METADATA_KEY_VIDEO_HEIGHT = 19
|
|
METADATA_KEY_VIDEO_WIDTH = 18
|
|
METADATA_KEY_WRITER = 11
|
|
METADATA_KEY_YEAR = 8
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._data_source: Optional[str] = None
|
|
self._extracted: Dict[int, str] = {}
|
|
self._released = False
|
|
|
|
def setDataSource(self, path: str) -> None:
|
|
self._data_source = path
|
|
self._extract()
|
|
|
|
def setDataSource(self, path: str, headers: Dict[str, str]) -> None:
|
|
self._data_source = path
|
|
self._headers = headers
|
|
self._extract()
|
|
|
|
def setDataSource(self, uri: Any, headers: Dict[str, str]) -> None:
|
|
self._data_source = str(uri)
|
|
self._extract()
|
|
|
|
def setDataSource(self, fd: int) -> None:
|
|
self._data_source = f"fd:{fd}"
|
|
self._extract()
|
|
|
|
def _extract(self) -> None:
|
|
if self._data_source and os.path.exists(str(self._data_source)):
|
|
self._extracted[METADATA_KEY_HAS_AUDIO] = "yes"
|
|
self._extracted[METADATA_KEY_HAS_VIDEO] = "yes"
|
|
|
|
def extractMetadata(self, key_code: int) -> Optional[str]:
|
|
return self._extracted.get(key_code)
|
|
|
|
def getEmbeddedPicture(self) -> Optional[bytes]:
|
|
return None
|
|
|
|
def getFrameAtTime(self, time_us: int = -1,
|
|
option: int = OPTION_CLOSEST_SYNC) -> Optional[Any]:
|
|
return None
|
|
|
|
def getScaledFrameAtTime(self, time_us: int,
|
|
option: int,
|
|
width: int, height: int) -> Optional[Any]:
|
|
return None
|
|
|
|
def getPrimaryImage(self) -> Optional[Any]:
|
|
return None
|
|
|
|
def release(self) -> None:
|
|
self._released = True
|
|
self._extracted.clear()
|
|
|
|
def toString(self) -> str:
|
|
return f"MediaMetadataRetriever(source={self._data_source})"
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaPlayer 扩展
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaPlayer(Stub):
|
|
"""MediaPlayer — 媒体播放器(扩展)"""
|
|
|
|
MEDIA_ERROR_UNKNOWN = 1
|
|
MEDIA_ERROR_SERVER_DIED = 100
|
|
MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200
|
|
MEDIA_ERROR_IO = -1004
|
|
MEDIA_ERROR_MALFORMED = -1007
|
|
MEDIA_ERROR_TIMED_OUT = -110
|
|
MEDIA_ERROR_UNSUPPORTED = -1010
|
|
|
|
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
|
|
|
|
SEEK_CLOSEST = 3
|
|
SEEK_CLOSEST_SYNC = 2
|
|
SEEK_NEXT_SYNC = 1
|
|
SEEK_PREVIOUS_SYNC = 0
|
|
|
|
VIDEO_SCALING_MODE_SCALE_TO_FIT = 1
|
|
VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING = 2
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._data_source: Optional[str] = None
|
|
self._isplaying = False
|
|
self._isloop = False
|
|
self._volume: float = 1.0
|
|
self._position: int = 0
|
|
self._duration: int = 0
|
|
self._isplayback = False
|
|
self._isprepared = False
|
|
self._on_completion: Optional[Callable] = None
|
|
self._on_error: Optional[Callable] = None
|
|
self._on_prepared: Optional[Callable] = None
|
|
self._on_info: Optional[Callable] = None
|
|
self._lock = threading.Lock()
|
|
self._surface: Optional[Any] = None
|
|
|
|
@staticmethod
|
|
def create(context: Any, uri: Any) -> "MediaPlayer":
|
|
mp = MediaPlayer()
|
|
mp.setDataSource(str(uri))
|
|
mp.prepare()
|
|
return mp
|
|
|
|
def setDataSource(self, path: str) -> None:
|
|
self._data_source = path
|
|
|
|
def setDataSource(self, path: str, headers: Dict[str, str]) -> None:
|
|
self._data_source = path
|
|
|
|
def prepare(self) -> None:
|
|
self._isprepared = True
|
|
self._duration = 10000
|
|
if self._on_prepared:
|
|
self._on_prepared(self)
|
|
|
|
def prepareAsync(self) -> None:
|
|
self._isprepared = True
|
|
if self._on_prepared:
|
|
self._on_prepared(self)
|
|
|
|
def start(self) -> None:
|
|
self._isplaying = True
|
|
self._isplayback = True
|
|
|
|
def stop(self) -> None:
|
|
self._isplaying = False
|
|
self._isplayback = False
|
|
self._position = 0
|
|
|
|
def pause(self) -> None:
|
|
self._isplaying = False
|
|
|
|
def seekTo(self, msec: int, mode: int = SEEK_CLOSEST) -> None:
|
|
self._position = max(0, msec)
|
|
|
|
def getCurrentPosition(self) -> int:
|
|
return self._position
|
|
|
|
def getDuration(self) -> int:
|
|
return self._duration
|
|
|
|
def isPlaying(self) -> bool:
|
|
return self._isplaying
|
|
|
|
def isLooping(self) -> bool:
|
|
return self._isloop
|
|
|
|
def setLooping(self, looping: bool) -> None:
|
|
self._isloop = looping
|
|
|
|
def setVolume(self, left_volume: float, right_volume: float) -> None:
|
|
self._volume = (left_volume + right_volume) / 2.0
|
|
|
|
def getVolume(self) -> float:
|
|
return self._volume
|
|
|
|
def setSurface(self, surface: Any) -> None:
|
|
self._surface = surface
|
|
|
|
def setDisplay(self, holder: Any) -> None:
|
|
pass
|
|
|
|
def setScreenOnWhilePlaying(self, screen_on: bool) -> None:
|
|
pass
|
|
|
|
def setWakeMode(self, context: Any, mode: int) -> None:
|
|
pass
|
|
|
|
def setAudioStreamType(self, stream_type: int) -> None:
|
|
pass
|
|
|
|
def reset(self) -> None:
|
|
self.stop()
|
|
self._isprepared = False
|
|
|
|
def release(self) -> None:
|
|
self.reset()
|
|
|
|
def getVideoWidth(self) -> int:
|
|
return 0
|
|
|
|
def getVideoHeight(self) -> int:
|
|
return 0
|
|
|
|
def setOnCompletionListener(self, listener: Optional[Callable]) -> None:
|
|
self._on_completion = listener
|
|
|
|
def setOnErrorListener(self, listener: Optional[Callable]) -> None:
|
|
self._on_error = listener
|
|
|
|
def setOnPreparedListener(self, listener: Optional[Callable]) -> None:
|
|
self._on_prepared = listener
|
|
|
|
def setOnInfoListener(self, listener: Optional[Callable]) -> None:
|
|
self._on_info = listener
|
|
|
|
def setOnBufferingUpdateListener(self, listener: Optional[Callable]) -> None:
|
|
pass
|
|
|
|
def setOnSeekCompleteListener(self, listener: Optional[Callable]) -> None:
|
|
pass
|
|
|
|
def setOnVideoSizeChangedListener(self, listener: Optional[Callable]) -> None:
|
|
pass
|
|
|
|
def attachAuxEffect(self, effect_id: int) -> None:
|
|
pass
|
|
|
|
def toString(self) -> str:
|
|
return (f"MediaPlayer(source={self._data_source}, "
|
|
f"playing={self._isplaying})")
|
|
|
|
|
|
# ═══════════════════════════════════════════════════════════════
|
|
# MediaSync / MediaTimeProvider / TimedMetaData / WebVttParser
|
|
# ═══════════════════════════════════════════════════════════════
|
|
|
|
class MediaSync(Stub):
|
|
"""MediaSync — 媒体同步器"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._surface: Optional[Any] = None
|
|
self._playback_params: Optional[Any] = None
|
|
self._callback: Optional[Callable] = None
|
|
self._timestamp: int = 0
|
|
self._playback_rate: float = 1.0
|
|
|
|
def setSurface(self, surface: Any) -> None:
|
|
self._surface = surface
|
|
|
|
def setCallback(self, cb: Optional[Callable], handler: Any = None) -> None:
|
|
self._callback = cb
|
|
|
|
def getPlaybackParams(self) -> Any:
|
|
return self._playback_params
|
|
|
|
def setPlaybackParams(self, params: Any) -> None:
|
|
self._playback_params = params
|
|
|
|
def queueAudioBuffer(self, buffer_info: CodecBufferInfo, data: bytes,
|
|
offset: int, size: int, presentation_time_us: int) -> None:
|
|
pass
|
|
|
|
def queueVideoBuffer(self, data: bytes, buffer_info: CodecBufferInfo,
|
|
presentation_time_us: int) -> None:
|
|
pass
|
|
|
|
def render(self, presentation_time_us: int) -> bool:
|
|
return True
|
|
|
|
def flush(self) -> None:
|
|
pass
|
|
|
|
def getTimestamp(self) -> int:
|
|
return self._timestamp
|
|
|
|
def release(self) -> None:
|
|
pass
|
|
|
|
def toString(self) -> str:
|
|
return f"MediaSync(rate={self._playback_rate})"
|
|
|
|
|
|
class MediaTimeProvider(Stub):
|
|
"""MediaTimeProvider — 媒体时间提供器"""
|
|
|
|
def getTimeUs(self) -> int:
|
|
return int(time.time() * 1000000)
|
|
|
|
|
|
class TimedMetaData(Stub):
|
|
"""TimedMetaData — 定时元数据"""
|
|
|
|
def __init__(self, timestamp_us: int = 0, meta_data: bytes = b""):
|
|
super().__init__()
|
|
self._timestamp_us = timestamp_us
|
|
self._meta_data = meta_data
|
|
|
|
def getTimestamp(self) -> int:
|
|
return self._timestamp_us
|
|
|
|
def getMetaData(self) -> bytes:
|
|
return self._meta_data
|
|
|
|
def toString(self) -> str:
|
|
return (f"TimedMetaData(timestamp={self._timestamp_us}, "
|
|
f"size={len(self._meta_data)})")
|
|
|
|
|
|
class WebVttParser(Stub):
|
|
"""WebVttParser — WebVTT 字幕解析器"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self._cues: List["WebVttCue"] = []
|
|
|
|
def parse(self, data: str) -> List["WebVttCue"]:
|
|
self._cues.clear()
|
|
lines = data.split("\n")
|
|
current_cue: Optional["WebVttCue"] = None
|
|
for line in lines:
|
|
line = line.strip()
|
|
if line.startswith("WEBVTT"):
|
|
continue
|
|
if line == "":
|
|
if current_cue is not None:
|
|
self._cues.append(current_cue)
|
|
current_cue = None
|
|
continue
|
|
if "-->" in line:
|
|
parts = line.split("-->")
|
|
start = self._parse_time(parts[0].strip())
|
|
end = self._parse_time(parts[1].strip())
|
|
current_cue = WebVttCue(start, end)
|
|
elif current_cue is not None:
|
|
current_cue.add_text(line)
|
|
if current_cue is not None:
|
|
self._cues.append(current_cue)
|
|
return list(self._cues)
|
|
|
|
def _parse_time(self, time_str: str) -> int:
|
|
"""将 HH:MM:SS.mmm 格式转换为微秒"""
|
|
if ":" in time_str:
|
|
parts = time_str.split(":")
|
|
if len(parts) == 3:
|
|
h, m, s = parts
|
|
else:
|
|
h = "0"
|
|
m, s = parts
|
|
sec_parts = s.split(".")
|
|
seconds = int(sec_parts[0])
|
|
millis = int(sec_parts[1]) if len(sec_parts) > 1 else 0
|
|
total_ms = int(h) * 3600000 + int(m) * 60000 + seconds * 1000 + millis
|
|
return total_ms * 1000
|
|
return 0
|
|
|
|
def getCues(self) -> List["WebVttCue"]:
|
|
return list(self._cues)
|
|
|
|
def clear(self) -> None:
|
|
self._cues.clear()
|
|
|
|
|
|
class WebVttCue(Stub):
|
|
"""WebVttCue — WebVTT 字幕提示"""
|
|
|
|
def __init__(self, start_us: int = 0, end_us: int = 0, text: str = ""):
|
|
super().__init__()
|
|
self._start_us = start_us
|
|
self._end_us = end_us
|
|
self._text = text
|
|
|
|
def add_text(self, text: str) -> None:
|
|
if self._text:
|
|
self._text += "\n" + text
|
|
else:
|
|
self._text = text
|
|
|
|
def getStartTime(self) -> int:
|
|
return self._start_us
|
|
|
|
def getEndTime(self) -> int:
|
|
return self._end_us
|
|
|
|
def getText(self) -> str:
|
|
return self._text
|
|
|
|
def toString(self) -> str:
|
|
return (f"WebVttCue(start={self._start_us}, "
|
|
f"end={self._end_us}, text={self._text[:30]})") |