当前位置:   article > 正文

android MediaCodec解析_android h265 codec-specific data

android h265 codec-specific data

简介

MediaCodec类可以获取底层媒体编码/解码库,是Android底层多媒体支持库的一部分(一般和MediaExtractor、MediaSync、MediaMuxer、MediaCrypto、MediaDrm、Image、Surface、AudioTrack搭配使用)。
MediaCodec Buffers流
宽泛的说,codec(编解码器)通过异步的方式对输入的数据进行处理,输出处理后的数据,过程中需要一系列的输入/输出Buffers。最简单的情况下,先把数据放进一个空的输入buffer(申请或者获取得到)中,发送给codec,codec对数据进行处理转换后放进一个输出buffer中,拿到输出buffer后,自行处理输出buffer中的数据,之后释放输出buffer并返回给codec。

数据类型

codec可以操作三种数据:压缩过的数据、raw格式audio数据、raw格式video数据。

  • 这三种数据都可以用ByteBuffers进行处理。
  • 但是处理raw video数据时,应该使用Surface以增强性能。Surface使用native层buffer,没有经过映射、拷贝到ByteBuffers,所以效率更高。正常情况下,使用Surface时获取不到raw格式的video数据,需要通过ImageReader类获取raw格式的vedio帧数据,由于native层的buffers可以直接映射为ByteBuffer,所以效率依然很高。
  • 当使用ByteBuffer模式时,通过Image类的getInput()/OutPutImage(int)方法,就可以得到raw格式的video帧数据。

Commpressed Buffer

输入buffers和输出buffers根据格式类型包含不同的压缩数据,对于vedio,buffer中是视频的一帧数据;对于audio来说,一般下是一个访问单元(若干毫秒的audio)数据,也有可能是多个访问单元的数据。任何情况下,buffer只在帧或者访问单元的边界上开始或结束,不会在任一字符边界开始或者结束。

Raw Audio Buffers

Raw Audio buffers包含了PCM audio数据的整个帧,是音频通道中每个通道的样本。每个样本在都是一个用native byte顺序存放的16-bit signed integer。

      
      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
      
      
short[] getSamplesForChannel(MediaCodec codec, int bufferId, int channelIx) {
ByteBuffer outputBuffer = codec.getOutputBuffer(bufferId);
MediaFormat format = codec.getOutputFormat(bufferId);
ShortBuffer samples = outputBuffer.order(ByteOrder.nativeOrder()).asShortBuffer();
int numChannels = formet.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
if (channelIx < 0 || channelIx >= numChannels) {
return null;
}
short[] res = new short[samples.remaining() / numChannels];
for ( int i = 0; i < res.length; ++i) {
res[i] = samples.get(i * numChannels + channelIx);
}
return res;
}

Raw Video Buffers

在ByteBuffer模式下,video buffers是根据其颜色格式存放的,通过getCodeInfo().getCapabilitiesForType(……)可以获取支持的颜色格式数组。Video codecs支持以下三种颜色格式。

  • native raw videoformat:通过COLOR_FormatSurface标记的格式,可以与Surface的输入/输出一起使用
  • flexible YUV buffers(例如COLOR_FormatYUV420Flexible):可以同时与Surface的输入/输出和ByteBuffer模式下使用。
  • 其他具体的格式:一般只支持ByteBuffer模式。其中一些是厂家的具体格式,另外的声明在MediaCodecInfo.CodecCapabilities。对于和flexible格式相同的颜色格式,仍然可以使用getInput/OutPutImage(int)。

    从Android LOLLIPOP_MR1开始,所有的video codecs都支持flexible YUV4::2:0 buffers

在老设备上获取Raw Video ByteBuffers

在Android LOLLIPOP和Image支持之前,需要通过输出格式参数:KEY_STRIDE和DEY_SLICE_HEIGHT来描述raw格式的输出buffers。

