首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Android平台如何通过OTG外接UVC摄像头实时预览并回传到GB28181平台

Android平台如何通过OTG外接UVC摄像头实时预览并回传到GB28181平台

原创
作者头像
音视频牛哥
发布于 2025-01-02 08:19:02
发布于 2025-01-02 08:19:02
1.1K0
举报
文章被收录于专栏:RTMP推送RTMP推送GB28181技术

如何采集UVC Camera数据

早在2015年,我们在做Android平台RTMP直播推送模块的时候,就有用到UVC摄像头采集,比如移动单兵或采集执法场景,除了Android系统自带的摄像头外,设备端还需要支持UVC外接摄像头。先说什么是UVC吧?实际上,UVC全称为USB Video Class,即:USB视频类,是一种为USB视频捕获设备定义的协议标准。是Microsoft与另外几家设备厂商联合推出的为USB视频捕获设备定义的协议标准,已成为USB org标准之一。

Android平台实现UVC采集,无需重复造轮子,可以参考市面上比较成熟的采集方案,比如libuvccamera之类,非常成熟完善。以下是利用libuvccamera获取 NV12数据的步骤:

我们是先把libuvccamera相关库进行ndk编译,然后把相关java文件或资源打包成aar,然后到demo工程加载:

初始化和打开摄像头

代码语言:java
AI代码解释
复制
public void initCameraHelper() {
	Toast.makeText(this, "initCameraHelper", Toast.LENGTH_SHORT).show();

	if (mCameraHelper == null) {
		mCameraHelper = new CameraHelper();
		mCameraHelper.setStateCallback(mStateListener);
	}
}

设置视频格式预览和获取数据帧

代码语言:java
AI代码解释
复制
private final ICameraHelper.StateCallback mStateListener = new ICameraHelper.StateCallback() {
	@Override
	public void onAttach(UsbDevice device) {
		selectDevice(device);
	}

	@Override
	public void onDeviceOpen(UsbDevice device, boolean isFirstOpen) {
		mCameraHelper.openCamera();
	}

	@Override
	public void onCameraOpen(UsbDevice device) {

		mCameraHelper.startPreview();
		mCameraHelper.addSurface(mCameraViewMain.getHolder().getSurface(), false);
	}

	@Override
	public void onCameraClose(UsbDevice device) {

		if (mCameraHelper != null) {
			mCameraHelper.removeSurface(mCameraViewMain.getHolder().getSurface());
		}

		clearFPS();
	}

	@Override
	public void onDeviceClose(UsbDevice device) {
	}

	@Override
	public void onDetach(UsbDevice device) {
	}

	@Override
	public void onCancel(UsbDevice device) {
	}

};

如果需要对接RTMP推送、GB28181设备接入模块或轻量级RTSP服务,只要在数据回调的地方,把数据投递出去即可;

代码语言:java
AI代码解释
复制
mCameraHelper.setFrameCallback(frame -> {
    ...
	int w = size.width, h = size.height;
	int y_stride =  size.width, uv_stride =  size.width;
	int y_offset = 0, uv_offset =  size.width * size.height;
	int is_vertical_flip = 0, is_horizontal_flip = 0;
	int rotation_degree = 0;
	int scale_w = 0, scale_h = 0, scale_filter_mode = 0;

	for (LibPublisherWrapper i : publisher_array_)
	{
		i.PostLayerImageNV21ByteBuffer(0, 0, 0,
			frame, y_offset, y_stride, frame, uv_offset, uv_stride, w, h,
			is_vertical_flip, is_horizontal_flip, scale_w, scale_h, scale_filter_mode, rotation_degree);
	}

}, UVCCamera.PIXEL_FORMAT_NV21);

如何对接GB28181模块

对大牛直播SDK来说,只要数据源有了,任督二脉也就打通了,数据可以实时录制到MP4文件,可以推送到RTMP,可以对接到轻量级RTSP服务,当然也可以对接到GB28181设备接入模块。

