summary refs log tree commit diff
diff options
context:
space:
mode:
authorKeiichi Watanabe <keiichiw@chromium.org>2019-12-06 22:24:40 +0900
committerCommit Bot <commit-bot@chromium.org>2020-05-23 09:54:13 +0000
commit57df6a0ab23c3b2ba233b9aa5886ecf47ba3f91f (patch)
tree11bc695e179762b6eac26302e6b89db55b251dba
parentb2ca24c97b0084b805f7da28804b4c430c151ccb (diff)
downloadcrosvm-57df6a0ab23c3b2ba233b9aa5886ecf47ba3f91f.tar
crosvm-57df6a0ab23c3b2ba233b9aa5886ecf47ba3f91f.tar.gz
crosvm-57df6a0ab23c3b2ba233b9aa5886ecf47ba3f91f.tar.bz2
crosvm-57df6a0ab23c3b2ba233b9aa5886ecf47ba3f91f.tar.lz
crosvm-57df6a0ab23c3b2ba233b9aa5886ecf47ba3f91f.tar.xz
crosvm-57df6a0ab23c3b2ba233b9aa5886ecf47ba3f91f.tar.zst
crosvm-57df6a0ab23c3b2ba233b9aa5886ecf47ba3f91f.zip
devices: virtio: Initial implementation of virtio-video device
This CL adds a fundamental part of the virtio video device, which will
be shared between the encoder and the decoder.
Both devices uses the virtio-video protocol proposed as RFC v3 [1,2].
The corresponding driver code is at CL:2060327 and its children CLs.

The actual decoding and encoding logic will be implemented in different
CLs.

[1]: mail: https://markmail.org/thread/wxdne5re7aaugbjg
[2]: PDF: https://drive.google.com/file/d/1jOsS2WdVhL4PpcWLO8Zukq5J0fXDiWn-/view

BUG=b:147465619, b:140082257
TEST=cargo check --features=video-decoder,video-encoder
TEST=ARCVM started with --video-decoder --video-encoder

Cq-Depend: chromium:2203997
Change-Id: I01999eea218ba0f3aaed1558ca2311a57d0c6819
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/crosvm/+/1973973
Reviewed-by: Keiichi Watanabe <keiichiw@chromium.org>
Tested-by: Keiichi Watanabe <keiichiw@chromium.org>
Tested-by: kokoro <noreply+kokoro@google.com>
Commit-Queue: Keiichi Watanabe <keiichiw@chromium.org>
-rw-r--r--Cargo.lock10
-rw-r--r--Cargo.toml3
-rw-r--r--devices/Cargo.toml3
-rw-r--r--devices/src/virtio/mod.rs8
-rw-r--r--devices/src/virtio/video/command.rs335
-rw-r--r--devices/src/virtio/video/control.rs83
-rw-r--r--devices/src/virtio/video/decoder/mod.rs40
-rw-r--r--devices/src/virtio/video/device.rs93
-rw-r--r--devices/src/virtio/video/encoder/mod.rs40
-rw-r--r--devices/src/virtio/video/error.rs84
-rw-r--r--devices/src/virtio/video/event.rs36
-rw-r--r--devices/src/virtio/video/format.rs116
-rw-r--r--devices/src/virtio/video/macros.rs46
-rw-r--r--devices/src/virtio/video/mod.rs250
-rw-r--r--devices/src/virtio/video/params.rs111
-rw-r--r--devices/src/virtio/video/protocol.rs487
-rw-r--r--devices/src/virtio/video/response.rs95
-rw-r--r--devices/src/virtio/video/worker.rs362
-rw-r--r--seccomp/x86_64/video_device.policy24
-rw-r--r--src/crosvm.rs4
-rw-r--r--src/linux.rs88
-rw-r--r--src/main.rs10
22 files changed, 2328 insertions, 0 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 4df284d..a696e32 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -183,6 +183,7 @@ dependencies = [
  "kvm_sys 0.1.0",
  "libc 0.2.44 (registry+https://github.com/rust-lang/crates.io-index)",
  "libcras 0.1.0",
+ "libvda 0.1.0",
  "linux_input_sys 0.1.0",
  "msg_on_socket_derive 0.1.0",
  "msg_socket 0.1.0",
@@ -412,6 +413,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "libvda"
+version = "0.1.0"
+dependencies = [
+ "enumn 0.1.0",
+ "libc 0.2.44 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pkg-config 0.3.11 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
 name = "linux_input_sys"
 version = "0.1.0"
 dependencies = [
diff --git a/Cargo.toml b/Cargo.toml
index 3fdad6b..55f7df0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -35,6 +35,8 @@ default-no-sandbox = []
 gpu = ["devices/gpu"]
 plugin = ["protos/plugin", "crosvm_plugin", "protobuf"]
 tpm = ["devices/tpm"]
+video-decoder = ["devices/video-decoder"]
+video-encoder = ["devices/video-encoder"]
 wl-dmabuf = ["devices/wl-dmabuf", "gpu_buffer", "resources/wl-dmabuf"]
 x = ["devices/x"]
 virtio-gpu-next = ["gpu_renderer/virtio-gpu-next"]
@@ -89,6 +91,7 @@ assertions = { path = "assertions" }
 audio_streams = { path = "../../third_party/adhd/audio_streams" } # ignored by ebuild
 data_model = { path = "data_model" }
 libcras = { path = "../../third_party/adhd/cras/client/libcras" } # ignored by ebuild
+libvda = { path = "../../platform2/arc/vm/libvda/rust" } # ignored by ebuild
 minijail-sys = { path = "../../aosp/external/minijail" } # ignored by ebuild
 poll_token_derive = { path = "sys_util/poll_token_derive" }
 sync = { path = "sync" }
diff --git a/devices/Cargo.toml b/devices/Cargo.toml
index 931ea36..8bea78d 100644
--- a/devices/Cargo.toml
+++ b/devices/Cargo.toml
@@ -7,6 +7,8 @@ edition = "2018"
 [features]
 gpu = ["gpu_buffer", "gpu_display", "gpu_renderer"]
 tpm = ["protos/trunks", "tpm2"]
+video-decoder = ["libvda"]
+video-encoder = ["libvda"]
 wl-dmabuf = []
 x = ["gpu_display/x"]
 gfxstream = ["gpu"]
@@ -28,6 +30,7 @@ kvm = { path = "../kvm" }
 kvm_sys = { path = "../kvm_sys" }
 libc = "*"
 libcras = "*"
+libvda = { version = "*", optional = true }
 linux_input_sys = { path = "../linux_input_sys" }
 msg_on_socket_derive = { path = "../msg_socket/msg_on_socket_derive" }
 msg_socket = { path = "../msg_socket" }
diff --git a/devices/src/virtio/mod.rs b/devices/src/virtio/mod.rs
index 4d5d2cb..ce62551 100644
--- a/devices/src/virtio/mod.rs
+++ b/devices/src/virtio/mod.rs
@@ -17,6 +17,8 @@ mod queue;
 mod rng;
 #[cfg(feature = "tpm")]
 mod tpm;
+#[cfg(any(feature = "video-decoder", feature = "video-encoder"))]
+mod video;
 mod virtio_device;
 mod virtio_pci_common_config;
 mod virtio_pci_device;
@@ -44,6 +46,8 @@ pub use self::queue::*;
 pub use self::rng::*;
 #[cfg(feature = "tpm")]
 pub use self::tpm::*;
+#[cfg(any(feature = "video-decoder", feature = "video-encoder"))]
+pub use self::video::*;
 pub use self::virtio_device::*;
 pub use self::virtio_pci_device::*;
 pub use self::wl::*;
@@ -76,6 +80,8 @@ const TYPE_CRYPTO: u32 = 20;
 const TYPE_IOMMU: u32 = 23;
 const TYPE_FS: u32 = 26;
 const TYPE_PMEM: u32 = 27;
+const TYPE_VIDEO_ENC: u32 = 30;
+const TYPE_VIDEO_DEC: u32 = 31;
 // Additional types invented by crosvm
 const MAX_VIRTIO_DEVICE_ID: u32 = 63;
 const TYPE_WL: u32 = MAX_VIRTIO_DEVICE_ID;
@@ -114,6 +120,8 @@ pub fn type_to_str(type_: u32) -> Option<&'static str> {
         TYPE_PMEM => "pmem",
         TYPE_WL => "wl",
         TYPE_TPM => "tpm",
+        TYPE_VIDEO_DEC => "video-decoder",
+        TYPE_VIDEO_ENC => "video-encoder",
         _ => return None,
     })
 }