在一些设备上切片高度被标示为0。折意味着切片的高度要么与帧的高度一样,要么与切片的高度的某个值对其(通常为2的幂)。但是,这种情况下没有一个标准或简单的方法分辨切片的实际高度。而且,U平面竖直方向上的格式也没有被指定或者定义,通常为切片高度的一半。

参数KEY_WIDTH和DEY_HEIGHT指定video帧的尺寸,但是大多数的编码的video(picture)只是video帧的一部分,由裁剪矩形表示。
使用下面一些参数可以获取raw output images的裁剪矩形,这些参数通过outputformat获取。如果这些参数不存在,video将占据整个video帧。在应用任何旋转操作之前,裁剪矩形在输出帧的context中被解释。

Format Key Type Description
“crop-left” Integer The left-coordinate (x) of the crop rectangle
“crop-top” Integer The top-coordinate (y) of the crop rectangle
“crop-right” Integer The right-coordinate (x) MINUS 1 of the crop rectangle
“crop-bottom” Integer The bottom-coordinate (y) MINUS 1 of the crop rectangle

The right and bottom coordinates can be understood as the coordinates of the right-most valid column/bottom-most valid row of the cropped output image.

旋转之前的video帧的尺寸可以通过如下方式计算:

      
      
1
2
3
4
5
6
7
8
9
      
      
MediaFormat format = decoder.getOutputFormat(…);
int width = format.getInteger(MediaFormat.KEY_WIDTH);
if (format.containsKey( "crop-left") && format.containsKey( "crop-right")) {
width = format.getInteger( "crop-right") + 1 - format.getInteger( "crop-left");
}
int height = format.getInteger(MediaFormat.KEY_HEIGHT);
if (format.containsKey( "crop-top") && format.containsKey( "crop-bottom")) {
height = format.getInteger( "crop-bottom") + 1 - format.getInteger( "crop-top");
}

要注意BufferInfo.offset的含义在不同设备上不一致。在某些设备上,偏移指向裁剪矩形的左上角像素,而在大多数设备上,它指向整个帧的左上角像素。

状态

在codec的生命周期中,存在三种状态:

  1. Stopped:包含Uninitialized、Configured、Error三种子状态。
  2. Executing:包含Flushed、Running、End-of-Stream三种子状态。
  3. Released。

MediaCodec状态

  1. 当用工厂方法床架一个codec时,codec处于Uninitialized状态。首先通过configure(……)方法注册codec,使其处于Configure状态;然后调用start()方法使其进入Executing状态,在这个状态下,就可以通过buffer队列处理数据。
  2. Executing有三个子状态:Flushed、Running、End-of-Stream。在调用start()之后codec立即今日Flushed状态,持有所有的buffers;当从buffer队列拿到第一个输入buffer时,codec进入Running状态,codec整个生命周期的大部分时间都处在这个状态;当输入buffer是end-of-stream标示时,codec切换到End-of-Stream状态,这个状态下,codec不再接收输入buffer,但是仍然生成输出buffer,直到遇见输出buffer的end-of-stream标示。在Executing状态下,可以通过调用flush()方法,使codec在任何时候切换回Flushed状态。
  3. 当调用stop()方法后,codec返回到Uninitialized状态,可以再次Configured。在codec使用结束后,必须调用release()方法释放codec。
  4. 在极少数情况下,codec可能遇到error进入Error状态。这是队列操作的无效返回值或者exception进行通讯的。这时可以调用reset()方法就可以使codec再次可用,也可以在任何状态下调用reset()方法,使codec返回到Uninitialized状态,否则就应该调用release()方法释放。

创建

使用MediaCodecList创建一个制定媒体格式(MediaFormat)的MediaCodec。当解码文件或者流时,可以通过MediaExtractor.getTrackFormat获取所需的格式,如果需要注入特定的特性,可以通过MediaFormat.setFeatureEnabled添加;然后调用MediaCodecList.findDecoderForFormat获取能够处理这种媒体格式的codec的名称;最后,使用createByCodecName(String)创建codec。