这里,我们对接大牛直播SDK的SmartGBD GB28181设备接入模块为例,Android终端除支持常规的音视频数据接入外,还可以支持移动设备位置(MobilePosition)订阅和通知、语音广播和语音对讲、云台控制回调和预置位查询,支持对接数据类型如下:

  • 编码前数据(目前支持的有YV12/NV21/NV12/I420/RGB24/RGBA32/RGB565等数据类型);
  • 编码后数据(如无人机等264/HEVC数据,或者本地解析的MP4音视频数据);
  • 拉取RTSP或RTMP流并接入至GB28181平台(比如其他IPC的RTSP流,可通过Android平台GB28181接入到国标平台)。

UVC摄像头对接,无非就是回调NV12、NV21或YUV420SP数据,通过调用相关的数据投递接口,实现UVC数据的投递即可。

UVC摄像头插入,APP链接的时候,会有权限提醒:

启动GB28181,国标平台侧发起实时回传请求后,UVC设备侧采集到数据,编码打包并实时传输到平台。

功能支持

  • ​[视频格式]H.264/H.265(Android H.265硬编码);
  • [音频格式]G.711 A律、AAC;
  • [音量调节]Android平台采集端支持实时音量调节;
  • [H.264硬编码]支持H.264特定机型硬编码;
  • [H.265硬编码]支持H.265特定机型硬编码;
  • [软硬编码参数配置]支持gop间隔、帧率、bit-rate设置;
  • [软编码参数配置]支持软编码profile、软编码速度、可变码率设置;
  • 支持纯视频、音视频PS打包传输;
  • 支持RTP OVER UDP和RTP OVER TCP被动模式;
  • 支持信令通道网络传输协议TCP/UDP设置;
  • 支持注册、注销,支持注册刷新及注册有效期设置;
  • 支持设备目录查询应答;
  • 支持心跳机制,支持心跳间隔、心跳检测次数设置;
  • 支持移动设备位置(MobilePosition)订阅和通知;
  • 适用国家标准:GB/T 28181—2016;
  • 支持语音广播;
  • 支持语音对讲;
  • 支持图像抓拍;
  • 支持历史视音频文件检索;
  • 支持历史视音频文件下载;
  • 支持历史视音频文件回放;
  • 支持云台控制和预置位查询;
  • [实时水印]支持动态文字水印、png水印;
  • [镜像]Android平台支持前置摄像头实时镜像功能;
  • [实时静音]支持实时静音/取消静音;
  • [实时快照]支持实时快照;
  • [降噪]支持环境音、手机干扰等引起的噪音降噪处理、自动增益、VAD检测;
  • [外部编码前视频数据对接]支持YUV数据对接;
  • [外部编码前音频数据对接]支持PCM对接;
  • [外部编码后视频数据对接]支持外部H.264数据对接;
  • [外部编码后音频数据对接]外部AAC数据对接;
  • [扩展录像功能]支持和录像SDK组合使用,录像相关功能。​

系统要求

  • SDK支持Android 5.1及以上版本;
  • 支持的CPU架构:armv7, arm64, x86, x86_64。

准备工作

  • 确保SmartPublisherJniV2.java放到com.daniulive.smartpublisher包名下(可在其他包名下调用);
  • 如需集成语音广播、语音对讲功能,确保SmartPlayerJniV2.java放到com.daniulive.smartplayer包名下(可在其他包名下调用);
  • smartavengine.jar和smartgbsipagent.jar加入到工程;
  • 拷贝libSmartPublisher.so和libSmartPlayer.so(如需语音广播或语音对讲)到工程;
  • AndroidManifast.xml添加相关权限:
代码语言:java
AI代码解释
复制
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" ></uses-permission>
<uses-permission android:name="android.permission.INTERNET" ></uses-permission>
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"></uses-permission>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"></uses-permission>
  • Load相关so:
代码语言:java
AI代码解释
复制
static {  
    System.loadLibrary("SmartPublisher");
    System.loadLibrary("SmartPlayer");
}
  • build.gradle配置32/64位库:
代码语言:java
AI代码解释
复制
splits {
    abi {
        enable true
        reset()
        // Specifies a list of ABIs that Gradle should create APKs for
        include 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' //select ABIs to build APKs for
        // Specify that we do not want to also generate a universal APK that includes all ABIs
        universalApk true
    }
}
  • 如需集成到自己系统测试,请用大牛直播SDK的app name,授权版按照授权app name正常使用即可;
  • 如何改app-name,strings.xml做以下修改:
代码语言:java
AI代码解释
复制
<string name="app_name">SmartPublisherSDKDemo</string>

接口详解

以Android平台Camera2对接为例,信令部分需要实现如下标红接口:

代码语言:java
AI代码解释
复制
public class MainActivity extends Activity implements ViewTreeObserver.OnGlobalLayoutListener, Camera2Listener,
        GBSIPAgentListener, GBSIPAgentPlayListener, GBSIPAgentAudioBroadcastListener,
        GBSIPAgentDeviceControlListener, GBSIPAgentQueryCommandListener, 
        GBSIPAgentTalkListener, 
        GBSIPAgentQueryRecordInfoListener{
}

媒体数据处理接口,可参照SmartPublisherJniV2.java,如需语音广播或语音对讲,可参照SmartPlayerJniV2.java。

信令处理

GBSIPAgentListener主要系GB28181注册、心跳、DevicePosition等,如注册成功、注册超时、注册网络传输层错误、心跳异常、设备位置请求处理:

代码语言:java
AI代码解释
复制
public interface GBSIPAgentListener
{
    /*注册成功
    * @param dateString: 服务器日期,用来校准设备端时间,用户自行决定是否校准设备时间
    */
    void ntsRegisterOK(String dateString);

    /*
    *注册超时
    */
    void ntsRegisterTimeout();

    /*
    *注册网络传输层异常
    */
    void ntsRegisterTransportError(String errorInfo);

    /*
    *心跳达到异常次数
    */
    void ntsOnHeartBeatException(int exceptionCount, String lastExceptionInfo);

    /*
     * 设备位置请求, 这个主要用在移动设备位置订阅上
     * @param interval 请求间隔, 单位是毫秒
     */
    void ntsOnDevicePositionRequest(String deviceId, int interval);
}

GBSIPAgentPlayListener主要系GB28181的Invite、Ack、Bye等处理:

代码语言:java
AI代码解释
复制
public interface GBSIPAgentPlayListener {

    /*
     *收到s=Play的实时视音频点播
     */
    void ntsOnInvitePlay(String deviceId, SessionDescription sessionDescription);

    /*
     *发送play invite response 异常
     */
    void ntsOnPlayInviteResponseException(String deviceId, int statusCode, String errorInfo);

    /*
     * 收到CANCEL play INVITE请求
     */
    void ntsOnCancelPlay(String deviceId);

    /*
     * 收到Ack
     */
    void ntsOnAckPlay(String deviceId);

    /*
     * 收到Bye
     */
    void ntsOnByePlay(String deviceId);

    /*
     * 不是在收到BYE Message情况下, 终止Play
     */
    void ntsOnTerminatePlay(String deviceId);

    /*
     * Play会话对应的对话终止, 一般不会出发这个回调,目前只有在响应了200K, 但在64*T1时间后还没收到ACK,才可能会出发
    收到这个, 请做相关清理处理
    */
    void ntsOnPlayDialogTerminated(String deviceId);
}

GBSIPAgentAudioBroadcastListener主要系GB28181语音广播处理相关,如有语音广播相关需求,可参照demo实例实现:

代码语言:java
AI代码解释
复制
public interface GBSIPAgentAudioBroadcastListener {

    /*
     *收到语音广播通知
     */
    void ntsOnNotifyBroadcastCommand(String fromUserName, String fromUserNameAtDomain, String sn, String sourceID, String targetID);

    /*
     *需要准备接受语音广播的SDP内容
     */
    void ntsOnAudioBroadcast(String commandFromUserName, String commandFromUserNameAtDomain, String sourceID, String targetID);

    /*
     *音频广播, 发送Invite请求异常
     */
    void ntsOnInviteAudioBroadcastException(String sourceID, String targetID, String errorInfo);

    /*
     *音频广播, 等待Invite响应超时
     */
    void ntsOnInviteAudioBroadcastTimeout(String sourceID, String targetID);

