热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

androidcamera采集、H264编码与Rtmp推流

MediaPlus是基于FFmpeg从零开发的android多媒体组件,主要包括:采集,编码,同步,推流&#x


MediaPlus是基于FFmpeg从零开发的android多媒体组件,主要包括:采集,编码,同步,推流,滤镜及直播及短视频比较通用的功能等,后续功能的新增都会有相应文档更新,感谢关注。

  • android相机的视频采集格式比较多 ,如:NV21,NV12,YV12等。他们之间的区别就是U,V排列顺序不一致,具体YUV相关内容可以看看其他详细的文档,如:[总结]FFMPEG视音频编解码零基础学习方法。

需要了解的就是:YUV采样,数据分布及空间大小计算。
YUV采样:

24439730_13282389538k8V.jpg

YUV420P YUV排序如下图:

1346422959_6364.png

NV12,NV21,YV12,I420都属于YUV420,但是YUV420 又分为YUV420P,YUV420SP,P与SP区别就是,前者YUV420P UV顺序存储,而YUV420SP则是UV交错存储,这是最大的区别,具体的yuv排序就是这样的:
I420: YYYYYYYY UU VV ->YUV420P
YV12: YYYYYYYY VV UU ->YUV420P
NV12: YYYYYYYY UVUV ->YUV420SP
NV21: YYYYYYYY VUVU ->YUV420SP

那么H264编码,为什么需要把android 相机采集的NV21数据转换成YUV420P?
刚开始对这些颜色格式也很模糊,后来找到了真理:因为H264编码必须要用 I420, 所以这里必须要处理色彩格式转换。
MediaPlus采集视频数据为NV21格式,以下描述如何获取android camera采集的每一帧数据,并处理色彩格式转换,代码如下:

  • 获取相机采集数据:

    mCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);mParams = mCamera.getParameters();setCameraDisplayOrientation(this, Camera.CameraInfo.CAMERA_FACING_BACK, mCamera);mParams.setPreviewSize(SRC_FRAME_WIDTH, SRC_FRAME_HEIGHT);mParams.setPreviewFormat(ImageFormat.NV21); //preview format:NV21
    mParams.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);m_camera.setDisplayOrientation(90);mCamera.setParameters(mParams); // setting camera parametersm_camera.addCallbackBuffer(m_nv21);m_camera.setPreviewCallbackWithBuffer(this);m_camera.startPreview();@Overridepublic void onPreviewFrame(byte[] data, Camera camera) {// TODO Auto-generated method stub//data这里就是获取到的NV21数据m_camera.addCallbackBuffer(m_nv21);//这里要添加一次缓冲,否则onPreviewFrame可能不会再被回调}

因为NV21数据的所需空间大小(字节)=宽 x 高 x 3 / 2 (y=WxH,u=WxH/4,v=WxH/4);所以我们需要建立一个byte数组,作为采集视频数据的缓冲区.
MediaPlus>>app.mobile.nativeapp.com.libmedia.core.streamer.RtmpPushStreamer 类主要采集音视频数据,并交由底层处理;有两个线程分别用于处理音视频,AudioThread 、VideoThread.

  • 首先看下VideoThread