注意:在android LOLLIPOP中,给MediaCodecList.findDecoder/EncoderForFormat指定的格式一定不能包含帧速率。使用format.setString(MediaFormat.KEY_FRAME_RATE,null)来清除任何format中存在的帧速率。

也可以通过createDecoder/EncoderByType(String)创建一个为特定MIME类型格式的codec,但是不能注入特性,而且可能创建一个处理不了指定媒体格式的codec。

创建安全解码器

在androi KITKAT_WATCH及更早的版本中,安全的codec可能不在MediaCodecList中,但是在系统中仍然可用。存在的安全codec可以通过名字+”.secure”(所有的安全codec都必须以”.secure”结尾)进行实例化。如果codec不存在,createByCodecName(String)将抛出一个IOException异常。
从Android LOLLIPOP开始,应该在媒体格式中使用FEATURE_SecurePlayback特性创建安全解码器。

初始化

创建codec后,如果要异步处理数据,需要通过setCallback指定一个回调函数,然后使用特定的媒体格式配置一个codec。初始化时可以指定视频源输出到Surface、生成raw格式video data(如 video 解码器),设置安全codec的解码参数。由于某些codec可以在多种模式下运行,所以必须指定是否将其作为一个解码器或者编码器。
从android LOLLIPOP,可以在Configured状态查询输入/输出的格式,然后在codec starting之前验证配置配置结果。
如果要用raw video buffer的codec(如video编码器)本地处理raw格式的输入video buffer,可以在配置后通过createInputSurface()为输入数据创建一个目标Surface。或者建立一个codec通过调用setInputSurface(Suface)来使用已经创建过的持久化的输入Surface。

格式中的Codec-specific Data

一些格式,特别是AAC音频和MPEG4、H.264、H.265视频格式,要求实际数据要以Codec-specific Data或者buffer包含的设置数据的数字为前缀。处理这些压缩的格式时,Codec-specific Data数据必须在code start()之后、在任何帧数据到来之前提交给codec。在调用queueInputBuffer()时,必须对这些数据进行BUFFER_FLAG_CODEC_CONFIG标示。
Codec-specific Data可以包含在传递给带有”csd-0”,”csd-1”等关键字的ByteBuffer条目的配置中,这些关键字始终包含在从MediaExtractor获取的MediaFormat中。格式中的Codec-specific Data在codec start()时会自动提交给codec,不要明确递交。如果格式中不包含,可以根据格式要求,使用buffer指定的数字,用正确的顺序选择提交。在H.264 AVC的情况下,还可以连接所有codec-specific data,并将其作为单个codec-config buffer提交。
android中使用如下codec-specific data buffer。这些也需要按照适合MediaMuxer轨道配置的轨道格式进行设置。每个参数集和标有(*)的 codec-specific-data 都必须以起始代码“\ x00 \ x00 \ x00 \ x01”开头。

Format CSD buffer #0 CSD buffer #1 CSD buffer #2
AAC Decoder-specific information from ESDS* Not Used Not Used
VORBIS Identification header Setup header Not Used
OPUS Identification header Pre-skip in nanosecs

(unsigned 64-bit native-order integer.)
This overrides the pre-skip value in the identification header. | Seek Pre-roll in nanosecs
(unsigned 64-bit native-order integer.)
MPEG-4 | Decoder-specific information from ESDS | Not Used | Not Used
H.264 AVC | SPS (Sequence Parameter Sets
) | PPS (Picture Parameter Sets) | Not Used
H.265 HEVC | VPS (Video Parameter Sets
) +
SPS (Sequence Parameter Sets) +
PPS (Picture Parameter Sets
) | Not Used | Not Used
VP9 | VP9 CodecPrivate Data (optional) | Not Used | Not Used

注意:如果codec在输出buffer或者输出格式更改返回之前或者刚刚start()就被立即flushed,codec specific data可能在flush时丢失,必须使用 BUFFER_FLAG_CODEC_CONFIG标示buffer重新提交codec specific data,以保证codec正常工作。

