短视频成知识传播重要渠道,抖音将重点支持100位院士、100家科研机构优质内容 Android AVDemo(8):视频编码,H.264 和 H.265 都支持丨音视频工程示例
短视频成知识传播重要渠道,抖音将重点支持100位院士、100家科研机构优质内容 Android AVDemo(8):视频编码,H.264 和 H.265 都支持丨音视频工程示例,
短视频成知识传播重要渠道,抖音将重点支持100位院士、100家科研机构优质内容
未来网北京5月29日电(记者 凌萌)5月29日,中国科普研究所联合抖音发布《短视频平台共创知识传播新生态》(以下简称“报告”),阐述短视频及短视频平台对于知识传播产生的影响。报告指出,社会正在见证一场互联网上的知识大众化浪潮,短视频已经成为知识传播的重要途径。
报告的问卷调查结果显示,越来越多的人通过短视频获取知识,短视频成为学校、书本、线上课程平台、中长视频等途径之外的一大补充学习渠道。
报告指出,短视频开启了知识传播的新入口,它凭借视频的立体、鲜活、丰富展现让知识更容易传播,吸引众多创作者和专业机构进入短视频平台,与亿万用户展开知识互动。
发布会上,抖音还发布了“抖音科普扶持计划”,宣布将鼓励和支持100家科研科普机构(如各地科协、科研院所、学会等)做科普,将重点支持100位院士的科普内容,重点扶持和激励1000多位各类型科普优质创作者生产内容。
报告显示,短视频已经成为知识传播的重要渠道,95%的受访者表示会通过短视频获取知识,而且认为自己平均一周中接触到的知识有55%来自短视频。以抖音平台为例,仅2024年1月新生成的知识类短视频内容数量超过3.37亿个,比2023年7月增长了30%,这些知识类短视频聚集了科学普及、卫生健康、个人理财、历史文化等方方面面的知识。
中国科普研究所科普创作与传播研究室主任、研究员陈玲认为,短视频平台形成的知识传播生态具备高效连接能力。知识网络在规模扩张的同时,支撑创作者、用户和平台方共同高效运作、实现高质量发展。以抖音为例,已有上百个科技机构、上百所双一流高校、上百位知名学者进驻,成为高质量知识的传播源头。
短视频平台用户学习需求的增长、知识创作者增加、知识短视频占比上升,形成了以满足知识需求的可持续业态。优质知识创作者通过短视频平台,可以获得可持续的收益。在抖音,2023年活跃的“知识达人”中约七成通过平台获得了收入,促进了知识创作的职业化。
报告指出,短视频平台兼具娱乐性和学习性,通过营造共创共治的环境,推动资源合理配置,促进了知识普惠和社会价值实现。
会上,抖音平台知识创作者、“星球研究所”合伙人洪天祥以《创作“不可被替代”的科普内容》为题,分享了“星球研究所”抖音账号的创作理念和经验,帮助各界理解科普类创作者应当如何做出内容差异化、实现优质知识内容创作的可持续性。科普中国发展服务中心副主任徐来以“科普中国”抖音号为例,分享了突破“综合科普”困局的思考,探讨公共机构为主体的政务号未来可以如何通过短视频平台创作和传播内容。
图说:抖音知识达人“星球研究所”创始人介绍创作经验
相关创作者代表认为,优质知识内容才是核心竞争力,当下知识类内容创作生态繁荣,活跃于短视频平台的创作者已经是不容忽视的知识传播力量。对于高校、博物馆等知识机构以及科学家和大量专业人士而言,平台不仅能帮他们找到想要学习的用户,还能准确定位用户需求,根据用户评价对好评内容进行精选,从而为有效的知识创作提供保障,这是平台成功吸引大批“大家”“专才”“顶流”进驻并迸发创作热情的主要原因。
在抖音,院士创作者长期受到网友的欢迎,《院士开讲》和《好奇中国》两个项目截至目前已经累计支持超过50位院士的内容,相关节目正片累计播放量超2500万,官方话题累计播放量超15亿。目前,已有8位两院院士在抖音开设账号。
发布会上抖音还宣布将推出“科普扶持计划”,重点鼓励和支持100家科研科普机构(如各地科协、科研院所、学会等)在抖音做科普,重点支持刘嘉麒院士、朱敏院士等100位院士的科普内容,重点扶持和激励超过1000位各类型科普优质创作者生产优质内容。
抖音知识垂类运营相关负责人介绍,抖音将全方位助力权威科普优质内容生产和分发,从运营技巧、直播培训等方面对入驻的科研、科普机构、创作者提供精细化服务。将不断提升作者创作体验,扶持多元稀缺内容,鼓励有创意的表达形式,帮助作者创作出更多、更精彩的好作品,以满足抖音用户日益增强且丰富的知识内容需求。
发布于:北京
Android AVDemo(8):视频编码,H.264 和 H.265 都支持丨音视频工程示例
vx 搜索『gjzkeyframe』 关注『关键帧Keyframe』来及时获得最新的音视频技术文章。
塞尚《樱桃和桃子》
iOS/Android 客户端开发同学如果想要开始学习音视频开发,最丝滑的方式是对音视频基础概念知识有一定了解后,再借助 iOS/Android 平台的音视频能力上手去实践音视频的采集 → 编码 → 封装 → 解封装 → 解码 → 渲染过程,并借助音视频工具来分析和理解对应的音视频数据。
在音视频工程示例这个栏目,我们将通过拆解采集 → 编码 → 封装 → 解封装 → 解码 → 渲染流程并实现 Demo 来向大家介绍如何在 iOS/Android 平台上手音视频开发。
这里是 Android 第八篇:Android 视频编码 Demo。这个 Demo 里包含以下内容:
1)实现一个视频采集模块;
2)实现两个视频编码模块 ByteBuffer、Surface,支持 H.264/H.265;
3)串联视频采集和编码模块,将采集到的视频数据输入给编码模块进行编码,并存储为文件;
4)详尽的代码注释,帮你理解代码逻辑和原理。
在本文中,我们将详解一下 Demo 的具体实现和源码。读完本文内容相信就能帮你掌握相关知识。
不过,如果你的需求是:1)直接获得全部工程源码;2)想进一步咨询音视频技术问题;3)咨询音视频职业发展问题。可以根据自己的需要考虑是否加入『关键帧的音视频开发圈』。
想要了解视频编码,可以看看这几篇:
《视频编码(1):H.264(AVC)》
《视频编码(2):H.265(HEVC)》
《视频编码(3):H.266(VVC)》
1、视频采集模块
在这个 Demo 中,视频采集模块 KFVideoCapture 的实现与《Android 视频采集 Demo》中一样,这里就不再重复介绍了,其接口如下:
KFIVideoCapture.java
public interface KFIVideoCapture {
///< 视频采集初始化。
public void setup(Context context, KFVideoCaptureConfig config, KFVideoCaptureListener listener, EGLContext eglShareContext);
///< 释放采集实例。
public void release();
///< 开始采集。
public void startRunning();
///< 关闭采集。
public void stopRunning();
///< 是否正在采集。
public boolean isRunning();
///< 获取 OpenGL 上下文。
public EGLContext getEGLContext();
///< 切换摄像头。
public void switchCamera();
}
2、视频 ByteBuffer 编码模块
在实现视频编码模块之前,我们先实现一个视频编码配置类 KFVideoEncoderConfig:
KFVideoEncoderConfig.java
public class KFVideoEncoderConfig {
public Size size = new Size(720,1280);
public int bitrate = 4 * 1024 * 1024;
public int fps = 30;
public int gop = 30 * 4;
public boolean isHEVC = false;
public int profile = MediaCodecInfo.CodecProfileLevel.AVCProfileBaseline;
public int profileLevel = MediaCodecInfo.CodecProfileLevel.AVCLevel1;
public KFVideoEncoderConfig() {
}
}
这里可以配置各种编码参数,但不同机型支持能力不同,通过此配置生成编码格式描述 MediaFormat。
接下来,我们来实现一个视频编码模块 KFByteBufferCodec,编码模块 KFByteBufferCodec 的实现与 《Android 音频编码 Demo》 中一样,这里就不再重复介绍了,其接口如下
KFMediaCodecInterface.java
public interface KFMediaCodecInterface {
public static final int KFMediaCodecInterfaceErrorCreate = -2000;
public static final int KFMediaCodecInterfaceErrorConfigure = -2001;
public static final int KFMediaCodecInterfaceErrorStart = -2002;
public static final int KFMediaCodecInterfaceErrorDequeueOutputBuffer = -2003;
public static final int KFMediaCodecInterfaceErrorParams = -2004;
public static int KFMediaCodeProcessParams = -1;
public static int KFMediaCodeProcessAgainLater = -2;
public static int KFMediaCodeProcessSuccess = 0;
///< 初始化 Codec,第一个参数需告知使用编码还是解码。
public void setup(boolean isEncoder,MediaFormat mediaFormat, KFMediaCodecListener listener, EGLContext eglShareContext);
///< 释放 Codec。
public void release();
///< 获取输出格式描述。
public MediaFormat getOutputMediaFormat();
///< 获取输入格式描述。
public MediaFormat getInputMediaFormat();
///< 处理每一帧数据,编码前与编码后都可以,支持编解码 2 种模式。
public int processFrame(KFFrame frame);
///< 清空 Codec 缓冲区。
public void flush();
}
上面是 KFByteBufferCodec 接口的设计,与音频编码对比区别如下:
1)音频编码使用了继承类 KFAudioByteBufferEncoder,视频编码则直接使用类 KFByteBufferCodec。
音频编码使用了继承类 KFByteBufferCodec,目的是切割合适大小的数据 2048 送入编码器,因为 AAC 数据编码每帧大小为 1024 * 2(位深 16 Bit)。
视频编码使用了类 KFByteBufferCodec。
2)外层使用构造方法时配置参数修改:
setup 接口 mInputMediaFormat 需要设置视频编码的格式描述。
更具体细节见上述代码及其注释。
3、视频 Surface 编码模块
接下来,我们来实现一个视频编码模块 KFVideoSurfaceEncoder,在这里输入采集后的数据,输出编码后的数据,同样也需要实现接口 KFMediaCodecInterface,参考模块 KFByteBufferCodec。
KFVideoSurfaceEncoder.java
public class KFVideoSurfaceEncoder implements KFMediaCodecInterface {
private static final String TAG = "KFVideoSurfaceEncoder";
private KFMediaCodecListener mListener = null; ///< 回调。
private KFGLContext mEGLContext = null; ///< GL 上下文。
private KFGLFilter mFilter = null; ///< 渲染到 Surface 特效。
private MediaCodec mEncoder = null; ///< 编码器。
private Surface mSurface = null; ///< 渲染 Surface 缓存。
private HandlerThread mEncoderThread = null; ///< 编码线程。
private Handler mEncoderHandler = null;
private Handler mMainHandler = new Handler(Looper.getMainLooper()); ///< 主线程。
private MediaCodec.BufferInfo mBufferInfo = new MediaCodec.BufferInfo();
private long mLastInputPts = 0;
private MediaFormat mOutputFormat = null; ///< 输出格式描述。
private MediaFormat mInputFormat = null; ///< 输入格式描述。
public KFVideoSurfaceEncoder() {
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void setup(boolean isEncoder,MediaFormat mediaFormat, KFMediaCodecListener listener, EGLContext eglShareContext) {
mInputFormat = mediaFormat;
mListener = listener;
mEncoderThread = new HandlerThread("KFSurfaceEncoderThread");
mEncoderThread.start();
mEncoderHandler = new Handler((mEncoderThread.getLooper()));
mEncoderHandler.post(()->{
if (mInputFormat == null) {
_callBackError(KFMediaCodecInterfaceErrorParams,"mInputFormat == null");
return;
}
///< 初始化编码器。
boolean setupSuccess = _setupEnocder();
if (setupSuccess) {
mEGLContext = new KFGLContext(eglShareContext,mSurface);
mEGLContext.bind();
///< 初始化特效,用于纹理渲染到编码器 Surface 上。
_setupFilter();
mEGLContext.unbind();
}
});
}
@Override
public MediaFormat getOutputMediaFormat() {
return mOutputFormat;
}
@Override
public MediaFormat getInputMediaFormat() {
return mInputFormat;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public void release() {
mEncoderHandler.post(()->{
///< 释放编码器。
if (mEncoder != null) {
try {
mEncoder.stop();
mEncoder.release();
} catch (Exception e) {
Log.e(TAG, "release: " + e.toString());
}
mEncoder = null;
}
///< 释放 GL 特效上下文。
if (mEGLContext != null) {
mEGLContext.bind();
if (mFilter != null) {
mFilter.release();
mFilter = null;
}
mEGLContext.unbind();
mEGLContext.release();
mEGLContext = null;
}
///< 释放 Surface 缓存。
if (mSurface != null) {
mSurface.release();
mSurface = null;
}
mEncoderThread.quit();
});
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
public int processFrame(KFFrame inputFrame) {
if (inputFrame == null || mEncoderHandler == null) {
return KFMediaCodeProcessParams;
}
KFTextureFrame frame = (KFTextureFrame)inputFrame;
mEncoderHandler.post(()-> {
if (mEncoder != null && mEGLContext != null) {
if (frame.isEnd) {
///< 最后一帧标记。
mEncoder.signalEndOfInputStream();
} else {
///< 最近一帧时间戳。
mLastInputPts = frame.usTime();
mEGLContext.bind();
///< 渲染纹理到编码器 Surface 设置视口。
GLES20.glViewport(0, 0, frame.textureSize.getWidth(), frame.textureSize.getHeight());
mFilter.render(frame);
///< 设置时间戳。
mEGLContext.setPresentationTime(frame.usTime() * 1000);
mEGLContext.swapBuffers();
mEGLContext.unbind();
///< 获取编码后的数据,尽量拿出最多的数据出来,回调给外层。
long outputDts = -1;
while (outputDts < mLastInputPts){
int bufferIndex = 0;
try {
bufferIndex = mEncoder.dequeueOutputBuffer(mBufferInfo, 10 * 1000);
} catch (Exception e) {
Log.e(TAG, "Unexpected MediaCodec exception in dequeueOutputBufferIndex, " + e);
_callBackError(KFMediaCodecInterfaceErrorDequeueOutputBuffer,e.getMessage());
return;
}
if (bufferIndex >= 0) {
ByteBuffer byteBuffer = mEncoder.getOutputBuffer(bufferIndex);
if (byteBuffer != null) {
outputDts = mBufferInfo.presentationTimeUs;
if (mListener != null) {
KFBufferFrame encodeFrame = new KFBufferFrame();
encodeFrame.buffer = byteBuffer;
encodeFrame.bufferInfo = mBufferInfo;
mListener.dataOnAvailable(encodeFrame);
}
} else {
break;
}
try {
mEncoder.releaseOutputBuffer(bufferIndex, false);
} catch (Exception e) {
Log.e(TAG, e.toString());
return;
}
} else {
if (bufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
mOutputFormat = mEncoder.getOutputFormat();
}
break;
}
}
}
}
});
return KFMediaCodeProcessSuccess;
}
@Override
public void flush() {
mEncoderHandler.post(()-> {
///< 刷新缓冲区。
if (mEncoder != null) {
try {
mEncoder.flush();
} catch (Exception e) {
Log.e(TAG, "flush error!" + e);
}
}
});
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private boolean _setupEnocder() {
///< 初始化编码器。
try {
String mimeType = mInputFormat.getString(MediaFormat.KEY_MIME);
mEncoder = MediaCodec.createEncoderByType(mimeType);
mEncoder.configure(mInputFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
} catch (IOException e) {
Log.e(TAG, "createEncoderByType" + e);
_callBackError(KFMediaCodecInterfaceErrorCreate,e.getMessage());
return false;
}
///< 创建 Surface。
mSurface = mEncoder.createInputSurface();
///< 开启编码器。
try {
mEncoder.start();
} catch (Exception e) {
Log.e(TAG, "start" + e );
_callBackError(KFMediaCodecInterfaceErrorStart,e.getMessage());
return false;
}
return true;
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
private void _setupFilter() {
///< 创建渲染模块,渲染到编码器 Surface。
if (mFilter == null) {
mFilter = new KFGLFilter(true, KFGLBase.defaultVertexShader,KFGLBase.defaultFragmentShader);
}
}
private void _callBackError(int error, String errorMsg){
///< 出错回调。
if (mListener != null) {
mMainHandler.post(()->{
mListener.onError(error,TAG + errorMsg);
});
}
}
}
上面是 KFVideoSurfaceEncoder 的实现,与视频编码 KFByteBufferCodec 对比区别如下:
1)数据源输入不同。
KFByteBufferCodec 输入为 YUV 数据 KFBufferFrame。
KFVideoSurfaceEncoder 输入为纹理数据 KFTextureFrame。
2)编码流水线不同。
KFByteBufferCodec 输入 YUV 数据进行编码。
KFVideoSurfaceEncoder 输入为纹理数据,执行 OpenGL 渲染,将纹理渲染到编码器缓存 mSurface。使用 mFilter.render 进行渲染,同时设置时间戳 setPresentationTime,交换前后台缓冲区 swapBuffers ,将纹理数据刷新到了 mSurface。最后取出编码后数据,需要注意 releaseOutputBuffer 方法第 2 个参数 render 设置为 true。
3)使用场景不同。
KFVideoSurfaceEncoder适用于输入数据为纹理的情况,例如采集后添加特效。
KFByteBufferCodec 适用于非纹理数据,例如游戏直播、录屏直播、图片转视频等输入数据为 ByteBuffer,此时没必要再做数据转换。
更具体细节见上述代码及其注释。
4、采集视频数据进行 H.264/H.265 编码和存储
我们在一个 MainActivity 中来实现视频采集及编码逻辑,因为 Android 编码的默认输出 AnnexB 码流格式,所以这里不需要转换。
MainActivity.java
public class MainActivity extends AppCompatActivity {
private KFIVideoCapture mCapture; ///< 采集器。
private KFVideoCaptureConfig mCaptureConfig; ///< 采集配置。
private KFRenderView mRenderView; ///< 渲染视图。
private KFGLContext mGLContext; ///< OpenGL 上下文。
private KFVideoEncoderConfig mEncoderConfig; ///< 编码配置。
private KFMediaCodecInterface mEncoder; ///< 编码。
private FileOutputStream mStream = null;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED ||
ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions((Activity) this,
new String[] {Manifest.permission.CAMERA,Manifest.permission.RECORD_AUDIO, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE},
1);
}
///< 创建 GL 上下文。
mGLContext = new KFGLContext(null);
///< 创建渲染视图。
mRenderView = new KFRenderView(this,mGLContext.getContext());
WindowManager windowManager = (WindowManager)this.getSystemService(this.WINDOW_SERVICE);
Rect outRect = new Rect();
windowManager.getDefaultDisplay().getRectSize(outRect);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(outRect.width(), outRect.height());
addContentView(mRenderView,params);
FrameLayout.LayoutParams startParams = new FrameLayout.LayoutParams(200, 120);
startParams.gravity = Gravity.CENTER_HORIZONTAL;
Button startButton = new Button(this);
startButton.setTextColor(Color.BLUE);
startButton.setText("开始");
startButton.setVisibility(View.VISIBLE);
startButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
///< 创建编码器。
if (mEncoder == null) {
mEncoder = new KFVideoSurfaceEncoder();
MediaFormat mediaFormat = KFAVTools.createVideoFormat(mEncoderConfig.isHEVC,mEncoderConfig.size, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface,mEncoderConfig.bitrate,mEncoderConfig.fps,mEncoderConfig.gop / mEncoderConfig.fps,mEncoderConfig.profile,mEncoderConfig.profileLevel);
mEncoder.setup(true,mediaFormat,mVideoEncoderListener,mGLContext.getContext());
((Button)view).setText("停止");
} else {
mEncoder.release();
mEncoder = null;
((Button)view).setText("开始");
}
}
});
addContentView(startButton, startParams);
///< 创建采集器。
mCaptureConfig = new KFVideoCaptureC[db:内容]?
- 卖2元“奶茶边角料”入账上亿,00后狂买这款“超级零食” 共享充电宝1小时收费近16元!涉事公司回应:门店自主定价
- 如何分辨孩子是不是便秘?便秘了怎么办? 【独家专访】健康体检巨头如何All in AI?对话美年健康总裁徐涛
- “毛孩子”寄养“一窝难求”,宠物市场何时迎来百亿品牌? 菱角是哪里的特产?你知道有几种类型吗?
- 双十一苹果手机几号买最便宜,2024淘宝京东双11手机销量排行榜前十名推荐 上海市市场监管局公布2024民生领域案件查办“铁拳”行动第五批典型案例
- 评论丨双11预售比直接购买更贵?市场早晚会惩罚那些“小聪明” 1元锅底,无限畅吃的小火锅,能吃出什么品质?
- 中国藏式护身符行业市场前景分析预测报告 1.5元骑10分钟 多地共享单车上调起步价
- 客单价涨2元,订单量增长48%!潮界如何把地方菜打成爆品? 这届双十一,天猫、京东豁出去了?这届双十一,平台太卷了双十一大促,传来了好消息。10月15日,据天猫、京东统计,10月14日晚8点双11开启后,家电、美...
- 10月,别忘吃“秋天第一鲜”,10元5斤,特鲜,懂行人都抢着买 下沉市场又火了:一线品牌猛攻,“地头蛇”强势守擂