/*** 视频采集线程*/class VideoThread extends Thread {public volatile boolean m_bExit = false;byte[] m_nv21Data = new byte[mVideoSizeConfig.srcFrameWidth* mVideoSizeConfig.srcFrameHeight * 3 / 2];byte[] m_I420Data = new byte[mVideoSizeConfig.srcFrameWidth* mVideoSizeConfig.srcFrameHeight * 3 / 2];byte[] m_RotateData = new byte[mVideoSizeConfig.srcFrameWidth* mVideoSizeConfig.srcFrameHeight * 3 / 2];byte[] m_MirrorData = new byte[mVideoSizeConfig.srcFrameWidth* mVideoSizeConfig.srcFrameHeight * 3 / 2];@Overridepublic void run() {// TODO Auto-generated method stubsuper.run();VideoCaptureInterface.GetFrameDataReturn ret;while (!m_bExit) {try {Thread.sleep(1, 10);if (m_bExit) {break;}} catch (InterruptedException e) {e.printStackTrace();}ret = mVideoCapture.GetFrameData(m_nv21Data,m_nv21Data.length);if (ret == VideoCaptureInterface.GetFrameDataReturn.RET_SUCCESS) {frameCount++;LibJniVideoProcess.NV21TOI420(mVideoSizeConfig.srcFrameWidth, mVideoSizeConfig.srcFrameHeight, m_nv21Data, m_I420Data);if (curCameraType == VideoCaptureInterface.CameraDeviceType.CAMERA_FACING_FRONT) {LibJniVideoProcess.MirrorI420(mVideoSizeConfig.srcFrameWidth, mVideoSizeConfig.srcFrameHeight, m_I420Data, m_MirrorData);LibJniVideoProcess.RotateI420(mVideoSizeConfig.srcFrameWidth, mVideoSizeConfig.srcFrameHeight, m_MirrorData, m_RotateData, 90);} else if (curCameraType == VideoCaptureInterface.CameraDeviceType.CAMERA_FACING_BACK) {LibJniVideoProcess.RotateI420(mVideoSizeConfig.srcFrameWidth, mVideoSizeConfig.srcFrameHeight, m_I420Data, m_RotateData, 90);}encodeVideo(m_RotateData, m_RotateData.length);}}}public void stopThread() {m_bExit = true;}}

为什么要旋转?
实际上android camera采集的时候,不管手机是纵向还是横向,视频都是横向进行采集,这样当手机纵向的时候,就会有角度差异;前置需要旋转270°,后置旋转90°,这样就能保证采集到的图像和手机方向是一致的。

处理镜像的原因是因为前置相机采集的图像默认就是镜像的,再做一次镜像,将图像还原回去。
MediaPlus中,使用libyuv来处理转换、旋转、镜像等。
MediaPlus>>app.mobile.nativeapp.com.libmedia.core.jni.LibJniVideoProcess 提供应用层接口

package app.mobile.nativeapp.com.libmedia.core.jni;import app.mobile.nativeapp.com.libmedia.core.config.MediaNativeInit;/*** 色彩空间处理* Created by android on 11/16/17.*/public class LibJniVideoProcess {static {MediaNativeInit.InitMedia();}/*** NV21转换I420** @param in_width 输入宽度* @param in_height 输入高度* @param srcData 源数据* @param dstData 目标数据* @return*/public static native int NV21TOI420(int in_width, int in_height,byte[] srcData,byte[] dstData);/*** 镜像I420* @param in_width 输入宽度* @param in_height 输入高度* @param srcData 源数据* @param dstData 目标数据* @return*/public static native int MirrorI420(int in_width, int in_height,byte[] srcData,byte[] dstData);/*** 指定角度旋转I420* @param in_width 输入宽度* @param in_height 输入高度* @param srcData 源数据* @param dstData 目标数据*/public static native int RotateI420(int in_width, int in_height,byte[] srcData,byte[] dstData, int rotationValue);}

libmedia/src/cpp/jni/jni_Video_Process.cpp 图像处理JNI层,libyuv比较强大,包括了所有YUV的转换等其他处理,简单描述下函数参数,如:

LIBYUV_API
int NV21ToI420(const uint8* src_y, int src_stride_y,const uint8* src_vu, int src_stride_vu,uint8* dst_y, int dst_stride_y,uint8* dst_u, int dst_stride_u,uint8* dst_v, int dst_stride_v,int width, int height);

  • src_y :y分量存储空间
  • src_stride_y :y分量宽度数据长度
  • src_vu:uv分量存储空间
  • src_stride_uv:uv分量宽度数据长度
  • dst_y :目标y分量存储空间
  • dst_u :目标u分量存储空间
  • dst_v :目标v分量存储空间
  • dst_stride_y:目标y分量宽度数据长度
  • dst_stride_u:目标v分量宽度数据长度
  • dst_stride_v:目标u分量宽度数据长度
  • width: 视频宽
  • height:视频高
  • 假设,一个8(宽)x6(高)的图像,函数参数如下:

int width=8;
int height=6;
//源数据存储空间
uint8_t *srcNV21Data;
//目标存储空间
uint8_t *dstI420Data;src_y=srcNV21Data;
src_uv=srcNV21Data + (widthxheight);
src_stride_y=width;
src_stride_uv=width/2;dst_y=dstI420Data;
dst_u=dstI420Data+(widthxheight);
dst_v=dstI420Data+(widthxheightx5/4);
dst_stride_y=width;
dst_stride_u=width/2;
dst_stride_v=width/2;

以下是调用libyuv完成图像转换、旋转、镜像的代码:

//
// Created by developer on 11/16/17.
//#include "jni_Video_Process.h"#ifdef __cplusplus
extern "C" {
#endifJNIEXPORT jint JNICALL
Java_app_mobile_nativeapp_com_libmedia_core_jni_LibJniVideoProcess_NV21TOI420(JNIEnv *env,class type,jin in_width,jin in_height,jbyteArray srcData_,jbyteArray dstData_) {jbyte *srcData = env->GetByteArrayElements(srcData_, NULL);jbyte *dstData = env->GetByteArrayElements(dstData_, NULL);VideoProcess::NV21TOI420(in_width, in_height, (const uint8_t *) srcData,(uint8_t *) dstData);return 0;
}JNIEXPORT jint JNICALL
Java_app_mobile_nativeapp_com_libmedia_core_jni_LibJniVideoProcess_MirrorI420(JNIEnv *env,class type,jin in_width,jin in_height,jbyteArray srcData_,jbyteArray dstData_) {jbyte *srcData = env->GetByteArrayElements(srcData_, NULL);jbyte *dstData = env->GetByteArrayElements(dstData_, NULL);VideoProcess::MirrorI420(in_width, in_height, (const uint8_t *) srcData,(uint8_t *) dstData);return 0;
}JNIEXPORT jint JNICALL
Java_app_mobile_nativeapp_com_libmedia_core_jni_LibJniVideoProcess_RotateI420(JNIEnv *env,class type,jin in_width,jin in_hegith,jbyteArray srcData_,jbyteArray dstData_,jint rotationValue) {jbyte *srcData = env->GetByteArrayElements(srcData_, NULL);jbyte *dstData = env->GetByteArrayElements(dstData_, NULL);return VideoProcess::RotateI420(in_width, in_hegith, (const uint8_t *) srcData,(uint8_t *) dstData, rotationValue);
}#ifdef __cplusplus
}
#endif

以上代码完成NV21转换为I420等处理,接下来将数据传入底层,就可以使用FFmpeg进行H264编码了,下图是底层C++封装类图:


类图说明了,MediaEncoder依赖于MediaCapture,MediaPushStreamer依赖MediaEncoder的相互关系。VideoCapture接收视频数据缓存至videoCaptureframeQueue,AudioCapture接收音频数据缓存至audioCaptureframeQueue,这样RtmpPushStreamer就可以调用MediaEncoder完成音视频编码,并推流。

MediaPlus>>app.mobile.nativeapp.com.libmedia.core.streamer.RtmpPushStreamer,InitNative()中调用了 initCapture()用于初始化接收音视频数据的两个类及initEncoder()初始化音视频编码器,当调用startPushStream开始直播推流时,经JNI方法LiveJniMediaManager.StartPush(pushUrl)开始底层编码推流。

/*** 初始化底层采集与编码器*/private boolean InitNative() {if (!initCapture()) {return false;}if (!initEncoder()) {return false;}Log.d("initNative", "native init success!");nativeInt &#61; true;return nativeInt;}/*** 开启推流* &#64;param pushUrl* &#64;return*/private boolean startPushStream(String pushUrl) {if (nativeInt) {int ret &#61; 0;ret &#61; LiveJniMediaManager.StartPush(pushUrl);if (ret <0) {Log.d("initNative", "native push failed!");return false;}return true;}return false;}

以下是开启推流时的JNI层调用&#xff1a;

*** 开始推流*/
JNIEXPORT jint JNICALL
Java_app_mobile_nativeapp_com_libmedia_core_jni_LiveJniMediaManager_StartPush(JNIEnv *env,jclass type,jstring url_) {mMutex.lock();if (videoCaptureInit && audioCaptureInit) {startStream &#61; true;isClose &#61; false;videoCapture->StartCapture();audioCapture->StartCapture();const char *url &#61; env->GetStringUTFChars(url_, 0);rtmpStreamer &#61; RtmpStreamer::Get();//初始化推流器if (rtmpStreamer->InitStreamer(url) !&#61; 0) {LOG_D(DEBUG, "jni initStreamer success!");mMutex.unlock();return -1;}rtmpStreamer->SetVideoEncoder(videoEncoder);rtmpStreamer->SetAudioEncoder(audioEncoder);if (rtmpStreamer->StartPushStream() !&#61; 0) {LOG_D(DEBUG, "jni push stream failed!");videoCapture->CloseCapture();audioCapture->CloseCapture();rtmpStreamer->ClosePushStream();mMutex.unlock();return -1;}LOG_D(DEBUG, "jni push stream success!");env->ReleaseStringUTFChars(url_, url);}mMutex.unlock();return 0;
}

AudioCapture\VideoCapture用于接收应用层传入的音视频数据及采集参数&#xff0c;libyuv转换的I420&#xff0c;LiveJniMediaManager.StartPush(pushUrl)调用后&#xff0c; videoCapture->StartCapture() VideoCapture就可以接收到上层传入音视频数据&#xff0c;

LiveJniMediaManager.EncodeH264(videoBuffer, length);JNIEXPORT jint JNICALL
Java_app_mobile_nativeapp_com_libmedia_core_jni_LiveJniMediaManager_EncodeH264(JNIEnv *env,jclass type,jbyteArray videoBuffer_,jint length) {if (videoCaptureInit && !isClose) {jbyte *videoSrc &#61; env->GetByteArrayElements(videoBuffer_, 0);uint8_t *videoDstData &#61; (uint8_t *) malloc(length);memcpy(videoDstData, videoSrc, length);OriginData *videoOriginData &#61; new OriginData();videoOriginData->size &#61; length;videoOriginData->data &#61; videoDstData;videoCapture->PushVideoData(videoOriginData);env->ReleaseByteArrayElements(videoBuffer_, videoSrc, 0);}return 0;
}

VideoCapture接收到数据后缓存至同步队列&#xff1a;

/*** 往队列中添加视频数据*/
int VideoCapture::PushVideoData(OriginData *originData) {if (ExitCapture) {return 0;}originData->pts &#61; av_gettime();LOG_D(DEBUG,"video capture pts :%lld",originData->pts);videoCaputureframeQueue.push(originData);return originData->size;
}

libmedia/src/main/cpp/core/VideoEncoder.cpp
libmedia/src/main/cpp/core/RtmpStreamer.cpp
这两个类是核心&#xff0c;前者负责编码视频&#xff0c;后者用于Rtmp推流,从前面的JNI调用开始推流 rtmpStreamer->SetVideoEncoder(videoEncoder)&#xff0c;可以看出来RtmpStreamer依赖VideoEncoder类&#xff0c;接下来说明下相互间如何完成编码及推流&#xff1a;


/**
* 视频编码任务
*/
void *RtmpStreamer::PushVideoStreamTask(void *pObj) {RtmpStreamer *rtmpStreamer &#61; (RtmpStreamer *) pObj;rtmpStreamer->isPushStream &#61; true;if (NULL &#61;&#61; rtmpStreamer->videoEncoder) {return 0;}VideoCapture *pVideoCapture &#61; rtmpStreamer->videoEncoder->GetVideoCapture();AudioCapture *pAudioCapture &#61; rtmpStreamer->audioEncoder->GetAudioCapture();if (NULL &#61;&#61; pVideoCapture) {return 0;}int64_t beginTime &#61; av_gettime();int64_t lastAudioPts &#61; 0;while (true) {if (!rtmpStreamer->isPushStream ||pVideoCapture->GetCaptureState()) {break;}OriginData *pVideoData &#61; pVideoCapture->GetVideoData();
// OriginData *pAudioData &#61; pAudioCapture->GetAudioData();//h264 encodeif (pVideoData !&#61; NULL && pVideoData->data) {
// if(pAudioData&&pAudioData->pts>pVideoData->pts){
// int64_t overValue&#61;pAudioData->pts-pVideoData->pts;
// pVideoData->pts&#43;&#61;overValue&#43;1000;
// LOG_D(DEBUG, "synchronized video audio pts videoPts:%lld audioPts:%lld", pVideoData->pts,pAudioData->pts);
// }pVideoData->pts &#61; pVideoData->pts - beginTime;LOG_D(DEBUG, "before video encode pts:%lld", pVideoData->pts);rtmpStreamer->videoEncoder->EncodeH264(&pVideoData);LOG_D(DEBUG, "after video encode pts:%lld", pVideoData->avPacket->pts);}if (pVideoData !&#61; NULL && pVideoData->avPacket->size > 0) {rtmpStreamer->SendFrame(pVideoData, rtmpStreamer->videoStreamIndex);}}return 0;
}int RtmpStreamer::StartPushStream() {videoStreamIndex &#61; AddStream(videoEncoder->videoCodecContext);audioStreamIndex &#61; AddStream(audioEncoder->audioCodecContext);pthread_create(&t3, NULL, RtmpStreamer::WriteHead, this);pthread_join(t3, NULL);VideoCapture *pVideoCapture &#61; videoEncoder->GetVideoCapture();AudioCapture *pAudioCapture &#61; audioEncoder->GetAudioCapture();pVideoCapture->videoCaputureframeQueue.clear();pAudioCapture->audioCaputureframeQueue.clear();if(writeHeadFinish) {pthread_create(&t1, NULL, RtmpStreamer::PushAudioStreamTask, this);pthread_create(&t2, NULL, RtmpStreamer::PushVideoStreamTask, this);}else{return -1;}
// pthread_create(&t2, NULL, RtmpStreamer::PushStreamTask, this);
// pthread_create(&t2, NULL, RtmpStreamer::PushStreamTask, this);return 0;
}

rtmpStreamer->StartPushStream()调用了&#xff0c;RtmpStreamer::StartPushStream();
在RtmpStreamer::StartPushStream()中&#xff0c;开起新的线程:

pthread_create(&t1, NULL, RtmpStreamer::PushAudioStreamTask, this);pthread_create(&t2, NULL, RtmpStreamer::PushVideoStreamTask, this);

在PushVideoStreamTask主要有以下调用:

  • 从VideoCapture队列中获取缓存的数据pVideoCapture->GetVideoData().
  • 计算PTS&#xff1a;pVideoData->pts &#61; pVideoData->pts - beginTime.
  • 编码器完成编码:rtmpStreamer->videoEncoder->EncodeH264(&pVideoData).
  • rtmpStreamer->SendFrame(pVideoData, rtmpStreamer->videoStreamIndex) 完成推流.

这样就完成了编码与推流的整个流程&#xff0c;那么是如何完成编码的?
因为在开启推流之前&#xff0c;就已经初始化了编码器&#xff0c;所以RtmpStreamer只需要调用VideoEncoder编码&#xff0c;其实VideoCapture,RtmpStreamer二者就是生产者与消费者的模式。
VideoEncoder::EncodeH264();正是完成了推流前的重要部分-视频编码。

int VideoEncoder::EncodeH264(OriginData **originData) {av_image_fill_arrays(outputYUVFrame->data,outputYUVFrame->linesize, (*originData)->data,AV_PIX_FMT_YUV420P, videoCodecContext->width,videoCodecContext->height, 1);outputYUVFrame->pts &#61; (*originData)->pts;int ret &#61; 0;ret &#61; avcodec_send_frame(videoCodecContext, outputYUVFrame);if (ret !&#61; 0) {
#ifdef SHOW_DEBUG_INFOLOG_D(DEBUG, "avcodec video send frame failed");
#endif}av_packet_unref(&videoPacket);ret &#61; avcodec_receive_packet(videoCodecContext, &videoPacket);if (ret !&#61; 0) {
#ifdef SHOW_DEBUG_INFOLOG_D(DEBUG, "avcodec video recieve packet failed");
#endif}(*originData)->Drop();(*originData)->avPacket &#61; &videoPacket;
#ifdef SHOW_DEBUG_INFOLOG_D(DEBUG, "encode video packet size:%d pts:%lld", (*originData)->avPacket->size,(*originData)->avPacket->pts);LOG_D(DEBUG, "Video frame encode success!");
#endif(*originData)->avPacket->size;return videoPacket.size;
}

以上就是H264编码的核心代码了&#xff0c;填充AVFrame&#xff0c;再完成编码&#xff0c;AVFrame data中存储的是编码前的数据&#xff0c;经编码后AVPacket data中存储的是压缩编码后的数据&#xff0c;再通过 RtmpStreamer::SendFrame()将编码后的数据发送出去。发送过程中&#xff0c;需要转换PTS&#xff0c;DTS时间基数&#xff0c;将本地编码器的时间基数&#xff0c;转换为AVStream中的时间基数。


int RtmpStreamer::SendFrame(OriginData *pData, int streamIndex) {std::lock_guard lk(mut1);AVRational stime;AVRational dtime;AVPacket *packet &#61; pData->avPacket;packet->stream_index &#61; streamIndex;LOG_D(DEBUG, "write packet index:%d index:%d pts:%lld", packet->stream_index, streamIndex,packet->pts);//判断是音频还是视频if (packet->stream_index &#61;&#61; videoStreamIndex) {stime &#61; videoCodecContext->time_base;dtime &#61; videoStream->time_base;}else if (packet->stream_index &#61;&#61; audioStreamIndex) {stime &#61; audioCodecContext->time_base;dtime &#61; audioStream->time_base;}else {LOG_D(DEBUG, "unknow stream index");return -1;}packet->pts &#61; av_rescale_q(packet->pts, stime, dtime);packet->dts &#61; av_rescale_q(packet->dts, stime, dtime);packet->duration &#61; av_rescale_q(packet->duration, stime, dtime);int ret &#61; av_interleaved_write_frame(iAvFormatContext, packet);if (ret &#61;&#61; 0) {if (streamIndex &#61;&#61; audioStreamIndex) {LOG_D(DEBUG, "---------->write &#64;&#64;&#64;&#64;&#64;&#64;&#64;&#64;&#64; frame success------->!");} else if (streamIndex &#61;&#61; videoStreamIndex) {LOG_D(DEBUG, "---------->write ######### frame success------->!");}} else {char buf[1024] &#61; {0};av_strerror(ret, buf, sizeof(buf));LOG_D(DEBUG, "stream index %d writer frame failed! :%s", streamIndex, buf);}return 0;
}

以上是MediaPlus H264编码与Rtmp推流的整个流程&#xff0c;相关文章待续......
能力有限&#xff0c;如有纰漏还请指正。

版权声明&#xff1a;本文为原创文章&#xff0c;转载请注明出处。

代码地址&#xff1a;github.com/javandoc/Me…






推荐阅读
  • 深入解析 Android 中 EditText 的 getLayoutParams 方法及其代码应用实例 ... [详细]
  • 本文介绍了一种自定义的Android圆形进度条视图,支持在进度条上显示数字,并在圆心位置展示文字内容。通过自定义绘图和组件组合的方式实现,详细展示了自定义View的开发流程和关键技术点。示例代码和效果展示将在文章末尾提供。 ... [详细]
  • 在Android平台中,播放音频的采样率通常固定为44.1kHz,而录音的采样率则固定为8kHz。为了确保音频设备的正常工作,底层驱动必须预先设定这些固定的采样率。当上层应用提供的采样率与这些预设值不匹配时,需要通过重采样(resample)技术来调整采样率,以保证音频数据的正确处理和传输。本文将详细探讨FFMpeg在音频处理中的基础理论及重采样技术的应用。 ... [详细]
  • 在Android开发中,实现多点触控功能需要使用`OnTouchListener`监听器来捕获触摸事件,并在`onTouch`方法中进行详细的事件处理。为了优化多点触控的交互体验,开发者可以通过识别不同的触摸手势(如缩放、旋转等)并进行相应的逻辑处理。此外,还可以结合`MotionEvent`类提供的方法,如`getPointerCount()`和`getPointerId()`,来精确控制每个触点的行为,从而提升用户操作的流畅性和响应性。 ... [详细]
  • 掌握Android UI设计:利用ZoomControls实现图片缩放功能
    本文介绍了如何在Android应用中通过使用ZoomControls组件来实现图片的缩放功能。ZoomControls提供了一种简单且直观的方式,让用户可以通过点击放大和缩小按钮来调整图片的显示大小。文章详细讲解了ZoomControls的基本用法、布局设置以及与ImageView的结合使用方法,适合初学者快速掌握Android UI设计中的这一重要功能。 ... [详细]
  • Android ListView 自定义 CheckBox 实现列表项多选功能详解
    本文详细介绍了在Android开发中如何在ListView的每一行添加CheckBox,以实现列表项的多选功能。用户不仅可以通过点击复选框来选择项目,还可以通过点击列表的任意一行来完成选中操作,提升了用户体验和操作便捷性。同时,文章还探讨了相关的事件处理机制和布局优化技巧,帮助开发者更好地实现这一功能。 ... [详细]
  • 本文介绍了在 Java 编程中遇到的一个常见错误:对象无法转换为 long 类型,并提供了详细的解决方案。 ... [详细]
  • 本文对比了杜甫《喜晴》的两种英文翻译版本:a. Pleased with Sunny Weather 和 b. Rejoicing in Clearing Weather。a 版由 alexcwlin 翻译并经 Adam Lam 编辑,b 版则由哈佛大学的宇文所安教授 (Prof. Stephen Owen) 翻译。 ... [详细]
  • 优化后的标题:深入探讨网关安全:将微服务升级为OAuth2资源服务器的最佳实践
    本文深入探讨了如何将微服务升级为OAuth2资源服务器,以订单服务为例,详细介绍了在POM文件中添加 `spring-cloud-starter-oauth2` 依赖,并配置Spring Security以实现对微服务的保护。通过这一过程,不仅增强了系统的安全性,还提高了资源访问的可控性和灵活性。文章还讨论了最佳实践,包括如何配置OAuth2客户端和资源服务器,以及如何处理常见的安全问题和错误。 ... [详细]
  • 微信小程序实现类似微博的无限回复功能,内置云开发数据库支持
    本文详细介绍了如何利用微信小程序实现类似于微博的无限回复功能,并充分利用了微信云开发的数据库支持。文中不仅提供了关键代码片段,还包含了完整的页面代码,方便开发者按需使用。此外,HTML页面中包含了一些示例图片,开发者可以根据个人喜好进行替换。文章还将展示详细的数据库结构设计,帮助读者更好地理解和实现这一功能。 ... [详细]
  • 如何高效启动大数据应用之旅?
    在前一篇文章中,我探讨了大数据的定义及其与数据挖掘的区别。本文将重点介绍如何高效启动大数据应用项目,涵盖关键步骤和最佳实践,帮助读者快速踏上大数据之旅。 ... [详细]
  • C#中实现高效UDP数据传输技术
    C#中实现高效UDP数据传输技术 ... [详细]
  • 本文探讨了在Android应用中实现动态滚动文本显示控件的优化方法。通过详细分析焦点管理机制,特别是通过设置返回值为`true`来确保焦点不会被其他控件抢占,从而提升滚动文本的流畅性和用户体验。具体实现中,对`MarqueeText.java`进行了代码层面的优化,增强了控件的稳定性和兼容性。 ... [详细]
  • 成功实现Asp.Net MVC3网站与MongoDB数据库的高效集成
    我们成功地构建了一个基于Asp.NET MVC3框架的网站,并实现了与MongoDB数据库的高效集成。此次更新不仅完善了基本的创建和显示功能,还全面实现了数据的增删改查操作。在创建功能方面,我们修复了之前代码中的错误,确保每个属性都能正确生成。此外,我们还对数据模型进行了优化,以提高系统的性能和稳定性。 ... [详细]
  • 在Android平台上利用FFmpeg的Swscale组件实现YUV与RGB格式互转
    本文探讨了在Android平台上利用FFmpeg的Swscale组件实现YUV与RGB格式互转的技术细节。通过详细分析Swscale的工作原理和实际应用,展示了如何在Android环境中高效地进行图像格式转换。此外,还介绍了FFmpeg的全平台编译过程,包括x264和fdk-aac的集成,并在Ubuntu系统中配置Nginx和Nginx-RTMP-Module以支持直播推流服务。这些技术的结合为音视频处理提供了强大的支持。 ... [详细]
author-avatar
PHPYeQ
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有