编码器(或生成压缩数据的codec)将在标有codec-config标志的输出buffer中的任何有效输出buffer之前创建并返回 codec specific data。 包含codec-specific-data的buffer没有有意义的时间戳。

数据处理

在调用api时,每个docec都持有一系列被buffer-ID引用的输入/输出buffre,当成功的start()后,客户端拥有既不输入也不输出的buffer。在同步模式下,调用dequeueInput/OutputBuffer(……)从codec中获取一个输入/输出buffer;在异步模式下,通过MediaCodec.Callback.onInput/OutputBufferAvailable(……)回调函数会自动接收到可用的buffer。
在获取输入buffer时,放入数据后通过queueInputBuffer或者queueSecureInputBuffer(如果使用decryption)提交给codec。不要提交多个具有相同时间戳的输入buffer(除非是标记为 codec-specific data)
在异步模式下,codec会通过onOutputBufferAvailable回调函数返回一个只读的输出buffer;在同步模式下,调用dequeuOutputBuffer函数获取只读的输出buffer。当输出buffer处理完毕后,调用其中一个的releaseOutputBuffer的方法,将buffer返回给codec。
当不需要立即向codec提交或者释放buffer时,持有输入/输出buffer可能造成codec停止,视设备而定。具体来说,codec可能会推迟生成输出buffer,直到所有未完成的buffer都被提交或者释放,所以要尽少的持有可用的buffer。
根据API的版本,有三种处理数据的方式:
Processing Mode | API version <= 20
Jelly Bean/KitKat | API version >= 21
Lollipop and later
———— | ————-|
Synchronous API using buffer arrays | Supported | Deprecated
Synchronous API using buffers | Not Available | Supported
Asynchronous API using buffers | Not Available | Supported

## 使用Buffer异步处理
从android LOLLIPOP开始,首选异步处理数据,在调用configure之前,设置一个回调函数,异步模式下,因为要让codec进入Running子状态接收输入buffer,所以必须在flush()之后调用start(),这就会使codec状态发生改变。同样,在初始调用下启动的codec将会直接进入到Running子状态,通过回调函数开始传递可用的输入buffer。
MediaCodec异步流程
MediaCodec在异步模式下的用法如下:

      
      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
      
      
MediaCodec codec = MediaCodec.createByCodecName(name);
MediaFormat mOutputFormat; // member variable
codec.setCallback( new MediaCodec.Callback() {
@Override
void onInputBufferAvailable(MediaCodec mc, int inputBufferId) {
ByteBuffer inputBuffer = codec.getInputBuffer(inputBufferId);
// fill inputBuffer with valid data
codec.queueInputBuffer(inputBufferId, …);
}
@Override
void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, …) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is equivalent to mOutputFormat
// outputBuffer is ready to be processed or rendered.
codec.releaseOutputBuffer(outputBufferId, …);
}
@Override
void onOutputFormatChanged(MediaCodec mc, MediaFormat format) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
mOutputFormat = format; // option B
}
@Override
void onError(…) {
}
});
codec.configure(format, …);
mOutputFormat = codec.getOutputFormat(); // option B
codec.start();
// wait for processing to complete
codec.stop();
codec.release();

使用Buffers同步处理

从android LOLLIPOP开始,即使在同步模式下使用codec也应该通过getInput/OutputBuffer(int)/getInput/OutputImage(int) 来获取输入/输出buffer,这样可以让框架层进行一些优化,如处理动态内容。如果使用getInput/OutputBuffers()则优化将禁用。

不要同时混用buffer和buffer array的方法,具体来说,只有在start()之后直接调用getInput/OutputBuffers,或者dequeued一个值为 INFO_OUTPUT_FORMAT_CHANGED的输出buffer ID后才能混用。