    /*
     *音频广播, 收到Invite消息最终响应
     */
    void ntsOnInviteAudioBroadcastResponse(String sourceID, String targetID, int statusCode, SessionDescription sessionDescription);

    /*
     * 音频广播, 收到BYE Message
     */
    void ntsOnByeAudioBroadcast(String sourceID, String targetID);


    /*
     * 不是在收到BYE Message情况下, 终止音频广播
     */
    void ntsOnTerminateAudioBroadcast(String sourceID, String targetID);
}

GBSIPAgentDeviceControlListener主要系GB28181设备控制相关,比如远程启动、云台控制:

代码语言:java
AI代码解释
复制
public interface GBSIPAgentDeviceControlListener {

    /*
     * 收到远程启动控制命令
     */
    void ntsOnDeviceControlTeleBootCommand(String deviceId, String teleBootValue);

    /*
    * 云台控制
     */
    void ntsOnDeviceControlPTZCmd(String deviceId, String typeValue);
}

GBSIPAgentQueryCommandListener主要系GB28181查询命令,如预置位查询:

代码语言:java
AI代码解释
复制
public interface GBSIPAgentQueryCommandListener {

    /*
     * 设备预置位查询
     */
    void ntsOnDevicePresetQueryCommand(String fromUserName, String fromUserNameAtDomain, String sn, String deviceId);
}

总结