diff --git a/devices/src/virtio/video/command.rs b/devices/src/virtio/video/command.rs
new file mode 100644
index 0000000..7bdb335
--- /dev/null
+++ b/devices/src/virtio/video/command.rs
@@ -0,0 +1,335 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Data structures for commands of virtio video devices.
+
+use std::convert::{TryFrom, TryInto};
+use std::fmt;
+use std::io;
+
+use data_model::Le32;
+use enumn::N;
+use sys_util::error;
+
+use crate::virtio::video::control::*;
+use crate::virtio::video::format::*;
+use crate::virtio::video::params::Params;
+use crate::virtio::video::protocol::*;
+use crate::virtio::Reader;
+
+/// An error indicating a failure while reading a request from the guest.
+#[derive(Debug)]
+pub enum ReadCmdError {
+    /// Failure while reading an object.
+    IoError(io::Error),
+    /// Invalid arguement is passed,
+    InvalidArgument,
+    /// The type of the command was invalid.
+    InvalidCmdType(u32),
+    /// The type of the requested control was unsupported.
+    UnsupportedCtrlType(u32),
+}
+
+impl fmt::Display for ReadCmdError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use self::ReadCmdError::*;
+        match self {
+            IoError(e) => write!(f, "failed to read an object: {}", e),
+            InvalidArgument => write!(f, "invalid arguement is passed in command"),
+            InvalidCmdType(t) => write!(f, "invalid command type: {}", t),
+            UnsupportedCtrlType(t) => write!(f, "unsupported control type: {}", t),
+        }
+    }
+}
+
+impl std::error::Error for ReadCmdError {}
+
+impl From<io::Error> for ReadCmdError {
+    fn from(e: io::Error) -> ReadCmdError {
+        ReadCmdError::IoError(e)
+    }
+}
+
+#[derive(PartialEq, Eq, PartialOrd, Ord, N, Clone, Copy, Debug)]
+#[repr(u32)]
+pub enum QueueType {
+    Input = VIRTIO_VIDEO_QUEUE_TYPE_INPUT,
+    Output = VIRTIO_VIDEO_QUEUE_TYPE_OUTPUT,
+}
+impl_try_from_le32_for_enumn!(QueueType, "queue_type");
+
+pub enum VideoCmd {
+    QueryCapability {
+        queue_type: QueueType,
+    },
+    StreamCreate {
+        stream_id: u32,
+        coded_format: Format,
+    },
+    StreamDestroy {
+        stream_id: u32,
+    },
+    StreamDrain {
+        stream_id: u32,
+    },
+    ResourceCreate {
+        stream_id: u32,
+        queue_type: QueueType,
+        resource_id: u32,
+        plane_offsets: Vec<u32>,
+        uuid: u128,
+    },
+    ResourceQueue {
+        stream_id: u32,
+        queue_type: QueueType,
+        resource_id: u32,
+        timestamp: u64,
+        data_sizes: Vec<u32>,
+    },
+    ResourceDestroyAll {
+        stream_id: u32,
+    },
+    QueueClear {
+        stream_id: u32,
+        queue_type: QueueType,
+    },
+    GetParams {
+        stream_id: u32,
+        queue_type: QueueType,
+    },
+    SetParams {
+        stream_id: u32,
+        queue_type: QueueType,
+        params: Params,
+    },
+    QueryControl {
+        query_ctrl_type: QueryCtrlType,
+    },
+    GetControl {
+        stream_id: u32,
+        ctrl_type: CtrlType,
+    },
+    SetControl {
+        stream_id: u32,
+        ctrl_val: CtrlVal,
+    },
+}
+
+impl<'a> VideoCmd {
+    /// Reads a request on virtqueue and construct a VideoCmd value.
+    pub fn from_reader(r: &'a mut Reader<'a>) -> Result<Self, ReadCmdError> {
+        use self::ReadCmdError::*;
+        use self::VideoCmd::*;
+
+        // Unlike structs in virtio_video.h in the kernel, our command structs in protocol.rs don't
+        // have a field of `struct virtio_video_cmd_hdr`. So, we first read the header here and
+        // a body below.
+        let hdr = r.read_obj::<virtio_video_cmd_hdr>()?;
+
+        Ok(match hdr.type_.into() {
+            VIRTIO_VIDEO_CMD_QUERY_CAPABILITY => {
+                let virtio_video_query_capability { queue_type, .. } = r.read_obj()?;
+                QueryCapability {
+                    queue_type: queue_type.try_into()?,
+                }
+            }
+            VIRTIO_VIDEO_CMD_STREAM_CREATE => {
+                let virtio_video_stream_create {
+                    in_mem_type,
+                    out_mem_type,
+                    coded_format,
+                    ..
+                } = r.read_obj()?;
+
+                if in_mem_type != VIRTIO_VIDEO_MEM_TYPE_VIRTIO_OBJECT
+                    || out_mem_type != VIRTIO_VIDEO_MEM_TYPE_VIRTIO_OBJECT
+                {
+                    error!("mem_type must be VIRTIO_OBJECT");
+                    return Err(InvalidArgument);
+                }
+                StreamCreate {
+                    stream_id: hdr.stream_id.into(),
+                    coded_format: coded_format.try_into()?,
+                }
+            }
+            VIRTIO_VIDEO_CMD_STREAM_DESTROY => {
+                let virtio_video_stream_destroy { .. } = r.read_obj()?;
+                StreamDestroy {
+                    stream_id: hdr.stream_id.into(),
+                }
+            }
+            VIRTIO_VIDEO_CMD_STREAM_DRAIN => {
+                let virtio_video_stream_drain { .. } = r.read_obj()?;
+                StreamDrain {
+                    stream_id: hdr.stream_id.into(),
+                }
+            }
+            VIRTIO_VIDEO_CMD_RESOURCE_CREATE => {
+                let virtio_video_resource_create {
+                    queue_type,
+                    resource_id,
+                    planes_layout,
+                    num_planes,
+                    plane_offsets,
+                    ..
+                } = r.read_obj()?;
+
+                // Assume ChromeOS-specific requirements.
+                if Into::<u32>::into(planes_layout) != VIRTIO_VIDEO_PLANES_LAYOUT_SINGLE_BUFFER {
+                    error!(
+                        "each buffer must be a single DMAbuf: {}",
+                        Into::<u32>::into(planes_layout),
+                    );
+                    return Err(InvalidArgument);
+                }
+
+                let num_planes: u32 = num_planes.into();
+                if num_planes as usize > plane_offsets.len() {
+                    error!(
+                        "num_planes must not exceed {} but {}",
+                        plane_offsets.len(),
+                        num_planes
+                    );
+                    return Err(InvalidArgument);
+                }
+                let plane_offsets = plane_offsets[0..num_planes as usize]
+                    .iter()
+                    .map(|x| Into::<u32>::into(*x))
+                    .collect::<Vec<u32>>();
+
+                let virtio_video_object_entry { uuid } = r.read_obj()?;
+
+                ResourceCreate {
+                    stream_id: hdr.stream_id.into(),
+                    queue_type: queue_type.try_into()?,
+                    resource_id: resource_id.into(),
+                    plane_offsets,
+                    uuid: u128::from_be_bytes(uuid),
+                }
+            }
+            VIRTIO_VIDEO_CMD_RESOURCE_QUEUE => {
+                let virtio_video_resource_queue {
+                    queue_type,
+                    resource_id,
+                    timestamp,
+                    num_data_sizes,
+                    data_sizes,
+                    ..
+                } = r.read_obj()?;
+
+                let num_data_sizes: u32 = num_data_sizes.into();
+                if num_data_sizes as usize > data_sizes.len() {
+                    return Err(InvalidArgument);
+                }
+                let data_sizes = data_sizes[0..num_data_sizes as usize]
+                    .iter()
+                    .map(|x| Into::<u32>::into(*x))
+                    .collect::<Vec<u32>>();
+                ResourceQueue {
+                    stream_id: hdr.stream_id.into(),
+                    queue_type: queue_type.try_into()?,
+                    resource_id: resource_id.into(),
+                    timestamp: timestamp.into(),
+                    data_sizes,
+                }
+            }
+            VIRTIO_VIDEO_CMD_RESOURCE_DESTROY_ALL => {
+                let virtio_video_resource_destroy_all {
+
+                    // `queue_type` should be ignored because destroy_all will affect both queues.
+                    // This field exists here by mistake.
+                    ..
+                } = r.read_obj()?;
+                ResourceDestroyAll {
+                    stream_id: hdr.stream_id.into(),
+                }
+            }
+            VIRTIO_VIDEO_CMD_QUEUE_CLEAR => {
+                let virtio_video_queue_clear { queue_type, .. } = r.read_obj()?;
+                QueueClear {
+                    stream_id: hdr.stream_id.into(),
+                    queue_type: queue_type.try_into()?,
+                }
+            }
+            VIRTIO_VIDEO_CMD_GET_PARAMS => {
+                let virtio_video_get_params { queue_type, .. } = r.read_obj()?;
+                GetParams {
+                    stream_id: hdr.stream_id.into(),
+                    queue_type: queue_type.try_into()?,
+                }
+            }
+            VIRTIO_VIDEO_CMD_SET_PARAMS => {
+                let virtio_video_set_params { params } = r.read_obj()?;
+                SetParams {
+                    stream_id: hdr.stream_id.into(),
+                    queue_type: params.queue_type.try_into()?,
+                    params: params.try_into()?,
+                }
+            }
+            VIRTIO_VIDEO_CMD_QUERY_CONTROL => {
+                let body = r.read_obj::<virtio_video_query_control>()?;
+                let query_ctrl_type = match body.control.into() {
+                    VIRTIO_VIDEO_CONTROL_BITRATE => QueryCtrlType::Bitrate,
+                    VIRTIO_VIDEO_CONTROL_PROFILE => QueryCtrlType::Profile(
+                        r.read_obj::<virtio_video_query_control_profile>()?
+                            .format
+                            .try_into()?,
+                    ),
+                    VIRTIO_VIDEO_CONTROL_LEVEL => QueryCtrlType::Level(
+                        r.read_obj::<virtio_video_query_control_level>()?
+                            .format
+                            .try_into()?,
+                    ),
+                    t => {
+                        return Err(ReadCmdError::UnsupportedCtrlType(t));
+                    }
+                };
+                QueryControl { query_ctrl_type }
+            }
+            VIRTIO_VIDEO_CMD_GET_CONTROL => {
+                let virtio_video_get_control { control, .. } = r.read_obj()?;
+                let ctrl_type = match control.into() {
+                    VIRTIO_VIDEO_CONTROL_BITRATE => CtrlType::Bitrate,
+                    VIRTIO_VIDEO_CONTROL_PROFILE => CtrlType::Profile,
+                    VIRTIO_VIDEO_CONTROL_LEVEL => CtrlType::Level,
+                    t => {
+                        return Err(ReadCmdError::UnsupportedCtrlType(t));
+                    }
+                };
+                GetControl {
+                    stream_id: hdr.stream_id.into(),
+                    ctrl_type,
+                }
+            }
+            VIRTIO_VIDEO_CMD_SET_CONTROL => {
+                let virtio_video_set_control { control, .. } = r.read_obj()?;
+                let ctrl_val = match control.into() {
+                    VIRTIO_VIDEO_CONTROL_BITRATE => CtrlVal::Bitrate(
+                        r.read_obj::<virtio_video_control_val_bitrate>()?
+                            .bitrate
+                            .into(),
+                    ),
+                    VIRTIO_VIDEO_CONTROL_PROFILE => CtrlVal::Profile(
+                        r.read_obj::<virtio_video_control_val_profile>()?
+                            .profile
+                            .try_into()?,
+                    ),
+                    VIRTIO_VIDEO_CONTROL_LEVEL => CtrlVal::Level(
+                        r.read_obj::<virtio_video_control_val_level>()?
+                            .level
+                            .try_into()?,
+                    ),
+                    t => {
+                        return Err(ReadCmdError::UnsupportedCtrlType(t));
+                    }
+                };
+                SetControl {
+                    stream_id: hdr.stream_id.into(),
+                    ctrl_val,
+                }
+            }
+            _ => return Err(ReadCmdError::InvalidCmdType(hdr.type_.into())),
+        })
+    }
+}
diff --git a/devices/src/virtio/video/control.rs b/devices/src/virtio/video/control.rs
new file mode 100644
index 0000000..6394dc2
--- /dev/null
+++ b/devices/src/virtio/video/control.rs
@@ -0,0 +1,83 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Implementation of data structures for virtio-video controls.
+
+use std::convert::From;
+use std::io;
+
+use data_model::Le32;
+
+use crate::virtio::video::format::{Format, Level, Profile};
+use crate::virtio::video::protocol::*;
+use crate::virtio::video::response::Response;
+use crate::virtio::Writer;
+
+#[derive(Debug)]
+pub enum QueryCtrlType {
+    Bitrate,
+    Profile(Format),
+    Level(Format),
+}
+
+#[allow(dead_code)] // TODO(keiichiw): Remove this attribute
+#[derive(Debug)]
+pub enum QueryCtrlResponse {
+    Profile(Vec<Profile>),
+    Level(Vec<Level>),
+}
+
+impl Response for QueryCtrlResponse {
+    fn write(&self, w: &mut Writer) -> Result<(), io::Error> {
+        match self {
+            QueryCtrlResponse::Profile(ps) => {
+                w.write_obj(virtio_video_query_control_resp_profile {
+                    num: Le32::from(ps.len() as u32),
+                    ..Default::default()
+                })?;
+                w.write_iter(ps.iter().map(|p| Le32::from(*p as u32)))
+            }
+            QueryCtrlResponse::Level(ls) => {
+                w.write_obj(virtio_video_query_control_resp_level {
+                    num: Le32::from(ls.len() as u32),
+                    ..Default::default()
+                })?;
+                w.write_iter(ls.iter().map(|l| Le32::from(*l as u32)))
+            }
+        }
+    }
+}
+
+#[derive(Debug)]
+pub enum CtrlType {
+    Bitrate,
+    Profile,
+    Level,
+}
+
+#[derive(Debug)]
+pub enum CtrlVal {
+    Bitrate(u32),
+    Profile(Profile),
+    Level(Level),
+}
+
+impl Response for CtrlVal {
+    fn write(&self, w: &mut Writer) -> Result<(), io::Error> {
+        match self {
+            CtrlVal::Bitrate(r) => w.write_obj(virtio_video_control_val_bitrate {
+                bitrate: Le32::from(*r),
+                ..Default::default()
+            }),
+            CtrlVal::Profile(p) => w.write_obj(virtio_video_control_val_profile {
+                profile: Le32::from(*p as u32),
+                ..Default::default()
+            }),
+            CtrlVal::Level(l) => w.write_obj(virtio_video_control_val_level {
+                level: Le32::from(*l as u32),
+                ..Default::default()
+            }),
+        }
+    }
+}
diff --git a/devices/src/virtio/video/decoder/mod.rs b/devices/src/virtio/video/decoder/mod.rs
new file mode 100644
index 0000000..d283a11
--- /dev/null
+++ b/devices/src/virtio/video/decoder/mod.rs
@@ -0,0 +1,40 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Implementation of the the `Decoder` struct, which is responsible for translation between the
+//! virtio protocols and LibVDA APIs.
+
+use sys_util::PollContext;
+
+use crate::virtio::resource_bridge::ResourceRequestSocket;
+use crate::virtio::video::command::VideoCmd;
+use crate::virtio::video::device::{Device, Token, VideoCmdResponseType, VideoEvtResponseType};
+use crate::virtio::video::error::*;
+
+pub struct Decoder;
+
+impl Decoder {
+    pub fn new() -> Self {
+        Decoder {}
+    }
+}
+
+impl Device for Decoder {
+    fn process_cmd(
+        &mut self,
+        _cmd: VideoCmd,
+        _poll_ctx: &PollContext<Token>,
+        _resource_bridge: &ResourceRequestSocket,
+    ) -> VideoResult<VideoCmdResponseType> {
+        Err(VideoError::InvalidOperation)
+    }
+
+    fn process_event_fd(&mut self, _stream_id: u32) -> Option<VideoEvtResponseType> {
+        None
+    }
+
+    fn take_resource_id_to_notify_eos(&mut self, _stream_id: u32) -> Option<u32> {
+        None
+    }
+}
diff --git a/devices/src/virtio/video/device.rs b/devices/src/virtio/video/device.rs
new file mode 100644
index 0000000..d3ec1d6
--- /dev/null
+++ b/devices/src/virtio/video/device.rs
@@ -0,0 +1,93 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Definition of the trait `Device` that each backend video device must implement.
+
+#![allow(dead_code)] // TODO(keiichiw): Remove this attribute
+
+use sys_util::{PollContext, PollToken};
+
+use crate::virtio::resource_bridge::ResourceRequestSocket;
+use crate::virtio::video::command::{QueueType, VideoCmd};
+use crate::virtio::video::error::*;
+use crate::virtio::video::event::VideoEvt;
+use crate::virtio::video::response;
+
+#[derive(PollToken, Debug)]
+pub enum Token {
+    CmdQueue,
+    EventQueue,
+    EventFd { id: u32 },
+    Kill,
+    InterruptResample,
+}
+
+/// A tag for commands being processed asynchronously in the back-end device.
+/// TODO(b/149720783): Remove this enum by using async primitives.
+#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)]
+pub enum AsyncCmdTag {
+    Queue {
+        stream_id: u32,
+        queue_type: QueueType,
+        resource_id: u32,
+    },
+    Drain {
+        stream_id: u32,
+    },
+    Clear {
+        stream_id: u32,
+        queue_type: QueueType,
+    },
+}
+
+/// A return value when a command from the guest is processed.
+#[derive(Debug)]
+pub enum VideoCmdResponseType {
+    /// The response for a synchronous command. This can be returned to the guest immediately via
+    /// command virtqueue.
+    Sync(response::CmdResponse),
+    /// The tag for an asynchronous command that the back-end device will complete.
+    /// Once the command is completed, its result will be sent with the same tag.
+    /// This can be seen as a poor man's future pattern.
+    Async(AsyncCmdTag),
+}
+
+/// A return value when processing a event the back-end device sent.
+#[derive(Debug)]
+pub enum VideoEvtResponseType {
+    /// The response for an asynchronous command that was enqueued through `process_cmd` before.
+    /// The `tag` must be same as the one returned when the command is enqueued.
+    AsyncCmd {
+        tag: AsyncCmdTag,
+        resp: VideoResult<response::CmdResponse>,
+    },
+    /// The event that happened in the back-end device.
+    Event(VideoEvt),
+}
+
+pub trait Device {
+    /// Processes a virtio-video command.
+    /// If the command expects a synchronous response, it returns a response as `VideoCmdResponseType::Sync`.
+    /// Otherwise, it returns a name of the descriptor chain that will be used when a response is prepared.
+    /// Implementations of this method is passed a PollContext object which can be used to add or remove
+    /// FDs to poll. It is expected that only Token::EventFd items would be added. When a Token::EventFd
+    /// event arrives, process_event_fd() will be invoked.
+    /// TODO(b/149720783): Make this an async function.
+    fn process_cmd(
+        &mut self,
+        cmd: VideoCmd,
+        poll_ctx: &PollContext<Token>,
+        resource_bridge: &ResourceRequestSocket,
+    ) -> VideoResult<VideoCmdResponseType>;
+
+    /// Processes an available Token::EventFd event.
+    /// If the message is sent via commandq, the return value is `VideoEvtResponseType::AsyncCmd`.
+    /// Otherwise (i.e. case of eventq), it's `VideoEvtResponseType::Event`.
+    /// TODO(b/149720783): Make this an async function.
+    fn process_event_fd(&mut self, stream_id: u32) -> Option<VideoEvtResponseType>;
+
+    /// Returns an ID for an available output resource that can be used to notify EOS.
+    /// Note that this resource must be enqueued by `ResourceQueue` and not be returned yet.
+    fn take_resource_id_to_notify_eos(&mut self, stream_id: u32) -> Option<u32>;
+}
diff --git a/devices/src/virtio/video/encoder/mod.rs b/devices/src/virtio/video/encoder/mod.rs
new file mode 100644
index 0000000..d6b8cef
--- /dev/null
+++ b/devices/src/virtio/video/encoder/mod.rs
@@ -0,0 +1,40 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Implementation of the the `Encoder` struct, which is responsible for translation between the
+//! virtio protocols and LibVDA APIs.
+
+use sys_util::PollContext;
+
+use crate::virtio::resource_bridge::ResourceRequestSocket;
+use crate::virtio::video::command::VideoCmd;
+use crate::virtio::video::device::{Device, Token, VideoCmdResponseType, VideoEvtResponseType};
+use crate::virtio::video::error::*;
+
+pub struct Encoder;
+
+impl Encoder {
+    pub fn new() -> Self {
+        Encoder {}
+    }
+}
+
+impl Device for Encoder {
+    fn process_cmd(
+        &mut self,
+        _cmd: VideoCmd,
+        _poll_ctx: &PollContext<Token>,
+        _resource_bridge: &ResourceRequestSocket,
+    ) -> VideoResult<VideoCmdResponseType> {
+        Err(VideoError::InvalidOperation)
+    }
+
+    fn process_event_fd(&mut self, _stream_id: u32) -> Option<VideoEvtResponseType> {
+        None
+    }
+
+    fn take_resource_id_to_notify_eos(&mut self, _stream_id: u32) -> Option<u32> {
+        None
+    }
+}
diff --git a/devices/src/virtio/video/error.rs b/devices/src/virtio/video/error.rs
new file mode 100644
index 0000000..1709306
--- /dev/null
+++ b/devices/src/virtio/video/error.rs
@@ -0,0 +1,84 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Errors that can happen while encoding or decoding.
+
+use std::fmt;
+use std::io;
+
+use data_model::Le32;
+
+use crate::virtio::resource_bridge::ResourceBridgeError;
+use crate::virtio::video::protocol::*;
+use crate::virtio::video::response::Response;
+use crate::virtio::Writer;
+
+/// An error indicating something went wrong while encoding or decoding.
+/// Unlike `virtio::video::Error`, `VideoError` is not fatal for `Worker`.
+#[derive(Debug)]
+#[allow(dead_code)]
+pub enum VideoError {
+    /// Invalid argument.
+    InvalidArgument,
+    /// Invalid operation
+    InvalidOperation,
+    /// Invalid stream ID is specified.
+    InvalidStreamId(u32),
+    /// Invalid resource ID is specified.
+    InvalidResourceId { stream_id: u32, resource_id: u32 },
+    /// Invalid parameters are specified.
+    InvalidParameter,
+    /// Failed to get a resource FD via resource_bridge.
+    ResourceBridgeFailure(ResourceBridgeError),
+    /// `libvda` returned an error.
+    VdaError(libvda::Error),
+    /// `libvda` returned a failure response.
+    VdaFailure(libvda::Response),
+}
+
+impl fmt::Display for VideoError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use self::VideoError::*;
+        match self {
+            InvalidArgument => write!(f, "invalid argument"),
+            InvalidOperation => write!(f, "invalid operation"),
+            InvalidStreamId(id) => write!(f, "invalid stream ID {}", id),
+            InvalidResourceId {
+                stream_id,
+                resource_id,
+            } => write!(
+                f,
+                "invalid resource ID {} for stream {}",
+                resource_id, stream_id
+            ),
+            InvalidParameter => write!(f, "invalid parameter"),
+            ResourceBridgeFailure(id) => write!(f, "failed to get resource FD for id {}", id),
+            VdaError(e) => write!(f, "error occurred in libvda: {}", e),
+            VdaFailure(r) => write!(f, "failed while processing a requst in VDA: {}", r),
+        }
+    }
+}
+
+impl std::error::Error for VideoError {}
+
+pub type VideoResult<T> = Result<T, VideoError>;
+
+impl Response for VideoError {
+    fn write(&self, w: &mut Writer) -> Result<(), io::Error> {
+        use VideoError::*;
+
+        let type_ = Le32::from(match *self {
+            InvalidResourceId { .. } => VIRTIO_VIDEO_RESP_ERR_INVALID_RESOURCE_ID,
+            InvalidStreamId(_) => VIRTIO_VIDEO_RESP_ERR_INVALID_STREAM_ID,
+            InvalidParameter => VIRTIO_VIDEO_RESP_ERR_INVALID_PARAMETER,
+            // TODO(b/1518105): Add more detailed error code if a new protocol supports ones.
+            _ => VIRTIO_VIDEO_RESP_ERR_INVALID_OPERATION,
+        });
+
+        w.write_obj(virtio_video_cmd_hdr {
+            type_,
+            ..Default::default()
+        })
+    }
+}
diff --git a/devices/src/virtio/video/event.rs b/devices/src/virtio/video/event.rs
new file mode 100644
index 0000000..8c49253
--- /dev/null
+++ b/devices/src/virtio/video/event.rs
@@ -0,0 +1,36 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Events can happen in virtio video devices.
+
+use std::io;
+
+use data_model::Le32;
+use enumn::N;
+
+use crate::virtio::video::protocol::*;
+use crate::virtio::video::response::Response;
+use crate::virtio::Writer;
+
+#[allow(dead_code)] // TODO(keiichiw): Remove this attribute
+#[derive(Debug, Copy, Clone, N)]
+pub enum EvtType {
+    Error = VIRTIO_VIDEO_EVENT_ERROR as isize,
+    DecResChanged = VIRTIO_VIDEO_EVENT_DECODER_RESOLUTION_CHANGED as isize,
+}
+
+#[derive(Debug, Clone)]
+pub struct VideoEvt {
+    pub typ: EvtType,
+    pub stream_id: u32,
+}
+
+impl Response for VideoEvt {
+    fn write(&self, w: &mut Writer) -> Result<(), io::Error> {
+        w.write_obj(virtio_video_event {
+            event_type: Le32::from(self.typ as u32),
+            stream_id: Le32::from(self.stream_id),
+        })
+    }
+}
diff --git a/devices/src/virtio/video/format.rs b/devices/src/virtio/video/format.rs
new file mode 100644
index 0000000..fc2c441
--- /dev/null
+++ b/devices/src/virtio/video/format.rs
@@ -0,0 +1,116 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Data structures that represent video format information in virtio video devices.
+
+use std::convert::{From, Into, TryFrom};
+use std::io;
+
+use data_model::Le32;
+use enumn::N;
+use sys_util::error;
+
+use crate::virtio::video::command::ReadCmdError;
+use crate::virtio::video::protocol::*;
+use crate::virtio::video::response::Response;
+use crate::virtio::Writer;
+
+#[derive(PartialEq, Eq, PartialOrd, Ord, N, Clone, Copy, Debug)]
+#[repr(u32)]
+pub enum Profile {
+    VP8Profile0 = VIRTIO_VIDEO_PROFILE_VP8_PROFILE0,
+    VP9Profile0 = VIRTIO_VIDEO_PROFILE_VP9_PROFILE0,
+    H264Baseline = VIRTIO_VIDEO_PROFILE_H264_BASELINE,
+}
+impl_try_from_le32_for_enumn!(Profile, "profile");
+
+#[derive(PartialEq, Eq, PartialOrd, Ord, N, Clone, Copy, Debug)]
+#[repr(u32)]
+pub enum Level {
+    H264_1_0 = VIRTIO_VIDEO_LEVEL_H264_1_0,
+}
+impl_try_from_le32_for_enumn!(Level, "level");
+
+#[derive(PartialEq, Eq, PartialOrd, Ord, N, Clone, Copy, Debug)]
+#[repr(u32)]
+pub enum Format {
+    // Raw formats
+    NV12 = VIRTIO_VIDEO_FORMAT_NV12,
+    YUV420 = VIRTIO_VIDEO_FORMAT_YUV420,
+
+    // Bitstream formats
+    H264 = VIRTIO_VIDEO_FORMAT_H264,
+    VP8 = VIRTIO_VIDEO_FORMAT_VP8,
+    VP9 = VIRTIO_VIDEO_FORMAT_VP9,
+}
+impl_try_from_le32_for_enumn!(Format, "format");
+
+#[derive(Debug, Default, Copy, Clone)]
+pub struct Crop {
+    pub left: u32,
+    pub top: u32,
+    pub width: u32,
+    pub height: u32,
+}
+impl_from_for_interconvertible_structs!(virtio_video_crop, Crop, left, top, width, height);
+
+#[derive(Debug, Default, Clone, Copy)]
+pub struct PlaneFormat {
+    pub plane_size: u32,
+    pub stride: u32,
+}
+impl_from_for_interconvertible_structs!(virtio_video_plane_format, PlaneFormat, plane_size, stride);
+
+#[derive(Debug, Default, Clone, Copy)]
+pub struct FormatRange {
+    pub min: u32,
+    pub max: u32,
+    pub step: u32,
+}
+impl_from_for_interconvertible_structs!(virtio_video_format_range, FormatRange, min, max, step);
+
+#[derive(Debug, Default, Clone)]
+pub struct FrameFormat {
+    pub width: FormatRange,
+    pub height: FormatRange,
+    pub bitrates: Vec<FormatRange>,
+}
+
+impl Response for FrameFormat {
+    fn write(&self, w: &mut Writer) -> Result<(), io::Error> {
+        w.write_obj(virtio_video_format_frame {
+            width: self.width.into(),
+            height: self.height.into(),
+            num_rates: Le32::from(self.bitrates.len() as u32),
+            ..Default::default()
+        })?;
+        w.write_iter(
+            self.bitrates
+                .iter()
+                .map(|r| Into::<virtio_video_format_range>::into(*r)),
+        )
+    }
+}
+
+#[derive(Debug, Clone)]
+pub struct FormatDesc {
+    pub mask: u64,
+    pub format: Format,
+    pub frame_formats: Vec<FrameFormat>,
+}
+
+impl Response for FormatDesc {
+    fn write(&self, w: &mut Writer) -> Result<(), io::Error> {
+        w.write_obj(virtio_video_format_desc {
+            mask: self.mask.into(),
+            format: Le32::from(self.format as u32),
+            // ChromeOS only supports single-buffer mode.
+            planes_layout: Le32::from(VIRTIO_VIDEO_PLANES_LAYOUT_SINGLE_BUFFER),
+            // No alignment is required on boards that we currently support.
+            plane_align: Le32::from(0),
+            num_frames: Le32::from(self.frame_formats.len() as u32),
+        })?;
+        self.frame_formats.iter().map(|ff| ff.write(w)).collect()
+    }
+}
diff --git a/devices/src/virtio/video/macros.rs b/devices/src/virtio/video/macros.rs
new file mode 100644
index 0000000..d1ed386
--- /dev/null
+++ b/devices/src/virtio/video/macros.rs
@@ -0,0 +1,46 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Macros that helps virtio video implementation.
+
+/// Implements TryFrom<data_model::Le32> for an enum that implements `enumn::N`.
+#[macro_export]
+macro_rules! impl_try_from_le32_for_enumn {
+    ($ty:ty, $name:literal) => {
+        impl TryFrom<Le32> for $ty {
+            type Error = ReadCmdError;
+
+            fn try_from(x: Le32) -> Result<Self, Self::Error> {
+                let v: u32 = x.into();
+                Self::n(v).ok_or_else(|| {
+                    error!(concat!("invalid ", $name, ": {}"), v);
+                    ReadCmdError::InvalidArgument
+                })
+            }
+        }
+    };
+}
+
+/// Implements `From` between two structs whose each field implements `From` each other.
+#[macro_export]
+macro_rules! impl_from_for_interconvertible_structs {
+    ($t1:ident, $t2:ident, $($v:ident),+) => {
+        impl_from_for_interconvertible_structs_core!($t1, $t2, $( $v ),+ );
+        impl_from_for_interconvertible_structs_core!($t2, $t1, $( $v ),+ );
+    };
+}
+
+macro_rules! impl_from_for_interconvertible_structs_core {
+    ($t1:ident, $t2:ident, $($v:ident),+) => {
+        impl From<$t1> for $t2 {
+            #[allow(clippy::needless_update)]
+            fn from(x :$t1) -> Self {
+                $t2 {
+                    $( $v: x.$v.into(), )+
+                    ..Default::default() // for paddings
+                }
+            }
+        }
+    };
+}
diff --git a/devices/src/virtio/video/mod.rs b/devices/src/virtio/video/mod.rs
new file mode 100644
index 0000000..295cd8c
--- /dev/null
+++ b/devices/src/virtio/video/mod.rs
@@ -0,0 +1,250 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! This module implements the virtio video encoder and decoder devices.
+//! The current implementation uses [v3 RFC] of the virtio-video protocol.
+//!
+//! [v3 RFC]: https://markmail.org/thread/wxdne5re7aaugbjg
+
+use std::fmt::{self, Display};
+use std::os::unix::io::{AsRawFd, RawFd};
+use std::thread;
+
+use data_model::{DataInit, Le32};
+use sys_util::{error, Error as SysError, EventFd, GuestMemory};
+
+use crate::virtio::resource_bridge::ResourceRequestSocket;
+use crate::virtio::virtio_device::VirtioDevice;
+use crate::virtio::{self, copy_config, DescriptorError, Interrupt, VIRTIO_F_VERSION_1};
+
+#[macro_use]
+mod macros;
+mod command;
+mod control;
+mod decoder;
+mod device;
+mod encoder;
+mod error;
+mod event;
+mod format;
+mod params;
+mod protocol;
+mod response;
+mod worker;
+
+use command::ReadCmdError;
+use device::AsyncCmdTag;
+use worker::Worker;
+
+const QUEUE_SIZE: u16 = 256;
+const QUEUE_SIZES: &[u16] = &[QUEUE_SIZE, QUEUE_SIZE];
+
+/// An error indicating something went wrong in virtio-video's worker.
+#[derive(Debug)]
+pub enum Error {
+    /// Creating PollContext failed.
+    PollContextCreationFailed(SysError),
+    /// A DescriptorChain contains invalid data.
+    InvalidDescriptorChain(DescriptorError),
+    /// Invalid output buffer is specified for EOS notification.
+    InvalidEOSResource { stream_id: u32, resource_id: u32 },
+    /// No available descriptor in which an event is written to.
+    DescriptorNotAvailable,
+    /// Output buffer for EOS is unavailable.
+    NoEOSBuffer { stream_id: u32 },
+    /// Error while polling for events.
+    PollError(SysError),
+    /// Failed to read a virtio-video command.
+    ReadFailure(ReadCmdError),
+    /// Got response for an unexpected asynchronous command.
+    UnexpectedResponse(AsyncCmdTag),
+    /// Failed to write an event into the event queue.
+    WriteEventFailure {
+        event: event::VideoEvt,
+        error: std::io::Error,
+    },
+}
+
+impl Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use Error::*;
+        match self {
+            PollContextCreationFailed(e) => write!(f, "failed to create PollContext: {}", e),
+            InvalidDescriptorChain(e) => write!(f, "DescriptorChain contains invalid data: {}", e),
+            InvalidEOSResource {
+                stream_id,
+                resource_id,
+            } => write!(
+                f,
+                "invalid resource {} was specified for stream {}'s EOS",
+                resource_id, stream_id
+            ),
+            DescriptorNotAvailable => {
+                write!(f, "no available descriptor in which an event is written to")
+            }
+            NoEOSBuffer { stream_id } => write!(
+                f,
+                "no output resource is available to notify EOS: {}",
+                stream_id
+            ),
+            PollError(err) => write!(f, "failed to poll events: {}", err),
+            ReadFailure(e) => write!(f, "failed to read a command from the guest: {}", e),
+            UnexpectedResponse(tag) => {
+                write!(f, "got a response for an untracked command: {:?}", tag)
+            }
+            WriteEventFailure { event, error } => write!(
+                f,
+                "failed to write an event {:?} into event queue: {}",
+                event, error
+            ),
+        }
+    }
+}
+
+impl std::error::Error for Error {}
+pub type Result<T> = std::result::Result<T, Error>;
+
+#[derive(Debug)]
+pub enum VideoDeviceType {
+    Decoder,
+    Encoder,
+}
+
+pub struct VideoDevice {
+    device_type: VideoDeviceType,
+    kill_evt: Option<EventFd>,
+    resource_bridge: Option<ResourceRequestSocket>,
+}
+
+impl VideoDevice {
+    pub fn new(
+        device_type: VideoDeviceType,
+        resource_bridge: Option<ResourceRequestSocket>,
+    ) -> VideoDevice {
+        VideoDevice {
+            device_type,
+            kill_evt: None,
+            resource_bridge,
+        }
+    }
+}
+
+impl Drop for VideoDevice {
+    fn drop(&mut self) {
+        if let Some(kill_evt) = self.kill_evt.take() {
+            // Ignore the result because there is nothing we can do about it.
+            let _ = kill_evt.write(1);
+        }
+    }
+}
+
+impl VirtioDevice for VideoDevice {
+    fn keep_fds(&self) -> Vec<RawFd> {
+        let mut keep_fds = Vec::new();
+        if let Some(resource_bridge) = &self.resource_bridge {
+            keep_fds.push(resource_bridge.as_raw_fd());
+        }
+        keep_fds
+    }
+
+    fn device_type(&self) -> u32 {
+        match &self.device_type {
+            VideoDeviceType::Decoder => virtio::TYPE_VIDEO_DEC,
+            VideoDeviceType::Encoder => virtio::TYPE_VIDEO_ENC,
+        }
+    }
+
+    fn queue_max_sizes(&self) -> &[u16] {
+        QUEUE_SIZES
+    }
+
+    fn features(&self) -> u64 {
+        1u64 << VIRTIO_F_VERSION_1
+            | 1u64 << protocol::VIRTIO_VIDEO_F_RESOURCE_NON_CONTIG
+            | 1u64 << protocol::VIRTIO_VIDEO_F_RESOURCE_VIRTIO_OBJECT
+    }
+
+    fn read_config(&self, offset: u64, data: &mut [u8]) {
+        let mut cfg = protocol::virtio_video_config {
+            version: Le32::from(0),
+            max_caps_length: Le32::from(1024), // Set a big number
+            max_resp_length: Le32::from(1024), // Set a big number
+        };
+        copy_config(data, 0, cfg.as_mut_slice(), offset);
+    }
+
+    fn activate(
+        &mut self,
+        mem: GuestMemory,
+        interrupt: Interrupt,
+        mut queues: Vec<virtio::queue::Queue>,
+        mut queue_evts: Vec<EventFd>,
+    ) {
+        if queues.len() != QUEUE_SIZES.len() {
+            error!(
+                "wrong number of queues are passed: expected {}, actual {}",
+                queues.len(),
+                QUEUE_SIZES.len()
+            );
+            return;
+        }
+        if queue_evts.len() != QUEUE_SIZES.len() {
+            error!(
+                "wrong number of event FDs are passed: expected {}, actual {}",
+                queue_evts.len(),
+                QUEUE_SIZES.len()
+            );
+        }
+
+        let (self_kill_evt, kill_evt) = match EventFd::new().and_then(|e| Ok((e.try_clone()?, e))) {
+            Ok(v) => v,
+            Err(e) => {
+                error!("failed to create kill EventFd pair: {:?}", e);
+                return;
+            }
+        };
+        self.kill_evt = Some(self_kill_evt);
+
+        let cmd_queue = queues.remove(0);
+        let cmd_evt = queue_evts.remove(0);
+        let event_queue = queues.remove(0);
+        let event_evt = queue_evts.remove(0);
+        let resource_bridge = match self.resource_bridge.take() {
+            Some(r) => r,
+            None => {
+                error!("no resource bridge is passed");
+                return;
+            }
+        };
+        let mut worker = Worker {
+            interrupt,
+            mem,
+            cmd_evt,
+            event_evt,
+            kill_evt,
+            resource_bridge,
+        };
+        let worker_result = match &self.device_type {
+            VideoDeviceType::Decoder => thread::Builder::new()
+                .name("virtio video decoder".to_owned())
+                .spawn(move || {
+                    let device = decoder::Decoder::new();
+                    worker.run(cmd_queue, event_queue, device)
+                }),
+            VideoDeviceType::Encoder => thread::Builder::new()
+                .name("virtio video encoder".to_owned())
+                .spawn(move || {
+                    let device = encoder::Encoder::new();
+                    worker.run(cmd_queue, event_queue, device)
+                }),
+        };
+        if let Err(e) = worker_result {
+            error!(
+                "failed to spawn virtio_video worker for {:?}: {}",
+                &self.device_type, e
+            );
+            return;
+        }
+    }
+}
diff --git a/devices/src/virtio/video/params.rs b/devices/src/virtio/video/params.rs
new file mode 100644
index 0000000..e4bb614
--- /dev/null
+++ b/devices/src/virtio/video/params.rs
@@ -0,0 +1,111 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Parameters for streams in virtio video devices.
+
+use std::convert::{From, Into, TryFrom};
+
+use data_model::Le32;
+use sys_util::error;
+
+use crate::virtio::video::command::{QueueType, ReadCmdError};
+use crate::virtio::video::format::*;
+use crate::virtio::video::protocol::*;
+
+/// Safe wrapper of `virtio_video_params`.
+/// Note that this struct doesn't have a field corresponding to `queue_type` in
+/// `virtio_video_params`. The type of queue should be stored by one that has an `Params` instance.
+#[derive(Debug, Default, Clone)]
+pub struct Params {
+    // Use `Option<Format>` instead of `Format` because an image format may not be determined until
+    // video decoding is started in the decoder.
+    pub format: Option<Format>,
+    pub frame_width: u32,
+    pub frame_height: u32,
+    pub min_buffers: u32,
+    pub max_buffers: u32,
+    pub crop: Crop,
+    pub frame_rate: u32,
+    pub plane_formats: Vec<PlaneFormat>,
+}
+
+impl TryFrom<virtio_video_params> for Params {
+    type Error = ReadCmdError;
+
+    fn try_from(
+        virtio_video_params {
+            format,
+            frame_width,
+            frame_height,
+            min_buffers,
+            max_buffers,
+            crop,
+            frame_rate,
+            num_planes,
+            plane_formats,
+            ..
+        }: virtio_video_params,
+    ) -> Result<Self, Self::Error> {
+        let num_planes = Into::<u32>::into(num_planes); // as usize;
+        if num_planes as usize > plane_formats.len() {
+            error!(
+                "num_planes must not exceed {} but {}",
+                plane_formats.len(),
+                Into::<u32>::into(num_planes)
+            );
+            return Err(ReadCmdError::InvalidArgument);
+        }
+        let plane_formats = plane_formats[0..num_planes as usize]
+            .iter()
+            .map(|x| Into::<PlaneFormat>::into(*x))
+            .collect::<Vec<_>>();
+
+        Ok(Params {
+            format: Format::n(format.into()),
+            frame_width: frame_width.into(),
+            frame_height: frame_height.into(),
+            min_buffers: min_buffers.into(),
+            max_buffers: max_buffers.into(),
+            crop: crop.into(),
+            frame_rate: frame_rate.into(),
+            plane_formats,
+        })
+    }
+}
+
+impl Params {
+    pub fn to_virtio_video_params(&self, queue_type: QueueType) -> virtio_video_params {
+        let Params {
+            format,
+            frame_width,
+            frame_height,
+            min_buffers,
+            max_buffers,
+            crop,
+            frame_rate,
+            plane_formats,
+        } = self;
+        let num_planes = Le32::from(plane_formats.len() as u32);
+        let mut p_fmts: [virtio_video_plane_format; VIRTIO_VIDEO_MAX_PLANES as usize] =
+            Default::default();
+        for (i, pf) in plane_formats.iter().enumerate() {
+            p_fmts[i] = Into::<virtio_video_plane_format>::into(*pf);
+        }
+
+        virtio_video_params {
+            queue_type: (queue_type as u32).into(),
+            format: format
+                .map(|f| Le32::from(f as u32))
+                .unwrap_or_else(|| Le32::from(0)),
+            frame_width: Le32::from(*frame_width),
+            frame_height: Le32::from(*frame_height),
+            min_buffers: Le32::from(*min_buffers),
+            max_buffers: Le32::from(*max_buffers),
+            crop: virtio_video_crop::from(*crop),
+            frame_rate: Le32::from(*frame_rate),
+            num_planes,
+            plane_formats: p_fmts,
+        }
+    }
+}
diff --git a/devices/src/virtio/video/protocol.rs b/devices/src/virtio/video/protocol.rs
new file mode 100644
index 0000000..0e2106e
--- /dev/null
+++ b/devices/src/virtio/video/protocol.rs
@@ -0,0 +1,487 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! This file was generated by the following commands and modified manually.
+//!
+//! ```
+//! $ bindgen virtio_video.h              \
+//!     --whitelist-type "virtio_video.*" \
+//!     --whitelist-var "VIRTIO_VIDEO_.*" \
+//!     --with-derive-default            \
+//!     --no-layout-tests                \
+//!     --no-prepend-enum-name > protocol.rs
+//! $ sed -i 's/u/u/g' protocol.rs
+//! $ sed -i 's/Le/Le/g' protocol.rs
+//! ```
+//!
+//! The main points of the manual modifications are as follows:
+//! * Removed `hdr` from each command struct so that we can read the header and a command body separately.
+//!   (cf. [related discussion](https://markmail.org/message/tr5g6axqq2zzq64y))
+//! * Added implementations of DataInit for each struct.
+
+#![allow(dead_code, non_snake_case, non_camel_case_types)]
+
+use data_model::{DataInit, Le32, Le64};
+
+pub const VIRTIO_VIDEO_F_RESOURCE_GUEST_PAGES: u32 = 0;
+pub const VIRTIO_VIDEO_F_RESOURCE_NON_CONTIG: u32 = 1;
+pub const VIRTIO_VIDEO_F_RESOURCE_VIRTIO_OBJECT: u32 = 2;
+pub const VIRTIO_VIDEO_MAX_PLANES: u32 = 8;
+pub const VIRTIO_VIDEO_FORMAT_RAW_MIN: virtio_video_format = 1;
+pub const VIRTIO_VIDEO_FORMAT_ARGB8888: virtio_video_format = 1;
+pub const VIRTIO_VIDEO_FORMAT_BGRA8888: virtio_video_format = 2;
+pub const VIRTIO_VIDEO_FORMAT_NV12: virtio_video_format = 3;
+pub const VIRTIO_VIDEO_FORMAT_YUV420: virtio_video_format = 4;
+pub const VIRTIO_VIDEO_FORMAT_YVU420: virtio_video_format = 5;
+pub const VIRTIO_VIDEO_FORMAT_RAW_MAX: virtio_video_format = 5;
+pub const VIRTIO_VIDEO_FORMAT_CODED_MIN: virtio_video_format = 4096;
+pub const VIRTIO_VIDEO_FORMAT_MPEG2: virtio_video_format = 4096;
+pub const VIRTIO_VIDEO_FORMAT_MPEG4: virtio_video_format = 4097;
+pub const VIRTIO_VIDEO_FORMAT_H264: virtio_video_format = 4098;
+pub const VIRTIO_VIDEO_FORMAT_HEVC: virtio_video_format = 4099;
+pub const VIRTIO_VIDEO_FORMAT_VP8: virtio_video_format = 4100;
+pub const VIRTIO_VIDEO_FORMAT_VP9: virtio_video_format = 4101;
+pub const VIRTIO_VIDEO_FORMAT_CODED_MAX: virtio_video_format = 4101;
+pub type virtio_video_format = u32;
+pub const VIRTIO_VIDEO_PROFILE_H264_MIN: virtio_video_profile = 256;
+pub const VIRTIO_VIDEO_PROFILE_H264_BASELINE: virtio_video_profile = 256;
+pub const VIRTIO_VIDEO_PROFILE_H264_MAIN: virtio_video_profile = 257;
+pub const VIRTIO_VIDEO_PROFILE_H264_EXTENDED: virtio_video_profile = 258;
+pub const VIRTIO_VIDEO_PROFILE_H264_HIGH: virtio_video_profile = 259;
+pub const VIRTIO_VIDEO_PROFILE_H264_HIGH10PROFILE: virtio_video_profile = 260;
+pub const VIRTIO_VIDEO_PROFILE_H264_HIGH422PROFILE: virtio_video_profile = 261;
+pub const VIRTIO_VIDEO_PROFILE_H264_HIGH444PREDICTIVEPROFILE: virtio_video_profile = 262;
+pub const VIRTIO_VIDEO_PROFILE_H264_SCALABLEBASELINE: virtio_video_profile = 263;
+pub const VIRTIO_VIDEO_PROFILE_H264_SCALABLEHIGH: virtio_video_profile = 264;
+pub const VIRTIO_VIDEO_PROFILE_H264_STEREOHIGH: virtio_video_profile = 265;
+pub const VIRTIO_VIDEO_PROFILE_H264_MULTIVIEWHIGH: virtio_video_profile = 266;
+pub const VIRTIO_VIDEO_PROFILE_H264_MAX: virtio_video_profile = 266;
+pub const VIRTIO_VIDEO_PROFILE_HEVC_MIN: virtio_video_profile = 512;
+pub const VIRTIO_VIDEO_PROFILE_HEVC_MAIN: virtio_video_profile = 512;
+pub const VIRTIO_VIDEO_PROFILE_HEVC_MAIN10: virtio_video_profile = 513;
+pub const VIRTIO_VIDEO_PROFILE_HEVC_MAIN_STILL_PICTURE: virtio_video_profile = 514;
+pub const VIRTIO_VIDEO_PROFILE_HEVC_MAX: virtio_video_profile = 514;
+pub const VIRTIO_VIDEO_PROFILE_VP8_MIN: virtio_video_profile = 768;
+pub const VIRTIO_VIDEO_PROFILE_VP8_PROFILE0: virtio_video_profile = 768;
+pub const VIRTIO_VIDEO_PROFILE_VP8_PROFILE1: virtio_video_profile = 769;
+pub const VIRTIO_VIDEO_PROFILE_VP8_PROFILE2: virtio_video_profile = 770;
+pub const VIRTIO_VIDEO_PROFILE_VP8_PROFILE3: virtio_video_profile = 771;
+pub const VIRTIO_VIDEO_PROFILE_VP8_MAX: virtio_video_profile = 771;
+pub const VIRTIO_VIDEO_PROFILE_VP9_MIN: virtio_video_profile = 1024;
+pub const VIRTIO_VIDEO_PROFILE_VP9_PROFILE0: virtio_video_profile = 1024;
+pub const VIRTIO_VIDEO_PROFILE_VP9_PROFILE1: virtio_video_profile = 1025;
+pub const VIRTIO_VIDEO_PROFILE_VP9_PROFILE2: virtio_video_profile = 1026;
+pub const VIRTIO_VIDEO_PROFILE_VP9_PROFILE3: virtio_video_profile = 1027;
+pub const VIRTIO_VIDEO_PROFILE_VP9_MAX: virtio_video_profile = 1027;
+pub type virtio_video_profile = u32;
+pub const VIRTIO_VIDEO_LEVEL_H264_MIN: virtio_video_level = 256;
+pub const VIRTIO_VIDEO_LEVEL_H264_1_0: virtio_video_level = 256;
+pub const VIRTIO_VIDEO_LEVEL_H264_1_1: virtio_video_level = 257;
+pub const VIRTIO_VIDEO_LEVEL_H264_1_2: virtio_video_level = 258;
+pub const VIRTIO_VIDEO_LEVEL_H264_1_3: virtio_video_level = 259;
+pub const VIRTIO_VIDEO_LEVEL_H264_2_0: virtio_video_level = 260;
+pub const VIRTIO_VIDEO_LEVEL_H264_2_1: virtio_video_level = 261;
+pub const VIRTIO_VIDEO_LEVEL_H264_2_2: virtio_video_level = 262;
+pub const VIRTIO_VIDEO_LEVEL_H264_3_0: virtio_video_level = 263;
+pub const VIRTIO_VIDEO_LEVEL_H264_3_1: virtio_video_level = 264;
+pub const VIRTIO_VIDEO_LEVEL_H264_3_2: virtio_video_level = 265;
+pub const VIRTIO_VIDEO_LEVEL_H264_4_0: virtio_video_level = 266;
+pub const VIRTIO_VIDEO_LEVEL_H264_4_1: virtio_video_level = 267;
+pub const VIRTIO_VIDEO_LEVEL_H264_4_2: virtio_video_level = 268;
+pub const VIRTIO_VIDEO_LEVEL_H264_5_0: virtio_video_level = 269;
+pub const VIRTIO_VIDEO_LEVEL_H264_5_1: virtio_video_level = 270;
+pub const VIRTIO_VIDEO_LEVEL_H264_MAX: virtio_video_level = 270;
+pub type virtio_video_level = u32;
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_config {
+    pub version: Le32,
+    pub max_caps_length: Le32,
+    pub max_resp_length: Le32,
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_config {}
+
+pub const VIRTIO_VIDEO_CMD_QUERY_CAPABILITY: virtio_video_cmd_type = 256;
+pub const VIRTIO_VIDEO_CMD_STREAM_CREATE: virtio_video_cmd_type = 257;
+pub const VIRTIO_VIDEO_CMD_STREAM_DESTROY: virtio_video_cmd_type = 258;
+pub const VIRTIO_VIDEO_CMD_STREAM_DRAIN: virtio_video_cmd_type = 259;
+pub const VIRTIO_VIDEO_CMD_RESOURCE_CREATE: virtio_video_cmd_type = 260;
+pub const VIRTIO_VIDEO_CMD_RESOURCE_QUEUE: virtio_video_cmd_type = 261;
+pub const VIRTIO_VIDEO_CMD_RESOURCE_DESTROY_ALL: virtio_video_cmd_type = 262;
+pub const VIRTIO_VIDEO_CMD_QUEUE_CLEAR: virtio_video_cmd_type = 263;
+pub const VIRTIO_VIDEO_CMD_GET_PARAMS: virtio_video_cmd_type = 264;
+pub const VIRTIO_VIDEO_CMD_SET_PARAMS: virtio_video_cmd_type = 265;
+pub const VIRTIO_VIDEO_CMD_QUERY_CONTROL: virtio_video_cmd_type = 266;
+pub const VIRTIO_VIDEO_CMD_GET_CONTROL: virtio_video_cmd_type = 267;
+pub const VIRTIO_VIDEO_CMD_SET_CONTROL: virtio_video_cmd_type = 268;
+pub const VIRTIO_VIDEO_RESP_OK_NODATA: virtio_video_cmd_type = 512;
+pub const VIRTIO_VIDEO_RESP_OK_QUERY_CAPABILITY: virtio_video_cmd_type = 513;
+pub const VIRTIO_VIDEO_RESP_OK_RESOURCE_QUEUE: virtio_video_cmd_type = 514;
+pub const VIRTIO_VIDEO_RESP_OK_GET_PARAMS: virtio_video_cmd_type = 515;
+pub const VIRTIO_VIDEO_RESP_OK_QUERY_CONTROL: virtio_video_cmd_type = 516;
+pub const VIRTIO_VIDEO_RESP_OK_GET_CONTROL: virtio_video_cmd_type = 517;
+pub const VIRTIO_VIDEO_RESP_ERR_INVALID_OPERATION: virtio_video_cmd_type = 768;
+pub const VIRTIO_VIDEO_RESP_ERR_OUT_OF_MEMORY: virtio_video_cmd_type = 769;
+pub const VIRTIO_VIDEO_RESP_ERR_INVALID_STREAM_ID: virtio_video_cmd_type = 770;
+pub const VIRTIO_VIDEO_RESP_ERR_INVALID_RESOURCE_ID: virtio_video_cmd_type = 771;
+pub const VIRTIO_VIDEO_RESP_ERR_INVALID_PARAMETER: virtio_video_cmd_type = 772;
+pub const VIRTIO_VIDEO_RESP_ERR_UNSUPPORTED_CONTROL: virtio_video_cmd_type = 773;
+pub type virtio_video_cmd_type = u32;
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_cmd_hdr {
+    pub type_: Le32,
+    pub stream_id: Le32,
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_cmd_hdr {}
+
+pub const VIRTIO_VIDEO_QUEUE_TYPE_INPUT: virtio_video_queue_type = 256;
+pub const VIRTIO_VIDEO_QUEUE_TYPE_OUTPUT: virtio_video_queue_type = 257;
+pub type virtio_video_queue_type = u32;
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_query_capability {
+    pub queue_type: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_query_capability {}
+
+pub const VIRTIO_VIDEO_PLANES_LAYOUT_SINGLE_BUFFER: virtio_video_planes_layout_flag = 1;
+pub const VIRTIO_VIDEO_PLANES_LAYOUT_PER_PLANE: virtio_video_planes_layout_flag = 2;
+pub type virtio_video_planes_layout_flag = u32;
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_format_range {
+    pub min: Le32,
+    pub max: Le32,
+    pub step: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_format_range {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_format_frame {
+    pub width: virtio_video_format_range,
+    pub height: virtio_video_format_range,
+    pub num_rates: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_format_frame {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_format_desc {
+    pub mask: Le64,
+    pub format: Le32,
+    pub planes_layout: Le32,
+    pub plane_align: Le32,
+    pub num_frames: Le32,
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_format_desc {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_query_capability_resp {
+    pub hdr: virtio_video_cmd_hdr,
+    pub num_descs: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_query_capability_resp {}
+
+pub const VIRTIO_VIDEO_MEM_TYPE_GUEST_PAGES: virtio_video_mem_type = 0;
+pub const VIRTIO_VIDEO_MEM_TYPE_VIRTIO_OBJECT: virtio_video_mem_type = 1;
+pub type virtio_video_mem_type = u32;
+#[repr(C)]
+#[derive(Copy, Clone)]
+pub struct virtio_video_stream_create {
+    pub in_mem_type: Le32,
+    pub out_mem_type: Le32,
+    pub coded_format: Le32,
+    pub padding: [u8; 4usize],
+    pub tag: [u8; 64usize],
+}
+impl Default for virtio_video_stream_create {
+    fn default() -> Self {
+        unsafe { ::std::mem::zeroed() }
+    }
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_stream_create {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_stream_destroy {}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_stream_destroy {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_stream_drain {}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_stream_drain {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_mem_entry {
+    pub addr: Le64,
+    pub length: Le32,
+    pub padding: [u8; 4usize],
+}
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_object_entry {
+    pub uuid: [u8; 16usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_object_entry {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_resource_create {
+    pub queue_type: Le32,
+    pub resource_id: Le32,
+    pub planes_layout: Le32,
+    pub num_planes: Le32,
+    pub plane_offsets: [Le32; 8usize],
+    pub num_entries: [Le32; 8usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_resource_create {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_resource_queue {
+    pub queue_type: Le32,
+    pub resource_id: Le32,
+    pub timestamp: Le64,
+    pub num_data_sizes: Le32,
+    pub data_sizes: [Le32; 8usize],
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_resource_queue {}
+
+pub const VIRTIO_VIDEO_BUFFER_FLAG_ERR: virtio_video_buffer_flag = 1;
+pub const VIRTIO_VIDEO_BUFFER_FLAG_EOS: virtio_video_buffer_flag = 2;
+pub const VIRTIO_VIDEO_BUFFER_FLAG_IFRAME: virtio_video_buffer_flag = 4;
+pub const VIRTIO_VIDEO_BUFFER_FLAG_PFRAME: virtio_video_buffer_flag = 8;
+pub const VIRTIO_VIDEO_BUFFER_FLAG_BFRAME: virtio_video_buffer_flag = 16;
+pub type virtio_video_buffer_flag = u32;
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_resource_queue_resp {
+    pub hdr: virtio_video_cmd_hdr,
+    pub timestamp: Le64,
+    pub flags: Le32,
+    pub size: Le32,
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_resource_queue_resp {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_resource_destroy_all {
+    pub queue_type: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_resource_destroy_all {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_queue_clear {
+    pub queue_type: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_queue_clear {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_plane_format {
+    pub plane_size: Le32,
+    pub stride: Le32,
+}
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_crop {
+    pub left: Le32,
+    pub top: Le32,
+    pub width: Le32,
+    pub height: Le32,
+}
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_params {
+    pub queue_type: Le32,
+    pub format: Le32,
+    pub frame_width: Le32,
+    pub frame_height: Le32,
+    pub min_buffers: Le32,
+    pub max_buffers: Le32,
+    pub crop: virtio_video_crop,
+    pub frame_rate: Le32,
+    pub num_planes: Le32,
+    pub plane_formats: [virtio_video_plane_format; 8usize],
+}
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_get_params {
+    pub queue_type: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_get_params {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_get_params_resp {
+    pub hdr: virtio_video_cmd_hdr,
+    pub params: virtio_video_params,
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_get_params_resp {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_set_params {
+    pub params: virtio_video_params,
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_set_params {}
+
+pub const VIRTIO_VIDEO_CONTROL_BITRATE: virtio_video_control_type = 1;
+pub const VIRTIO_VIDEO_CONTROL_PROFILE: virtio_video_control_type = 2;
+pub const VIRTIO_VIDEO_CONTROL_LEVEL: virtio_video_control_type = 3;
+pub type virtio_video_control_type = u32;
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_query_control_profile {
+    pub format: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_query_control_profile {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_query_control_level {
+    pub format: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_query_control_level {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_query_control {
+    pub control: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_query_control {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_query_control_resp_profile {
+    pub num: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_query_control_resp_profile {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_query_control_resp_level {
+    pub num: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_query_control_resp_level {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_query_control_resp {
+    pub hdr: virtio_video_cmd_hdr,
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_query_control_resp {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_get_control {
+    pub control: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_get_control {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_control_val_bitrate {
+    pub bitrate: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_control_val_bitrate {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_control_val_profile {
+    pub profile: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_control_val_profile {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_control_val_level {
+    pub level: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_control_val_level {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_get_control_resp {
+    pub hdr: virtio_video_cmd_hdr,
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_get_control_resp {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_set_control {
+    pub control: Le32,
+    pub padding: [u8; 4usize],
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_set_control {}
+
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_set_control_resp {
+    pub hdr: virtio_video_cmd_hdr,
+}
+pub const VIRTIO_VIDEO_EVENT_ERROR: virtio_video_event_type = 256;
+pub const VIRTIO_VIDEO_EVENT_DECODER_RESOLUTION_CHANGED: virtio_video_event_type = 512;
+pub type virtio_video_event_type = u32;
+#[repr(C)]
+#[derive(Debug, Default, Copy, Clone)]
+pub struct virtio_video_event {
+    pub event_type: Le32,
+    pub stream_id: Le32,
+}
+// Safe because auto-generated structs have no implicit padding.
+unsafe impl DataInit for virtio_video_event {}
diff --git a/devices/src/virtio/video/response.rs b/devices/src/virtio/video/response.rs
new file mode 100644
index 0000000..e4b4385
--- /dev/null
+++ b/devices/src/virtio/video/response.rs
@@ -0,0 +1,95 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Data structures for commands of virtio video devices.
+
+use std::io;
+
+use data_model::{Le32, Le64};
+
+use crate::virtio::video::command::QueueType;
+use crate::virtio::video::control::*;
+use crate::virtio::video::format::*;
+use crate::virtio::video::params::Params;
+use crate::virtio::video::protocol::*;
+use crate::virtio::Writer;
+
+pub trait Response {
+    /// Writes an object to virtqueue.
+    fn write(&self, w: &mut Writer) -> Result<(), io::Error>;
+}
+
+/// A response to a `VideoCmd`. These correspond to `VIRTIO_VIDEO_RESP_*`.
+#[allow(dead_code)] // TODO(keiichiw): Remove this attribute
+#[derive(Debug)]
+pub enum CmdResponse {
+    NoData,
+    QueryCapability(Vec<FormatDesc>),
+    ResourceQueue {
+        timestamp: u64,
+        flags: u32,
+        size: u32,
+    },
+    GetParams {
+        queue_type: QueueType,
+        params: Params,
+    },
+    QueryControl(QueryCtrlResponse),
+    GetControl(CtrlVal),
+}
+
+impl Response for CmdResponse {
+    /// Writes a response to virtqueue.
+    fn write(&self, w: &mut Writer) -> Result<(), io::Error> {
+        use CmdResponse::*;
+
+        let type_ = Le32::from(match self {
+            NoData => VIRTIO_VIDEO_RESP_OK_NODATA,
+            QueryCapability(_) => VIRTIO_VIDEO_RESP_OK_QUERY_CAPABILITY,
+            ResourceQueue { .. } => VIRTIO_VIDEO_RESP_OK_RESOURCE_QUEUE,
+            GetParams { .. } => VIRTIO_VIDEO_RESP_OK_GET_PARAMS,
+            QueryControl(_) => VIRTIO_VIDEO_RESP_OK_QUERY_CONTROL,
+            GetControl(_) => VIRTIO_VIDEO_RESP_OK_GET_CONTROL,
+        });
+
+        let hdr = virtio_video_cmd_hdr {
+            type_,
+            ..Default::default()
+        };
+
+        match self {
+            NoData => w.write_obj(hdr),
+            QueryCapability(descs) => {
+                w.write_obj(virtio_video_query_capability_resp {
+                    hdr,
+                    num_descs: Le32::from(descs.len() as u32),
+                    ..Default::default()
+                })?;
+                descs.iter().map(|d| d.write(w)).collect()
+            }
+            ResourceQueue {
+                timestamp,
+                flags,
+                size,
+            } => w.write_obj(virtio_video_resource_queue_resp {
+                hdr,
+                timestamp: Le64::from(*timestamp),
+                flags: Le32::from(*flags),
+                size: Le32::from(*size),
+            }),
+            GetParams { queue_type, params } => {
+                let params = params.to_virtio_video_params(*queue_type);
+                w.write_obj(virtio_video_get_params_resp { hdr, params })
+            }
+            QueryControl(r) => {
+                w.write_obj(virtio_video_query_control_resp { hdr })?;
+                r.write(w)
+            }
+            GetControl(val) => {
+                w.write_obj(virtio_video_get_control_resp { hdr })?;
+                val.write(w)
+            }
+        }
+    }
+}
diff --git a/devices/src/virtio/video/worker.rs b/devices/src/virtio/video/worker.rs
new file mode 100644
index 0000000..195854f
--- /dev/null
+++ b/devices/src/virtio/video/worker.rs
@@ -0,0 +1,362 @@
+// Copyright 2020 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+//! Worker that runs in a virtio-video thread.
+
+use std::collections::{BTreeMap, VecDeque};
+
+use sys_util::{error, EventFd, GuestMemory, PollContext};
+
+use crate::virtio::queue::{DescriptorChain, Queue};
+use crate::virtio::resource_bridge::ResourceRequestSocket;
+use crate::virtio::video::command::{QueueType, VideoCmd};
+use crate::virtio::video::device::{
+    AsyncCmdTag, Device, Token, VideoCmdResponseType, VideoEvtResponseType,
+};
+use crate::virtio::video::error::{VideoError, VideoResult};
+use crate::virtio::video::event::{self, EvtType, VideoEvt};
+use crate::virtio::video::protocol;
+use crate::virtio::video::response::{self, CmdResponse, Response};
+use crate::virtio::video::{Error, Result};
+use crate::virtio::{Interrupt, Reader, Writer};
+
+pub struct Worker {
+    pub interrupt: Interrupt,
+    pub mem: GuestMemory,
+    pub cmd_evt: EventFd,
+    pub event_evt: EventFd,
+    pub kill_evt: EventFd,
+    pub resource_bridge: ResourceRequestSocket,
+}
+
+/// BTreeMap which stores descriptor chains in which asynchronous responses will be written.
+type DescPool<'a> = BTreeMap<AsyncCmdTag, DescriptorChain<'a>>;
+/// Pair of a descriptor chain and a response to be written.
+type WritableResp<'a> = (DescriptorChain<'a>, VideoResult<response::CmdResponse>);
+
+/// Invalidates all pending asynchronous commands in a given `DescPool` value and returns an updated
+/// `DescPool` value and a list of `WritableResp` to be sent to the guest.
+fn cancel_pending_requests<'a>(
+    s_id: u32,
+    desc_pool: DescPool<'a>,
+) -> (DescPool<'a>, Vec<WritableResp<'a>>) {
+    let mut new_desc_pool: DescPool<'a> = Default::default();
+    let mut resps = vec![];
+
+    for (key, value) in desc_pool.into_iter() {
+        match key {
+            AsyncCmdTag::Queue { stream_id, .. } if stream_id == s_id => {
+                resps.push((
+                    value,
+                    Ok(CmdResponse::ResourceQueue {
+                        timestamp: 0,
+                        flags: protocol::VIRTIO_VIDEO_BUFFER_FLAG_ERR,
+                        size: 0,
+                    }),
+                ));
+            }
+            AsyncCmdTag::Drain { stream_id } | AsyncCmdTag::Clear { stream_id, .. }
+                if stream_id == s_id =>
+            {
+                // TODO(b/1518105): Use more appropriate error code if a new protocol supports one.
+                resps.push((value, Err(VideoError::InvalidOperation)));
+            }
+            AsyncCmdTag::Queue { .. } | AsyncCmdTag::Drain { .. } | AsyncCmdTag::Clear { .. } => {
+                // Keep commands for other streams.
+                new_desc_pool.insert(key, value);
+            }
+        }
+    }
+
+    (new_desc_pool, resps)
+}
+
+impl Worker {
+    /// Writes responses into the command queue.
+    fn write_responses<'a>(
+        &self,
+        cmd_queue: &mut Queue,
+        responses: &mut VecDeque<WritableResp>,
+    ) -> Result<()> {
+        let mut needs_interrupt_commandq = false;
+        // Write responses into command virtqueue.
+        while let Some((desc, resp)) = responses.pop_front() {
+            let desc_index = desc.index;
+            let mut writer = Writer::new(&self.mem, desc).map_err(Error::InvalidDescriptorChain)?;
+            match resp {
+                Ok(r) => {
+                    if let Err(e) = r.write(&mut writer) {
+                        error!("failed to write an OK response for {:?}: {}", r, e);
+                    }
+                }
+                Err(err) => {
+                    if let Err(e) = err.write(&mut writer) {
+                        error!("failed to write an Error response for {:?}: {}", err, e);
+                    }
+                }
+            }
+
+            cmd_queue.add_used(&self.mem, desc_index, writer.bytes_written() as u32);
+            needs_interrupt_commandq = true;
+        }
+        if needs_interrupt_commandq {
+            self.interrupt.signal_used_queue(cmd_queue.vector);
+        }
+        Ok(())
+    }
+
+    /// Writes a `VideoEvt` into the event queue.
+    fn write_event(&self, event_queue: &mut Queue, event: &mut event::VideoEvt) -> Result<()> {
+        let desc = event_queue
+            .peek(&self.mem)
+            .ok_or_else(|| Error::DescriptorNotAvailable)?;
+        event_queue.pop_peeked(&self.mem);
+
+        let desc_index = desc.index;
+        let mut writer = Writer::new(&self.mem, desc).map_err(Error::InvalidDescriptorChain)?;
+        event
+            .write(&mut writer)
+            .map_err(|error| Error::WriteEventFailure {
+                event: event.clone(),
+                error,
+            })?;
+        event_queue.add_used(&self.mem, desc_index, writer.bytes_written() as u32);
+        self.interrupt.signal_used_queue(event_queue.vector);
+        Ok(())
+    }
+
+    /// Handles a `DescriptorChain` value sent via the command queue and returns an updated
+    /// `DescPool` and `VecDeque` of `WritableResp` to be sent to the guest.
+    fn handle_command_desc<'a, T: Device>(
+        &'a self,
+        device: &mut T,
+        poll_ctx: &PollContext<Token>,
+        mut desc_pool: DescPool<'a>,
+        desc: DescriptorChain<'a>,
+    ) -> Result<(DescPool<'a>, VecDeque<WritableResp<'a>>)> {
+        let mut resps: VecDeque<WritableResp> = Default::default();
+        let mut reader =
+            Reader::new(&self.mem, desc.clone()).map_err(Error::InvalidDescriptorChain)?;
+
+        let cmd = VideoCmd::from_reader(&mut reader).map_err(Error::ReadFailure)?;
+
+        // If a destruction command comes, cancel pending requests.
+        match cmd {
+            VideoCmd::ResourceDestroyAll { stream_id } | VideoCmd::StreamDestroy { stream_id } => {
+                let (next_desc_pool, rs) = cancel_pending_requests(stream_id, desc_pool);
+                desc_pool = next_desc_pool;
+                resps.append(&mut Into::<VecDeque<_>>::into(rs));
+            }
+            _ => (),
+        };
+
+        // Process the command by the device.
+        let resp = device.process_cmd(cmd, &poll_ctx, &self.resource_bridge);
+
+        match resp {
+            Ok(VideoCmdResponseType::Sync(r)) => {
+                resps.push_back((desc.clone(), Ok(r)));
+            }
+            Ok(VideoCmdResponseType::Async(tag)) => {
+                // If the command expects an asynchronous response,
+                // store `desc` to use it after the back-end device notifies the
+                // completion.
+                desc_pool.insert(tag, desc);
+            }
+            Err(e) => {
+                resps.push_back((desc.clone(), Err(e)));
+            }
+        }
+
+        Ok((desc_pool, resps))
+    }
+
+    /// Handles the command queue returns an updated `DescPool`.
+    fn handle_command_queue<'a, T: Device>(
+        &'a self,
+        cmd_queue: &mut Queue,
+        device: &mut T,
+        poll_ctx: &PollContext<Token>,
+        mut desc_pool: DescPool<'a>,
+    ) -> Result<DescPool<'a>> {
+        let _ = self.cmd_evt.read();
+
+        while let Some(desc) = cmd_queue.pop(&self.mem) {
+            let (next_desc_pool, mut resps) =
+                self.handle_command_desc(device, poll_ctx, desc_pool, desc)?;
+            desc_pool = next_desc_pool;
+            self.write_responses(cmd_queue, &mut resps)?;
+        }
+        Ok(desc_pool)
+    }
+
+    /// Handles a `VideoEvtResponseType` value and returns an updated `DescPool` and `VecDeque` of
+    /// `WritableResp` to be sent to the guest.
+    fn handle_event_resp<'a, T: Device>(
+        &'a self,
+        event_queue: &mut Queue,
+        device: &mut T,
+        mut desc_pool: DescPool<'a>,
+        resp: VideoEvtResponseType,
+    ) -> Result<(DescPool<'a>, VecDeque<WritableResp>)> {
+        let mut responses: VecDeque<WritableResp> = Default::default();
+        match resp {
+            VideoEvtResponseType::AsyncCmd {
+                tag: AsyncCmdTag::Drain { stream_id },
+                resp,
+            } => {
+                let tag = AsyncCmdTag::Drain { stream_id };
+                let drain_desc = desc_pool
+                    .remove(&tag)
+                    .ok_or_else(|| Error::UnexpectedResponse(tag))?;
+
+                // When `Drain` request is completed, returns an empty output resource
+                // with EOS flag first.
+                let resource_id = device
+                    .take_resource_id_to_notify_eos(stream_id)
+                    .ok_or_else(|| Error::NoEOSBuffer { stream_id })?;
+
+                let q_desc = desc_pool
+                    .remove(&AsyncCmdTag::Queue {
+                        stream_id,
+                        queue_type: QueueType::Output,
+                        resource_id,
+                    })
+                    .ok_or_else(|| Error::InvalidEOSResource {
+                        stream_id,
+                        resource_id,
+                    })?;
+
+                responses.push_back((
+                    q_desc,
+                    Ok(CmdResponse::ResourceQueue {
+                        timestamp: 0,
+                        flags: protocol::VIRTIO_VIDEO_BUFFER_FLAG_EOS,
+                        size: 0,
+                    }),
+                ));
+
+                // Then, responds the Drain request.
+                responses.push_back((drain_desc, resp));
+            }
+            VideoEvtResponseType::AsyncCmd {
+                tag:
+                    AsyncCmdTag::Clear {
+                        queue_type,
+                        stream_id,
+                    },
+                resp,
+            } => {
+                let tag = AsyncCmdTag::Clear {
+                    queue_type,
+                    stream_id,
+                };
+                let desc = desc_pool
+                    .remove(&tag)
+                    .ok_or_else(|| Error::UnexpectedResponse(tag))?;
+
+                // When `Clear` request is completed, invalidate all pending requests.
+                let (next_desc_pool, resps) = cancel_pending_requests(stream_id, desc_pool);
+                desc_pool = next_desc_pool;
+                responses.append(&mut Into::<VecDeque<_>>::into(resps));
+
+                // Then, responds the `Clear` request.
+                responses.push_back((desc, resp));
+            }
+            VideoEvtResponseType::AsyncCmd { tag, resp } => {
+                let desc = desc_pool
+                    .remove(&tag)
+                    .ok_or_else(|| Error::UnexpectedResponse(tag))?;
+                responses.push_back((desc, resp));
+            }
+            VideoEvtResponseType::Event(mut evt) => {
+                self.write_event(event_queue, &mut evt)?;
+            }
+        };
+        Ok((desc_pool, responses))
+    }
+
+    /// Handles an event notified via an event FD and returns an updated `DescPool`.
+    fn handle_event_fd<'a, T: Device>(
+        &'a self,
+        cmd_queue: &mut Queue,
+        event_queue: &mut Queue,
+        device: &mut T,
+        desc_pool: DescPool<'a>,
+        stream_id: u32,
+    ) -> Result<DescPool<'a>> {
+        let resp = device.process_event_fd(stream_id);
+        match resp {
+            Some(r) => match self.handle_event_resp(event_queue, device, desc_pool, r) {
+                Ok((updated_desc_pool, mut resps)) => {
+                    self.write_responses(cmd_queue, &mut resps)?;
+                    Ok(updated_desc_pool)
+                }
+                Err(e) => {
+                    // Ignore result of write_event for a fatal error.
+                    let _ = self.write_event(
+                        event_queue,
+                        &mut VideoEvt {
+                            typ: EvtType::Error,
+                            stream_id,
+                        },
+                    );
+                    Err(e)
+                }
+            },
+            None => Ok(desc_pool),
+        }
+    }
+
+    pub fn run<T: Device>(
+        &mut self,
+        mut cmd_queue: Queue,
+        mut event_queue: Queue,
+        mut device: T,
+    ) -> Result<()> {
+        let poll_ctx: PollContext<Token> = PollContext::build_with(&[
+            (&self.cmd_evt, Token::CmdQueue),
+            (&self.event_evt, Token::EventQueue),
+            (&self.kill_evt, Token::Kill),
+            (self.interrupt.get_resample_evt(), Token::InterruptResample),
+        ])
+        .map_err(Error::PollContextCreationFailed)?;
+
+        // Stores descriptors in which responses for asynchronous commands will be written.
+        let mut desc_pool: DescPool<'_> = Default::default();
+
+        loop {
+            let poll_events = poll_ctx.wait().map_err(Error::PollError)?;
+
+            for poll_event in poll_events.iter_readable() {
+                match poll_event.token() {
+                    Token::CmdQueue => {
+                        desc_pool = self.handle_command_queue(
+                            &mut cmd_queue,
+                            &mut device,
+                            &poll_ctx,
+                            desc_pool,
+                        )?;
+                    }
+                    Token::EventQueue => {
+                        let _ = self.event_evt.read();
+                    }
+                    Token::EventFd { id } => {
+                        desc_pool = self.handle_event_fd(
+                            &mut cmd_queue,
+                            &mut event_queue,
+                            &mut device,
+                            desc_pool,
+                            id,
+                        )?;
+                    }
+                    Token::InterruptResample => {
+                        self.interrupt.interrupt_resample();
+                    }
+                    Token::Kill => return Ok(()),
+                }
+            }
+        }
+    }
+}
diff --git a/seccomp/x86_64/video_device.policy b/seccomp/x86_64/video_device.policy
new file mode 100644
index 0000000..fdc5935
--- /dev/null
+++ b/seccomp/x86_64/video_device.policy
@@ -0,0 +1,24 @@
+# Copyright 2020 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+@include /usr/share/policy/crosvm/common_device.policy
+
+# Syscalls specific to video devices.
+clock_getres: 1
+connect: 1
+fcntl: arg1 == F_GETFL || arg1 == F_SETFL || arg1 == F_DUPFD_CLOEXEC || arg1 == F_GETFD || arg1 == F_SETFD
+getdents: 1
+getegid: 1
+geteuid: 1
+getgid: 1
+getresgid: 1
+getresuid: 1
+getsockname: 1
+getuid: 1
+# ioctl: arg1 == DRM_IOCTL_*
+ioctl: arg1 & 0x6400
+openat: 1
+setpriority: 1
+socket: arg0 == AF_UNIX
+stat: 1
diff --git a/src/crosvm.rs b/src/crosvm.rs
index 49a08c0..33ed236 100644
--- a/src/crosvm.rs
+++ b/src/crosvm.rs
@@ -202,6 +202,8 @@ pub struct Config {
     pub virtio_input_evdevs: Vec<PathBuf>,
     pub split_irqchip: bool,
     pub vfio: Vec<PathBuf>,
+    pub video_dec: bool,
+    pub video_enc: bool,
 }
 
 impl Default for Config {
@@ -250,6 +252,8 @@ impl Default for Config {
             virtio_input_evdevs: Vec::new(),
             split_irqchip: false,
             vfio: Vec::new(),
+            video_dec: false,
+            video_enc: false,
         }
     }
 }
diff --git a/src/linux.rs b/src/linux.rs
index e480a4c..e95f372 100644
--- a/src/linux.rs
+++ b/src/linux.rs
@@ -792,6 +792,70 @@ fn create_wayland_device(
     })
 }
 
+#[cfg(any(feature = "video-decoder", feature = "video-encoder"))]
+fn create_video_device(
+    cfg: &Config,
+    typ: devices::virtio::VideoDeviceType,
+    resource_bridge: virtio::resource_bridge::ResourceRequestSocket,
+) -> DeviceResult {
+    let jail = match simple_jail(&cfg, "video_device")? {
+        Some(mut jail) => {
+            match typ {
+                devices::virtio::VideoDeviceType::Decoder => {
+                    add_crosvm_user_to_jail(&mut jail, "video-decoder")?
+                }
+                devices::virtio::VideoDeviceType::Encoder => {
+                    add_crosvm_user_to_jail(&mut jail, "video-encoder")?
+                }
+            };
+
+            // Create a tmpfs in the device's root directory so that we can bind mount files.
+            jail.mount_with_data(
+                Path::new("none"),
+                Path::new("/"),
+                "tmpfs",
+                (libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC) as usize,
+                "size=67108864",
+            )?;
+
+            // Render node for libvda.
+            let dev_dri_path = Path::new("/dev/dri/renderD128");
+            jail.mount_bind(dev_dri_path, dev_dri_path, false)?;
+
+            // Device nodes required by libchrome which establishes Mojo connection in libvda.
+            let dev_urandom_path = Path::new("/dev/urandom");
+            jail.mount_bind(dev_urandom_path, dev_urandom_path, false)?;
+            let system_bus_socket_path = Path::new("/run/dbus/system_bus_socket");
+            jail.mount_bind(system_bus_socket_path, system_bus_socket_path, true)?;
+
+            Some(jail)
+        }
+        None => None,
+    };
+
+    Ok(VirtioDeviceStub {
+        dev: Box::new(devices::virtio::VideoDevice::new(
+            typ,
+            Some(resource_bridge),
+        )),
+        jail,
+    })
+}
+
+#[cfg(any(feature = "video-decoder", feature = "video-encoder"))]
+fn register_video_device(
+    devs: &mut Vec<VirtioDeviceStub>,
+    resource_bridges: &mut Vec<virtio::resource_bridge::ResourceResponseSocket>,
+    cfg: &Config,
+    typ: devices::virtio::VideoDeviceType,
+) -> std::result::Result<(), Error> {
+    let (video_socket, gpu_socket) =
+        virtio::resource_bridge::pair().map_err(Error::CreateSocket)?;
+    resource_bridges.push(gpu_socket);
+    devs.push(create_video_device(cfg, typ, video_socket)?);
+    Ok(())
+}
+
 fn create_vhost_vsock_device(cfg: &Config, cid: u64, mem: &GuestMemory) -> DeviceResult {
     let dev = virtio::vhost::Vsock::new(cid, mem).map_err(Error::VhostVsockDeviceNew)?;
 
@@ -1088,6 +1152,30 @@ fn create_virtio_devices(
         )?);
     }
 
+    #[cfg(feature = "video-decoder")]
+    {
+        if cfg.video_dec {
+            register_video_device(
+                &mut devs,
+                &mut resource_bridges,
+                cfg,
+                devices::virtio::VideoDeviceType::Decoder,
+            )?;
+        }
+    }
+
+    #[cfg(feature = "video-encoder")]
+    {
+        if cfg.video_enc {
+            register_video_device(
+                &mut devs,
+                &mut resource_bridges,
+                cfg,
+                devices::virtio::VideoDeviceType::Encoder,
+            )?;
+        }
+    }
+
     #[cfg(feature = "gpu")]
     {
         if let Some(gpu_parameters) = &cfg.gpu_parameters {
diff --git a/src/main.rs b/src/main.rs
index 557c630..61fcd48 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1226,6 +1226,12 @@ fn set_argument(cfg: &mut Config, name: &str, value: Option<&str>) -> argument::
 
             cfg.vfio.push(vfio_path);
         }
+        "video-decoder" => {
+            cfg.video_dec = true;
+        }
+        "video-encoder" => {
+            cfg.video_enc = true;
+        }
 
         "help" => return Err(argument::Error::PrintHelp),
         _ => unreachable!(),
@@ -1396,6 +1402,10 @@ writeback=BOOL - Indicates whether the VM can use writeback caching (default: fa
           Argument::flag("split-irqchip", "(EXPERIMENTAL) enable split-irqchip support"),
           Argument::value("bios", "PATH", "Path to BIOS/firmware ROM"),
           Argument::value("vfio", "PATH", "Path to sysfs of pass through or mdev device"),
+          #[cfg(feature = "video-decoder")]
+          Argument::flag("video-decoder", "(EXPERIMENTAL) enable virtio-video decoder device"),
+          #[cfg(feature = "video-encoder")]
+          Argument::flag("video-encoder", "(EXPERIMENTAL) enable virtio-video encoder device"),
           Argument::short_flag('h', "help", "Print help message.")];
 
     let mut cfg = Config::default();