MediaCodec在同步模式下的用法如下:

      
      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
      
      
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
MediaFormat outputFormat = codec.getOutputFormat(); // option B
codec.start();
for (;;) {
int inputBufferId = codec.dequeueInputBuffer(timeoutUs);
if (inputBufferId >= 0) {
ByteBuffer inputBuffer = codec.getInputBuffer(…);
// fill inputBuffer with valid data
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
ByteBuffer outputBuffer = codec.getOutputBuffer(outputBufferId);
MediaFormat bufferFormat = codec.getOutputFormat(outputBufferId); // option A
// bufferFormat is identical to outputFormat
// outputBuffer is ready to be processed or rendered.
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
// Can ignore if using getOutputFormat(outputBufferId)
outputFormat = codec.getOutputFormat(); // option B
}
}
codec.stop();
codec.release();

使用Buffer Arrays同步处理(deprecated)

在android KITKAT_WATCH及以前,输入/输出buffer的集合用ByteBuffer[]表示。在start()方法调用成功后,使用 getInput/OutputBuffers()检索数组,用buffer IDs(非负)作为索引,如下面代码所示。注意,尽管数组的大小有一个上限,但是数组的大小和系统使用的输入/输出buffer的数量没有固定的联系。

      
      
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
      
      
MediaCodec codec = MediaCodec.createByCodecName(name);
codec.configure(format, …);
codec.start();
ByteBuffer[] inputBuffers = codec.getInputBuffers();
ByteBuffer[] outputBuffers = codec.getOutputBuffers();
for (;;) {
int inputBufferId = codec.dequeueInputBuffer(…);
if (inputBufferId >= 0) {
// fill inputBuffers[inputBufferId] with valid data
codec.queueInputBuffer(inputBufferId, …);
}
int outputBufferId = codec.dequeueOutputBuffer(…);
if (outputBufferId >= 0) {
// outputBuffers[outputBufferId] is ready to be processed or rendered.
codec.releaseOutputBuffer(outputBufferId, …);
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
outputBuffers = codec.getOutputBuffers();
} else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// Subsequent data will conform to new format.
MediaFormat format = codec.getOutputFormat();
}
}
codec.stop();
codec.release();

处理 End-of-stream

当到达输入数据的末尾时,在调用queueInputBuffer时,必须指定BUFFER_FLAG_END_OF_STREAM标志,发送给codec,可以在最后一个可用的输入buffer或者额外提交一个空的输入buffer设置end-of-stream标志。如果使用空buffer,其时间戳将被忽略。
codec会持续返回输出buffer,直到收到带有与MediaCodec.BufferInfo中end-of-stream标志相同的dequeueOutputBuffer或者返回onOutputBufferAvailable时停止,可以在最后一个有效的输出buffer上设置,也可以在最后一个有效输出buffer之后的空buffer中设置。
在输入流结束信号发送后,不要在提交额外的输入buffer,除非codec已经flushed或者stoped——restarted。

使用输出Surface

当使用输出Surface时,数据处理和ByteBuffer模式基本相同,但是不能获取输出buffer,值为null,如 getOutputBuffer/Image(int) 将会返回null;getOutputBuffers() 返回一个null数组。
使用输出Surface时,可以选择是否在surface上渲染每一个输出buffer,有以下三种方式:

  • 不渲染buffer:调用releaseOutputBuffer(bufferId, false);
  • 根据默认时间戳渲染buffer:调用releaseOutputBuffer(bufferId, true);
  • 根据指定时间戳渲染buffer:调用releaseOutputBuffer(bufferId, timestamp)。

从android M后,默认的时间戳是buffer的显示时间戳(纳秒),之前版本没有定义。另外,android M后,可以通过setOutputSurface动态改变输出Surface。

渲染到Surface时的转换

如果codec配置为Surface模式,任何裁剪矩形、旋转、缩放将会自动应用。

在android M之前,渲染到Surface时,软解可能没有应用旋转,也没有标准或者简单的方法识别软解,只能尝试看是否已应用旋转。
当渲染到Surface时,像素的长宽比未被考虑,意味着如果要保证恰当的最终城乡长宽比,在使用VIDEO_SCALING_MODE_SCALE_TO_FIT模式时,必须定位输出Surface。相反,只能对方形像素的内容使用VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING模式。
从android N 开始,视屏旋转90或者270度时,VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING模式可能无法正常工作。
当设置视频缩放模式时,注意每次输出buffer更改后都要reset。由于INFO_OUTPUT_BUFFERS_CHANGED事件deprecated,可以在每次输出格式更改后进行reset。