如果需要对接Android平台下的UVC摄像头,对我们来说,其实没有多少工作量,主要是稳定高效的拿到原始的YUV或NV21|NV12数据,我们来做软硬编码,打包即可,数据源拿到后,不管是录像还是对接RTMP、RTSP服务或GB28181都非常方便,以上是大概的流程,感兴趣的开发者,可以单独跟我沟通探讨。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
1 条评论
热度
最新
可以,大佬,互粉一下
可以,大佬,互粉一下
回复回复点赞举报
推荐阅读
ExoPlayer漫谈之解码器复用
播放器性能的极致优化,就是要发现播放流程中一点一滴的耗时,然后分析这些耗时,并提出有效的方式解决这些耗时.了解过ExoPlayer播放器的同学们都知道ExoPlayer的解码是依赖Android系统提供的原生的解码模块,即MediaCodec来实行视频和音频解码的.
马上就说
2021/01/21
2.4K0
ExoPlayer 漫谈之解码器复用
播放器性能的极致优化,就是要发现播放流程中一点一滴的耗时,然后分析这些耗时,并提出有效的方式解决这些耗时.了解过ExoPlayer播放器的同学们都知道ExoPlayer的解码是依赖Android系统提供的原生的解码模块,即MediaCodec来实行视频和音频解码的.
码上就说
2021/01/12
2.8K0
视频播放优化浅析
随着移动终端的普及和网络的提速,以短视频为媒介的内容成了大家普遍接受和喜欢的内容消费形式。但是短视频是如何从一个视频地址到我们能看见的音视频内容呢?我们都知道播放器就是用来完成视频从地址解析到视频渲染这个流程的集合。那在我们Android平台上播放器的发展和演进过程中,有哪些实现方式?他们背后都有些什么优缺点呢?对于一个内容消费者来说,在浏览短视频的过程中,哪些性能指标是影响用户体验的呢?技术人员对于这些性能指标有哪些可做的优化?以及在快速的版本迭代中如何保证海量用户的播放体验呢?带着这些问题,本文尝试从
微信终端开发团队
2021/06/02
4.7K0
播放器秒开优化丨音视频工业实战
视频播放时的画面打开速度是播放体验中一个非常重要的指标,如果视频画面打开速度太慢,用户失去耐心可能就直接划走不看了。如果视频速度打开够快,甚至可以带来业务上的收益,字节跳动就曾给出过一份数据:对一部分型号的 Android 手机,播放首帧时长从平均 170ms 优化到 100ms,带来了 0.6% 左右的用户播放时长提升。
关键帧
2023/02/14
3.7K0
播放器秒开优化丨音视频工业实战
如何让短视频做到”秒播“?(下)
前面我们讲到,如果要让短视频做到”秒播“的话,可以从域名解析、socket buffer、Probe buffer入手,对短视频小视频源码进行优化,那么我们今天来把剩余的几个方面介绍完。
云豹通讯员
2020/03/18
2K0
如何让短视频做到”秒播“?(下)
震惊!耗时还能这么优化??
0.文末为懒人版本 1.背景介绍    在视频号项目中,允许用户上传一分钟内的编辑视频,或者选择30min内的长视频。目前来看,整个发表(视频转码+上传)的耗时还略显偏久,虽然当下转码过程都是在手机后台运行,不会阻塞用户交互,但是由于视频未发表成功,视频点赞和转发功能都被限制,对用户和业务而言,这都是很不好的体验,有值得优化的必要。 1.1分析:耗时来源 整个耗时 = 视频转码耗时 + 上传耗时 目前上传的时间取决于用户网络,这个不是本文讨论的重点,先暂时不予考虑。 那么为什么我们需要对视频进行转码呢
微信终端开发团队
2021/08/26
6K0
音视频知识图谱 2022.12
前些时间,我在知识星球上创建了一个音视频技术社群:关键帧的音视频开发圈,在这里群友们会一起做一些打卡任务。比如:周期性地整理音视频相关的面试题,汇集一份音视频面试题集锦,你可以看看这个合集:音视频面试题集锦。再比如:循序渐进地归纳总结音视频技术知识,绘制一幅音视频知识图谱,你可以看看这个合集:音视频知识图谱。
关键帧
2023/02/14
6720
音视频知识图谱 2022.12
移动端视频缓存保障与CDN调度优化
我是来自网易云信的张根宁,今天我将会站在用户的角度来跟大家探讨播放器的相关优化,也会详细阐述网易云信团队在播放器方面都做了哪些努力。
LiveVideoStack
2019/08/16
3K0
一文掌握直播技术:实时音视频采集、编码、传输与播放
从游戏、教育、电商到娱乐,直播技术的应用场景无处不在。随着移动端的网速越来越快,直播技术的普及和发展将更加迅速。
陆业聪
2024/08/19
1.3K0
一文掌握直播技术:实时音视频采集、编码、传输与播放
新知 | 腾讯云视立方播放器技术实现与应用
新知系列课程第二季来啦!我们将为大家带来全真互联时代下新的行业趋势、新的技术方向以及新的应用场景分享。本期我们邀请了腾讯云音视频技术导师——李正通,为大家分享腾讯云视立方播放器技术实现与应用。 本次分享的主要内容分为三块,一是腾讯云视立方播放器的相关技术背景,二是业务侧经典场景应用方案,三是短视频场景应用的技术实现方案。 腾讯云视立方播放器技术背景 腾讯云视立方播放器基于腾讯视频同款内核打造,完美融合了腾讯视频的能力,视频兼容性、适配能力以及播放稳定性均大幅提升,解决了系统引擎各种播放异常问题。 功能全面
腾讯云音视频
2022/12/16
2.6K0
新知 | 腾讯云视立方播放器技术实现与应用
视频直播技术干货(十二):从入门到放弃,快速学习Android端直播技术
从游戏、教育、电商到娱乐,直播技术的应用场景无处不在。随着移动端的网速越来越快,直播技术的普及和发展将更加迅速。
JackJiang
2024/10/17
3010
视频直播技术干货(十二):从入门到放弃,快速学习Android端直播技术
全民K歌推流直播Web实践
背景 ---- 2020年受到疫情的影响,大众减少了线下娱乐,将更多的时间投入到了线上活动,直播行业迎来了一个小爆发,主播注册数量与线上观众不断增长。同时,在线直播演唱作为一种全新的演出模式,受到广大网友的好评,4月以来TME承办了近20场明星在线演唱会。 随着站外直播场景业务需求逐步增多,K歌直播旧的业务代码无法满足不断增长的产品功能需求和用户体验需求。在此背景下,Web侧急需为推流直播业务提供更加可靠的技术支持。 HLS和HTTP FLV ---- 目前K歌Web使用的直播流格式主要以HLS直播流为主
QQ音乐技术团队
2020/07/07
6.1K0
质量三维论如何持续推进腾讯视频播放体验提升
大家好,我是来自腾讯视频的李大龙。今天我将从以下几个方面为大家分享腾讯视频如何以质量三维论来持续推进视频播放体验的提升。
LiveVideoStack
2019/07/01
1.1K0
质量三维论如何持续推进腾讯视频播放体验提升
FFmpeg-音视频产品不容忽视的隐形王者
目前,业界的视频播放主要有三种架构:MPC, MPlayer和VLC,占据市场90%的份额。而三个架构均使用或者融合FFmpeg的视频解码技术。FFmpeg犹如至尊魔戒,驱使视频播放领域中的万物。
用户5521279
2019/12/12
1.7K0
FFmpeg-音视频产品不容忽视的隐形王者
音视频全链路技能分析之音视频消费侧技能树
各行各业都有鄙视链。娱乐圈的,拍电影的看不上拍电视的。IT圈的,C/C++工程师看不上Java、python、php这些搞高级API的小伙子。程序员之间,“文人相轻”的事情常有,但是平心而论,技术圈的事情确实有难易之分,工作有等级,那么技能就当然有高低。技术分高低,本地就是给我们一把尺,丈量一下自己水平,掂掂自己在领域中所处的位置。
马上就说
2020/12/11
1.7K0
音视频全链路技能分析之音视频消费侧技能树
RTMP直播播放器为什么要做秒开?
2015年我们做了RTMP直播播放器没多久,就加了快速播放接口,今天就从播放器用户体验的维度,谈谈为什么要做这个:
音视频牛哥
2024/11/25
2750
从体验出发构建以增长为目标的视频服务体系
增长一直是业务的诉求,和增长相关的因素很多,内容、人群、创意玩法、性能体验等等,本次LiveVideoStackCon 2021 音视频技术大会 北京站 我们邀请到了火山引擎点播技术研发负责人——浩铭老师。本次分享聚焦在字节跳动视频通过性能体验优化促进业务增长的实践。包括在分析方法上的探讨,如何衡量和预估体验优化对业务增长的贡献,以及具体的体验优化实践分享。
LiveVideoStack
2021/12/27
7230
从体验出发构建以增长为目标的视频服务体系
HLS 和 DASH 多编解码器和封装
FuboTV 是一家美国流媒体电视服务公司,为美国、加拿大和西班牙的客户提供服务,主要专注于分发体育直播的频道。根据国家/地区的不同,Fubo 提供的频道可能包括访问 NFL、MLB、NBA、NHL、MLS、CPL 和国际足球,以及新闻、网络电视连续剧和电影。
用户1324186
2021/12/22
2.3K0
HLS 和 DASH 多编解码器和封装
腾讯云音视频播放器又上新啦!短视频秒开组件、加密画中画等功能让音视频播放更专业!
根据腾讯云音视频官方的消息显示,播放器SDK是音视频终端SDK的子产品之一,它采用“腾讯视频”同款播放内核,经过内部业务长期优化和海量服务验证,对比系统播放器性能可提升20%-50%,同时具备“臻彩视听”、精准Seek、画中画等丰富功能,为用户提供直播、点播场景下流畅稳定的音视频播放能力,覆盖泛娱乐、电商、教育等多样化音视频业务场景,支持Web/H5、iOS、Android、Flutter平台。
三掌柜
2023/12/29
1.1K4
腾讯云音视频播放器又上新啦!短视频秒开组件、加密画中画等功能让音视频播放更专业!
腾讯云实时音视频技术发展简史 — 从编解码器容错优化到云端决策系统
大家好,我是腾讯视频云终端研发负责人常青,本次分享的主题和内容是关于腾讯音视频终端这些年来的进化演化以及在客户方面的实践应用,所以“进化”也是本次分享的主题词,说到进化大家可能首先联想到的是达尔文的进化论,因此我会先以一段故事来引出之后的内容。
LiveVideoStack
2019/09/04
1.6K0
腾讯云实时音视频技术发展简史 — 从编解码器容错优化到云端决策系统
推荐阅读
相关推荐
ExoPlayer漫谈之解码器复用
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档