【GStreamer】基於NVIDIA TEGRA系列板卡的硬體解碼及視訊推流
以NVIDIA TX1為例硬解碼就是利用硬體晶片來解碼的,TX1有單獨的解碼模組,NVDEC.
軟解碼是用軟體程式來解碼,比較佔用CPU資源
檢視cpu gpu 以及編解碼模組的使用:
sudo ./tegrastats
1 Gstreamer概述
Gstreamer是一個libraries和plugins的集合,用於幫助實現各種型別的多媒體應用程式,比如播放器,轉碼工具,多媒體伺服器等。利用Gstreamer編寫多媒體應用程式,就是利用elements構建一個pipeline。element是一個對多媒體流進行處理的object,比如如下的處理:
*讀取檔案。
*不同格式的編解碼。
*從硬體採集裝置上採集資料。
*
*多個流的複用。
elements的輸入叫做sink pads,輸出叫做source pads。應用程式通過pad把element連線起來構成pipeline,如下圖所示,其中順著流的方向為downstream,相反方向是upstream。
應用程式會收到來自pipeline的訊息和通知,比如EOS等。
總體設計
Gstreamer的設計目標如下:
快速處理大規模資料。
對多執行緒處理的完全支援。
能處理各種格式的流媒體。
不同資料流的同步。
處理多種裝置的能力。
基於Gstreamer的應用程式能夠具備的處理能力依賴於系統中安裝的不同種類功能的elements的數量。
Gstreamer核心不具備處理具體的media的功能,但是element處理media時需要具備的特性很多是由Gstreamer的核心提供的。
elements
element是pipeline的最小組成部分。element提供了多個pads,或者為sink,或者為source。一個element有四種可能的狀態,分別是NULL,READY,PAUSED,PLAYING。NULL和READY狀態下,element不對資料做任何處理,PLAYING狀態對資料進行處理,PAUSE狀態介於兩者之間,對資料進行preroll。應用程式通過函式呼叫控制pipeline在不同狀態之間進行轉換。
element的狀態變換不能跳過中間狀態,比如不能從READY狀態直接變換到PLAYING狀態,必須經過中間的PAUSE狀態。
element的狀態轉換成PAUSE會啟用element的pad。首先是source pad被啟用,然後是sink pad。pad被啟用後會呼叫activate函式,有一些pad會啟動一個Task。
PAUSE狀態下,pipeline會進行資料的preroll,目的是為後續的PLAYING狀態準備好資料,使得PLAYING啟動的速度更快。一些element需接收到足夠的資料才能完成向PAUSE狀態的轉變,sink pad只有在接收到第一個資料才能實現向PAUSE的狀態轉變。
通常情況下,element的狀態轉變需要協調一致。
可對element進行如下分類:
source,只提供資料來源。
sink,比如播放裝置。
transform
demuxer
muxer
Bin
bin是由多個element構成的特殊的element,用圖來說明:
Pipeline
pipeline是具備如下特性的特殊的bin:
選擇並管理一個全域性的時鐘。
基於選定的時鐘管理running_time。running_time用於同步,指的是pipeline在 PLAYING狀態下花費的時間。
管理pipeline的延遲。
通過GstBus提供element與應用程式間的通訊方式。
管理elements的全域性狀態,比如EOS,Error等。
Dataflow and buffers
Gstreamer支援兩種型別的資料流,分別是push模式和pull模式。在push模式下,upstream的element通過呼叫downstream的sink pads的函式實現資料的傳送。在pull模式下,downstream的element通過呼叫upstream的source pads的函式實現對資料的請求。
push模式是常用的模式,pull模式一般用於demuxer或者低延遲的音訊應用等。
在pads之間傳送的資料封裝在Buffer裡,Buffer中有一個指向實際資料的指標以及一些metadata。metadata的內容包括:
Timestamp
Offset
Duration
media type
其它
在push模式下,element通過呼叫gst_pad_push()函式把buffer傳送給對應的pad。在pull模式下,element通過呼叫gst_pad_pull_range()函式把pull過來。
element在push buffer之前需要確認對應的element具備處理buffer中的資料型別的能力。在傳說紅之前首先查詢對應的element能夠處理的格式的種類,並從中選擇合適的格式,通過gst_buffer_set_caps()函式對buffer進行設定,然後才傳送資料。
收到一個buffer後,element要首先對buffer進行檢查以確認是否能夠處理。
可以呼叫gst_buffer_new()函式建立一個新的buffer,也可以呼叫gst_pad_alloc_buffer()函式申請一個可用的buffer。採用第二種方法接收資料的buffer可以設定接收其它型別的資料,這是通過對buffer的caps進行設定來實現的。
選擇媒體型別並對buffer進行設定的處理過程叫做caps negotianation。
Caps
Caps,也就是媒體型別,採用key/value對的列表來描述。key是一個字串型別,value的型別可能是int/float/string型別的single/list/range。
Data flow and events
除了資料流,還有events流。與資料流不同,events的傳送方向既有downstream的,也有upstream的。
events用於傳遞EOS,flushing,seeking等訊息。
有的events必須和data flow一起進行serialized。serialized的events比如TAG,非serialized的events比如FLUSH。
Pipeline construction
gst_pipeline_create()函式用於建立一個pipeline,gst_bin_add()函式用於向pipeline中新增element,gst_bin_remove()函式用於從pipeline中移除element。gst_element_get_pad()函式用於檢索pipeline中的element。gst_pad_link()函式用於把pads連線在一起。
有的element會在資料流開始傳送的時候建立新的pads,通過呼叫函式g_signal_connect()函式,能在新的pads被建立的時候接收到訊息。
由於處理的資料互相不相容,有的elements是不能被連線到一起的。gst_pad_get_caps()函式查詢element能夠處理的資料型別。
Pipeline clock
Pipeline的一個重要功能是為pipeline中的所有elements選擇一個全域性時鐘。
時鐘的作用是提供一個每秒為GST_SECOND的單調遞增的時鐘,單位是納秒。element利用這個時鐘時間來播放資料。
在pipeline被設為PLAYING之前,pipeline查詢每一個element是否能提供clock,並按照如下次序來選擇clock:
應用程式選擇了一個clock。
如果source element提供了clock。
其它任何提供了clock的element。
選擇一個預設的系統clock。
也有特殊的情況,比如存在音訊sink提供了clock,那麼就選擇其提供的clock。
Pipeline states
完成了pads的連結和signals的連結,就可以設定pipeline為PAUSED狀態啟動資料流的處理。當bin(這裡指的是pipeline)進行狀態轉換的時候要轉換所有的children的狀態,轉換的次序是從sink element開始到source element結束,這樣做的目的是為了確保upstream element提供資料的時候,downstream element已經準備好。
Pipeline status
Pipeline會通過bus嚮應用程式通報發生的events。bus是由pipeline提供的一個object,可以通過gst_pipeline_get_bus()函式取得。
bus分佈到加入pipeline的每一個element。element利用bus來發布messages。有各種不同型別的messages,比如ERRORS,WARNINGS,EOS,STATE_CHANGED等。
pipeline以特殊的方式處理接收到的EOS message,只有當所有的sink element傳送了EOS message的時候,pipeline才會把EOS傳送給應用程式。
也可以通過gst_element_query()函式獲取pipeline status,比如獲取當前的位置或者播放的時間。
Pipeline EOS
當source filter遇上了流結束,會沿著downstream的方向向下一個element傳送一個EOS的event,這個event依次傳送給每一個element,接收到EOS event的element不再接收資料。
啟動了執行緒的element傳送了EOS event後就不再發送資料。
EOS event最終會到達sink element。sink element會發送一個EOS訊息,通告流結束。pipeline在接收到EOS訊息以後,把訊息傳送給應用程式。只有在PLAYING狀態下會把EOS的訊息傳送給應用程式。
傳送了EOS以後,pipeline保持PLAYING狀態,等待應用程式把pipeline的狀態置為PAUSE或者READY。應用程式也可以進行seek操作。
2 Gstreamer解碼
2.1 呼叫Gstreamer解碼多路rtsp(部分程式碼)
/*
*Author:mxj
*/
#ifndef __GSTREAMER_CAMERA_H__
#define __GSTREAMER_CAMERA_H__
#include <gst/gst.h>
#include <string>
struct _GstAppSink;//宣告結構體和類
class QWaitCondition;
class QMutex;
/*** gstreamer CSI camera using nvcamerasrc (or optionally v4l2src)
* @ingroup util
*/
class gstCamera
{
public:
// 建立camera類
static gstCamera* Create( int v4l2_device=-1 ); // use onboard camera by default (>=0 for V4L2)
static gstCamera* Create( uint32_t width, uint32_t height, int v4l2_device=-1 );
// 解構函式
~gstCamera();
// 開始和停止流
bool Open();
void Close();
// 採集YUV(NV12格式)
bool Capture( void** cpu, void** cuda, unsigned long timeout=ULONG_MAX );
// 抓取YUV-NV12 CUDA image, 轉換成 float4 RGBA (畫素範圍在 0-255)
// 轉換如果在CPU上進行,設定zeroCopy=true,預設只在CUDA上.
bool ConvertRGBA( void* input, void** output, bool zeroCopy=false );
// 影象大小資訊 inline(行內函數,適合簡單的函式)
inline uint32_t GetWidth() const { return mWidth; }
inline uint32_t GetHeight() const { return mHeight; }
inline uint32_t GetPixelDepth() const { return mDepth; }
inline uint32_t GetSize() const { return mSize; }
// 預設影象大小,可以在create時改變
static const uint32_t DefaultWidth = 1280;
static const uint32_t DefaultHeight = 720;
private:
static void onEOS(_GstAppSink* sink, void* user_data);
static GstFlowReturn onPreroll(_GstAppSink* sink, void* user_data);//GstFlowReturn 傳遞流
static GstFlowReturn onBuffer(_GstAppSink* sink, void* user_data);
gstCamera();
bool init();
bool buildLaunchStr();
void checkMsgBus();
void checkBuffer();
//GstBus
_GstBus* mBus;//GstBus 非同步同步訊息
_GstAppSink* mAppSink;
_GstElement* mPipeline;
std::string mLaunchStr="rtspsrc location=rtsp://admin:[email protected]:554/h264/ch1/main/av_stream latency=0 ! queue ! rtph264depay ! h264parse ! queue ! omxh264dec ! appsink name=mysink";
uint32_t mWidth;
uint32_t mHeight;
uint32_t mDepth;
uint32_t mSize;
static const uint32_t NUM_RINGBUFFERS = 16;//環形佇列來解決資料阻塞問題
void* mRingbufferCPU[NUM_RINGBUFFERS];
void* mRingbufferGPU[NUM_RINGBUFFERS];
QWaitCondition* mWaitEvent;
//mutex.lock() //鎖住互斥量(mutex)。如果互斥量是解鎖的,那麼當前執行緒就立即佔用並鎖定它。否則,當前執行緒就會被阻塞,知道掌握這個互斥量的執行緒對它解鎖為止。
//mutex.unlock()//解鎖
//mutex.tryLock()//嘗試解鎖,如果該互斥量已經鎖住,它就會立即返回
QMutex* mWaitMutex;
QMutex* mRingMutex;
uint32_t mLatestRGBA;
uint32_t mLatestRingbuffer;
bool mLatestRetrieved;
void* mRGBA[NUM_RINGBUFFERS];
int mV4L2Device; // -1 for onboard, >=0 for V4L2 device
inline bool onboardCamera() const { return (mV4L2Device < 0); }
};
#endif
bool gstCamera::Capture( void** cpu, void** cuda, unsigned long timeout )
{
/*wait() 函式必須傳入一個已上鎖的 mutex 物件,在 wait() 執行過程中,
mutex一直保持上鎖狀態,直到呼叫作業系統的wait_block 在阻塞的一瞬間把 mutex 解鎖
(嚴格說來應該是原子操作,即系統能保證在真正執行阻塞等待指令時才解鎖)。
另一執行緒喚醒後,wait() 函式將在第一時間重新給 mutex 上鎖(這種操作也是原子的)
,直到顯示呼叫 mutex.unlock() 解鎖。*/
mWaitMutex->lock();
const bool wait_result = mWaitEvent->wait(mWaitMutex, timeout);
mWaitMutex->unlock();
if( !wait_result )
return false;
mRingMutex->lock();
const uint32_t latest = mLatestRingbuffer;
const bool retrieved = mLatestRetrieved;
mLatestRetrieved = true;
mRingMutex->unlock();
// skip if it was already retrieved
if( retrieved )
return false;
if( cpu != NULL )
*cpu = mRingbufferCPU[latest];
if( cuda != NULL )
*cuda = mRingbufferGPU[latest];
return true;
}
#define release_return { gst_sample_unref(gstSample); return; }
// checkBuffer
void gstCamera::checkBuffer()
{
bool write_flags=true;//預設寫資料
if( !mAppSink )
return;
// block waiting for the buffer 函式被喚醒until A sample or EOS 可用 或者appsink 被設定成 ready/null state
GstSample* gstSample = gst_app_sink_pull_sample(mAppSink);
if( !gstSample )
{
printf(LOG_GSTREAMER "gstreamer camera -- gst_app_sink_pull_sample() returned NULL...\n");
return;
}
//get buffer from gstSample
GstBuffer* gstBuffer = gst_sample_get_buffer(gstSample);
if( !gstBuffer )
{
printf(LOG_GSTREAMER "gstreamer camera -- gst_sample_get_buffer() returned NULL...\n");
return;
}
// retrieve
GstMapInfo map;
if( !gst_buffer_map(gstBuffer, &map, GST_MAP_READ) )
{
printf(LOG_GSTREAMER "gstreamer camera -- gst_buffer_map() failed...\n");
return;
}
//gst_util_dump_mem(map.data, map.size);
void* gstData = map.data; //GST_BUFFER_DATA(gstBuffer);
const uint32_t gstSize = map.size; //GST_BUFFER_SIZE(gstBuffer);
if( !gstData )
{
printf(LOG_GSTREAMER "gstreamer camera -- gst_buffer had NULL data pointer...\n");
release_return;
}
// 取出caps
GstCaps* gstCaps = gst_sample_get_caps(gstSample);
if( !gstCaps )
{
printf(LOG_GSTREAMER "gstreamer camera -- gst_buffer had NULL caps...\n");
release_return;
}
GstStructure* gstCapsStruct = gst_caps_get_structure(gstCaps, 0);
if( !gstCapsStruct )
{
printf(LOG_GSTREAMER "gstreamer camera -- gst_caps had NULL structure...\n");
release_return;
}
// get width & height of the buffer
int width = 0;
int height = 0;
if( !gst_structure_get_int(gstCapsStruct, "width", &width) ||
!gst_structure_get_int(gstCapsStruct, "height", &height) )
{
printf(LOG_GSTREAMER "gstreamer camera -- gst_caps missing width/height...\n");
release_return;
}
if( width < 1 || height < 1 )
release_return;
mWidth = width;
mHeight = height;
mDepth = (gstSize * 8) / (width * height);
mSize = gstSize;
//printf(LOG_GSTREAMER "gstreamer camera recieved %ix%i frame (%u bytes, %u bpp)\n", width, height, gstSize, mDepth);
// make sure ringbuffer is allocated
if( !mRingbufferCPU[0] )
{
for( uint32_t n=0; n < NUM_RINGBUFFERS; n++ )
{
if( !cudaAllocMapped(&mRingbufferCPU[n], &mRingbufferGPU[n], gstSize) )
printf(LOG_CUDA "gstreamer camera -- failed to allocate ringbuffer %u (size=%u)\n", n, gstSize);
}
printf(LOG_CUDA "gstreamer camera -- allocated %u ringbuffers, %u bytes each\n", NUM_RINGBUFFERS, gstSize);
}
// copy to next ringbuffer
const uint32_t nextRingbuffer = (mLatestRingbuffer + 1) % NUM_RINGBUFFERS;
//printf(LOG_GSTREAMER "gstreamer camera -- using ringbuffer #%u for next frame\n", nextRingbuffer);
memcpy(mRingbufferCPU[nextRingbuffer], gstData, gstSize);
// FILE *fp=fopen("out.yuv","w+");
// fwrite(gstData,gstSize,1,fp);
// fclose(fp);
//test h264 write
//void *writedata=map.data;
// while(write_flags==true)
// {
// FILE *fp=fopen("out.264","a+");
// fwrite(writedata,gstSize,1,fp);
// write_flags=false;
// fclose(fp);
// }
gst_buffer_unmap(gstBuffer, &map);
//gst_buffer_unref(gstBuffer);
gst_sample_unref(gstSample);
// update and signal sleeping threads
mRingMutex->lock();
mLatestRingbuffer = nextRingbuffer;
mLatestRetrieved = false;
mRingMutex->unlock();
mWaitEvent->wakeAll();
}
可以在帶顯示的時候解碼4路1080rtsp流.
不加顯示可以做到6路1080p解碼
2.2 opencv中使用Gstreamer解碼海康rtsp攝像頭
A.安裝gstreamer依賴
B.重新編譯安裝opencv
cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=/usr/local -D CUDA_GENERATION=Kepler ..
C.確認opencv cmake後的提示中Gstreamer的五個選項都為on
D.編譯opencv之後的使用方法:
VideoCapture(“rtspsrc location=\”rtsp://admin:[email protected]/h264/h264/main/av_stream\” latency=10 ! rtph264depay ! h264parse ! omxh264dec ! videoconvert ! appsink sync=false”)
2.3 呼叫tegra_multimedia解碼
這是nvidia自帶的編解碼框架 目前用官方的解碼1080p的h264可以做到解碼速度達到150fps接入rtsp的相機流
解決方法:ffmpeg/live555解析之後做個數據拷貝
我在看nvidia官方的demo,瞭解清楚資料結構之後就可以對接解析好的h264視訊流(後續更新)
2.4 視訊推流
需求:將處理後的opencv資料進行推流到網頁解決方案:
A> 用tegra_multimedia編碼資料為h264, live555推流為rtsp
B> 直接用gstreamer推流(gstreamer自帶rtsp的部分)
相關推薦
【GStreamer】基於NVIDIA TEGRA系列板卡的硬體解碼及視訊推流
以NVIDIA TX1為例硬解碼就是利用硬體晶片來解碼的,TX1有單獨的解碼模組,NVDEC. 軟解碼是用軟體程式來解碼,比較佔用CPU資源 檢視cpu gpu 以及編解碼模組的使用: sudo ./tegrastats1 Gstreamer概述 Gstreamer是一個li
【GStreamer學習】基於NVIDIA TX系列板卡的硬體解碼及視訊推流
以NVIDIA TX1為例硬解碼就是利用硬體晶片來解碼的,TX1有單獨的解碼模組,NVDEC. 軟解碼是用軟體程式來解碼,比較佔用CPU資源 檢視cpu gpu 以及編解碼模組的使用: sudo ./tegrastats 1 Gstreamer概述 Gstreamer是一個li
【原創】基於NodeJS Express框架開發的一個VIP視訊網站專案及原始碼分享
專案名稱:視訊網站專案 開發語言:HTML,CSS(前端),JavaScript,NODEJS(expres)(後臺) 資料庫:MySQL 開發環境:Win7,Webstorm 上線部署環境:Linux伺服器 主要功能我們先來看一下最終的效果:使用者主頁的搭建: 實現了主頁輪播圖的顯示和切換,使用者可以從
【深入淺出】| 基於深度學習的機器翻譯(附PDF+視訊下載)
由公眾號"機器學習演算法與Python學習"整理源|將門創投本文所分享的是清華大學劉洋副教授講解
ASP.NET CORE系列【四】基於Claim登錄授權
amp account 技術 time 其他 cookie first arp 好的 介紹 關於什麽是Claim? 可以看看其他大神的文章: http://www.cnblogs.com/jesse2013/p/aspnet-identity-claims-base
【開源】OSharp框架學習系列(1):總體設計及系列導航
正是 html 組織 內聚性 權限 是什麽 enc 3-0 分發 OSharp是什麽? OSharp是個快速開發框架,但不是一個大而全的包羅萬象的框架,嚴格的說,OSharp中什麽都沒有實現。與其他大而全的框架最大的不同點,就是OSharp只做抽象封裝,不做實現。依賴註
【Rpc】基於開源Dubbo分布式RPC服務框架的部署整合
c-s 基於 1.8 git 編譯 handle direct 有著 ride 一、前言 Dubbo 作為SOA服務化治理方案的核心框架,用於提高業務邏輯的復用、整合、集中管理,具有極高的可靠性(HA)和伸縮性,被應用於阿裏巴巴各成員站點,同時在包括JD、當當在內的眾多互
【自動化】基於Spark streaming的SQL服務實時自動化運維
body oop nbsp define mysq tco source font getc 設計背景 spark thriftserver目前線上有10個實例,以往通過監控端口存活的方式很不準確,當出故障時進程不退出情況很多,而手動去查看日誌再重啟處理服務這個過程很低效
【轉】基於Map的簡易記憶化緩存
還在 自己 == map cti extends inter end 參考資料 看到文章後,自己也想寫一些關於這個方面的,但是覺得寫的估計沒有那位博主好,而且又會用到裏面的許多東西,所以幹脆轉載。但是會在文章末尾寫上自己的學習的的東西。 原文出處如下: http://www
【redis】基於redis實現分布式並發鎖
val 內容 等待隊列 過多 具體實現 exec ret abs con 基於redis實現分布式並發鎖(註解實現) 說明 前提, 應用服務是分布式或多服務, 而這些"多"有共同的"redis"; GitHub: https:
【轉】基於localStorage的資源離線和更新技術
同時 前端 event 原來 read 前端資源 獲取 tex tor ServiceWorker的資源離線與更新 ServiceWorker是替代Application Cache的機制,目前為止其兼容性很差。 localStorage資源離線緩存與更新 基本思路:將
【docker】基於Dockerfile構建mysqld服務鏡像
mysqld服務鏡像一 創建構建目錄結構 # mkdir -pv docker/mysql# cd docker/mysql/二 寫Dockerfile 文件# vim Dockerfile #此處 sshd:latest 為上篇文章中創建的鏡像#此Dockerfile 「dookerpool」的
【docker】基於Dockerfile構建monogdb服務鏡像
monogdb服務鏡像① 查看內容,包括寫好的Dockerfile和若幹腳本等。從GitHub Dockerpool社區賬戶下載Mongodb鏡像項目:git clone https://github.com/DockerPool/Mongodb.git 並修改文件[root@docker1 Mongodb]
【DevOps】團隊敏捷開發系列--開山篇
jmeter junit ger 優秀 資料 開發 load 分析 針對 隨著軟件發布叠代的頻率越來越高,傳統的「瀑布型」(開發—測試—發布)模式已經不能滿足快速交付的需求。2009 年左右 DevOps 應運而生,開發運維一體化,通過自動化工具與流程讓整個軟件開發構建、
【轉載】Android Bug分析系列:第三方平臺安裝app啟動後,home鍵回到桌面後點擊app啟動時會再次啟動入口類bug的原因剖析
特殊 返回 androidm android系統 圖片 管理 相關 OS 簡便 前言 前些天,測試MM發現了一個比較奇怪的bug。 具體表現是: 1、將app包通過電腦QQ傳送到手機QQ上面,點擊安裝,安裝後選擇打開app (此間的應用邏輯應該是要觸發 【閃屏頁
【轉載】基於rasa的對話系統搭建(上)
生成模型 efi 實體類 total ted twisted -m serve feature 文章介紹使用rasa nlu和 rasa core 實現一個電信領域對話系統demo,實現簡單的業務查詢辦理功能,更完善的實現需要
【Scala】基於8.0版本的jdbc進行資料庫連接出現
下面這個程式碼可以正常執行,沒有問題 import java.sql.{Connection, DriverManager, ResultSet, Statement} object Main { def main(args: Array[String]): Unit = {
【Kubernetes】基於角色的許可權控制:RBAC
Kubernetes中所有的API物件,都儲存在Etcd裡,對這些API物件的操作,一定都是通過訪問kube-apiserver實現的,原因是需要APIServer來做授權工作。 在Kubernetes中,負責完成授權(Authorization)工作的機制,就是RBAC:基於角色的訪問控制(Rol
【原始碼】基於IEEE 14匯流排標準的複合微電網SIMULINK模型
本程式設計了一種基於IEEE 14匯流排標準的複合微電網模型,該微電網模型包括柴油發電機、PV模型、電池儲能系統、電弧爐等非線性負載。微電網採用併網執行方式。 本模型的參考文獻: A new approach for soft synchronization of microgri
【8】C++進階系列(過載)
1、過載規則 c++幾乎可以過載全部的運算子,而且只能夠過載c++已有的運算子。 其中,不能過載的運算子:"." 、 ".*" 、"::"、"?:" 過載之後運算子的優先順序和結合性都不會改變。 運算子過載是針對新型資料的實際需要,對原有運算子進行適當的改造。例如: 使複數的物件