使用输入Surface

当使用输入Surface时,没有可访问的输入buffer,这是因为buffer自动的从输入Surface传到codec,调用dequeueInputBuffer将会抛出IllegalStateException异常,getInputBuffers() 返回的是一个不可写的伪ByteBuffer[]。
调用 signalEndOfInputStream() 传递 end-of-stream标志后,输入Surface就会停止向codec提交数据。

seeking和自适应播放支持

无论是否支持并配置为自适应播放,视频解码器(以及一些压缩数据的编码器)在seek和格式更改的行为都不同。可以通过CodecCapatilities.isFeatureSupported(String)检查解码器是否支持自适应播放。视频解码器的自适应播放只有在将codec配置到Surface上时,才会被激活。

流边界和关键帧

重要的是,在start()或者flush()后输入数据要在合适的流边界开始:第一帧必须是关键帧。
关键帧可以通过自身被完全解码(大多数codec的I帧),并且关键帧之后没有帧要显示指的是关键帧之前的帧。
下表对不同视频格式合适的关键帧进行了总结:
Format | Suitable key frame
———— | ————-
VP9/VP8 | a suitable intraframe where no subsequent frames refer to frames prior to this frame.(There is no specific name for such key frame.)
H.265 HEVC | IDR or CRA
H.264 AVC | IDR
MPEG-4
H.263
MPEG-2 | a suitable I-frame where no subsequent frames refer to frames prior to this frame.(There is no specific name for such key frame.)

## 不支持自适应播放的解码器(包括不解码到Surface)
为了开始解码与之前提交数据不相邻的数据,必须flush解码器。由于所有的输出buffer在flush时被立即撤销,所以需要首先发送信号,等到end-of-stream标志时在flush。重要的是,刷新后的输入数据在合适的流边界/关键帧开始。
对于某些视频格式,即H.264,H.265,VP8和VP9,也可以改变画面大小或配置中间流。为此,须将整个新的codec-specific configuration data与关键帧一起打包到单个buffer(包括任何起始代码)中,并将其作为常规输入buffer提交。
在图像大小更改发生之后及在返回新尺寸的任何帧之前,可以从dequeueOutputBuffer或onOutputFormatChanged回调中获得INFO_OUTPUT_FORMAT_CHANGED返回值。

就像codec-specific configuration data一样,在更改图片大小后不久,调用flush()时要小心,如果没有收到图片尺寸更改的确认,需要重新请求图片大小。

错误处理

工厂方法createByCodecName和createDecoder / EncoderByType抛出IOException异常,必须捕获或声明传递。当不允许该方法调用该codec状态时,MediaCodec方法会抛出IllegalStateException异常,通常是由于应用程序API使用不正确。涉及安全buffer的方法可能会抛出MediaCodec.CryptoException异常,该错误的详细信息可从getErrorCode()获取。
内部codec错误导致MediaCodec.CodecException异常,即使应用程序正确使用API,也可能由于媒体内容损坏,硬件故障,资源耗尽等引起此异常。接收到CodecException异常时,建议操作可以通过调用isRecoverable()和isTransient()来确定:

  • recoverable errors(可恢复的错误):如果isRecoverable()返回true,则调用stop(),configure(…)和start()来恢复。
  • transient errors(瞬时错误):如果isTransient()返回true,则资源暂时不可用,并且可能会在稍后重试该方法。
  • fatal errors(致命错误):如果bothRecoverable()和isTransient()都返回false,则CodecException是致命的,codec必须rest或released。

    isRecoverable()和isTransient()不会同时返回true

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/684814
推荐阅读
相关标签
  

闽ICP备14008679号