diff --git a/bob/bio/face/config/database/replaymobile.py b/bob/bio/face/config/database/replaymobile.py
new file mode 100644
index 0000000000000000000000000000000000000000..e8b4d9014d8e603188ded37636552909b4fde079
--- /dev/null
+++ b/bob/bio/face/config/database/replaymobile.py
@@ -0,0 +1,30 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+# Yannick Dayer <yannick.dayer@idiap.ch>
+
+"""Replay-mobile CSV database interface configuration
+
+The Replay-Mobile Database for face spoofing consists of video clips of
+photo and video attack attempts under different lighting conditions.
+
+The vulnerability analysis pipeline uses single frames extracted from the
+videos to be accepted by most face recognition systems.
+
+Feed this file (defined as resource: ``replaymobile-img``) to ``bob bio pipelines`` as
+configuration:
+
+    $ bob bio pipelines vanilla-biometrics -v --write-metadata-scores replaymobile-img inception-resnetv2-msceleb
+
+    $ bob bio pipelines vanilla-biometrics -v --write-metadata-scores my_config/protocol.py replaymobile-img inception-resnetv2-msceleb
+"""
+
+from bob.bio.face.database.replaymobile import ReplayMobileBioDatabase
+
+default_protocol = "grandtest"
+
+if "protocol" not in locals():
+    protocol = default_protocol
+
+database = ReplayMobileBioDatabase(
+    protocol=protocol,
+)
diff --git a/bob/bio/face/config/database/replaymobile_licit.py b/bob/bio/face/config/database/replaymobile_licit.py
deleted file mode 100644
index 1ed5205094b2b68525726bd499d837ab7fa0b36e..0000000000000000000000000000000000000000
--- a/bob/bio/face/config/database/replaymobile_licit.py
+++ /dev/null
@@ -1,18 +0,0 @@
-#!/usr/bin/env python
-
-from bob.bio.face.database import ReplayMobileBioDatabase
-from bob.bio.base.pipelines.vanilla_biometrics import DatabaseConnector
-from bob.extension import rc
-
-
-replay_mobile_directory = rc["bob.db.replay_mobile.directory"]
-
-database = DatabaseConnector(
-    ReplayMobileBioDatabase(
-        original_directory=replay_mobile_directory,
-        original_extension=".mov",
-        protocol="grandtest-licit",
-    ),
-    annotation_type="bounding-box",
-    fixed_positions=None,
-)
diff --git a/bob/bio/face/config/database/replaymobile_spoof.py b/bob/bio/face/config/database/replaymobile_spoof.py
deleted file mode 100644
index 3f9d75b70e04d8e42685b43681c50b15118b299c..0000000000000000000000000000000000000000
--- a/bob/bio/face/config/database/replaymobile_spoof.py
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/usr/bin/env python
-
-from bob.bio.face.database import ReplayMobileBioDatabase
-from bob.bio.base.pipelines.vanilla_biometrics import DatabaseConnector
-from bob.extension import rc
-
-
-replay_mobile_directory = rc["bob.db.replay_mobile.directory"]
-
-database = DatabaseConnector(
-    ReplayMobileBioDatabase(
-        original_directory=replay_mobile_directory,
-        original_extension=".mov",
-        protocol="grandtest-spoof",
-    ),
-    annotation_type="bounding-box",
-    fixed_positions=None,
-    # Only compare with spoofs from the same target identity
-    allow_scoring_with_all_biometric_references=False,
-)
diff --git a/bob/bio/face/database/replaymobile.py b/bob/bio/face/database/replaymobile.py
index 4d9fa06e805f6395174fb0a2627421882d2f22af..2ebac35662bca566733437057f1ef2e82d040a23 100644
--- a/bob/bio/face/database/replaymobile.py
+++ b/bob/bio/face/database/replaymobile.py
@@ -1,132 +1,306 @@
 #!/usr/bin/env python
-# vim: set fileencoding=utf-8 :
+# Yannick Dayer <yannick.dayer@idiap.ch>
 
-"""   The Replay-Mobile Database for face spoofing implementation of
-bob.bio.base.database.BioDatabase interface."""
-
-from .database import FaceBioFile
-from bob.bio.base.database import BioDatabase
+from bob.bio.base.database import CSVDataset, CSVToSampleLoaderBiometrics
+from bob.pipelines.sample_loaders import AnnotationsLoader
+from bob.pipelines.sample import DelayedSample
+from bob.db.base.annotations import read_annotation_file
+from bob.extension.download import get_file
+from bob.io.video import reader
 from bob.extension import rc
 
+from sklearn.pipeline import make_pipeline
+import functools
+import os.path
+import logging
+import numpy
 
-class ReplayMobileBioFile(FaceBioFile):
-    """FaceBioFile implementation of the Replay Mobile Database"""
+logger = logging.getLogger(__name__)
 
-    def __init__(self, f):
-        super(ReplayMobileBioFile, self).__init__(client_id=f.client_id, path=f.path, file_id=f.id)
-        self._f = f
 
-    def load(self, directory=None, extension=None):
-        if extension in (None, '.mov'):
-            return self._f.load(directory, extension)
-        else:
-            return super(ReplayMobileBioFile, self).load(directory, extension)
+def load_frame_from_file_replaymobile(file_name, frame, should_flip):
+    """Loads a single frame from a video file for replay-mobile.
+
+    This function uses bob's video reader utility that does not load the full
+    video in memory to just access one frame.
+
+    Parameters
+    ----------
+
+    file_name: str
+        The video file to load the frames from
 
-    @property
-    def annotations(self):
-        return self._f.annotations
+    frame: None or list of int
+        The index of the frame to load.
 
+    capturing device: str
+        'mobile' devices' frames will be flipped vertically.
+        Other devices' frames will not be flipped.
 
-class ReplayMobileBioDatabase(BioDatabase):
+    Returns
+    -------
+
+    images: 3D numpy array
+        The frame of the video in bob format (channel, height, width)
     """
-    ReplayMobile database implementation of :py:class:`bob.bio.base.database.BioDatabase` interface.
-    It is an extension of an SQL-based database interface, which directly talks to ReplayMobile database, for
-    verification experiments (good to use in bob.bio.base framework).
+    logger.debug(f"Reading frame {frame} from '{file_name}'")
+    video_reader = reader(file_name)
+    image = video_reader[frame]
+    # Image captured by the 'mobile' device are flipped vertically.
+    # (Images were captured horizontally and bob.io.video does not read the
+    #   metadata correctly, whether it was on the right or left side)
+    if should_flip:
+        image = numpy.flip(image, 2)
+    # Convert to bob format (channel, height, width)
+    image = numpy.transpose(image, (0, 2, 1))
+    return image
+
+
+class ReplayMobileCSVFrameSampleLoader(CSVToSampleLoaderBiometrics):
+    """A loader transformer returning a specific frame of a video file.
+
+    This is specifically tailored for replay-mobile. It uses a specific loader
+    that processes the `should_flip` metadata to correctly orient the frames.
     """
 
-    def __init__(self, max_number_of_frames=None,
-                 annotation_directory=None,
-                 annotation_extension='.json',
-                 annotation_type='json',
-                 original_directory=rc['bob.db.replaymobile.directory'],
-                 original_extension='.mov',
-                 name='replay-mobile',
-                 **kwargs):
-        from bob.db.replaymobile.verificationprotocol import Database as LowLevelDatabase
-        self._db = LowLevelDatabase(
-            max_number_of_frames,
-            original_directory=original_directory,
-            original_extension=original_extension,
-            annotation_directory=annotation_directory,
-            annotation_extension=annotation_extension,
-            annotation_type=annotation_type,
+    def __init__(
+        self,
+        dataset_original_directory="",
+        extension="",
+        reference_id_equal_subject_id=True,
+    ):
+        super().__init__(
+            data_loader=None,
+            extension=extension,
+            dataset_original_directory=dataset_original_directory,
         )
+        self.reference_id_equal_subject_id = reference_id_equal_subject_id
+        self.references_list = []
 
-        # call base class constructors to open a session to the database
-        super(ReplayMobileBioDatabase, self).__init__(
-            name=name,
-            original_directory=original_directory,
-            original_extension=original_extension,
-            annotation_directory=annotation_directory,
-            annotation_extension=annotation_extension,
-            annotation_type=annotation_type,
-            **kwargs)
-        self._kwargs['max_number_of_frames'] = max_number_of_frames
+    def convert_row_to_sample(self, row, header):
+        """Creates a sample given a row of the CSV protocol definition."""
+        fields = dict([[str(h).lower(), r] for h, r in zip(header, row)])
 
-    @property
-    def original_directory(self):
-        return self._db.original_directory
+        if self.reference_id_equal_subject_id:
+            fields["subject_id"] = fields["reference_id"]
+        else:
+            if "subject_id" not in fields:
+                raise ValueError(f"`subject_id` not available in {header}")
+        if "should_flip" not in fields:
+            raise ValueError(f"`should_flip` not available in {header}")
+        if "purpose" not in fields:
+            raise ValueError(f"`purpose` not available in {header}")
 
-    @original_directory.setter
-    def original_directory(self, value):
-        self._db.original_directory = value
+        kwargs = {k: fields[k] for k in fields.keys() - {"id", "should_flip"}}
 
-    @property
-    def original_extension(self):
-        return self._db.original_extension
+        # Retrieve the references list
+        if (
+            fields["purpose"].lower() == "enroll"
+            and fields["reference_id"] not in self.references_list
+        ):
+            self.references_list.append(fields["reference_id"])
+        # Set the references list in the probes for vanilla-biometrics
+        if fields["purpose"].lower() != "enroll":
+            if fields["attack_type"]:
+                # Attacks are only compare to their target (no `spoof_neg`)
+                kwargs["references"] = [fields["reference_id"]]
+            else:
+                kwargs["references"] = self.references_list
+        # One row leads to multiple samples (different frames)
+        return DelayedSample(
+            functools.partial(
+                load_frame_from_file_replaymobile,
+                file_name=os.path.join(
+                    self.dataset_original_directory, fields["path"] + self.extension
+                ),
+                frame=int(fields["frame"]),
+                should_flip=fields["should_flip"] == "TRUE",
+            ),
+            key=fields["id"],
+            **kwargs,
+        )
 
-    @original_extension.setter
-    def original_extension(self, value):
-        self._db.original_extension = value
 
-    @property
-    def annotation_directory(self):
-        return self._db.annotation_directory
+def read_frame_annotation_file_replaymobile(file_name, frame, annotations_type="json"):
+    """Returns the bounding-box for one frame of a video file of replay-mobile.
 
-    @annotation_directory.setter
-    def annotation_directory(self, value):
-        self._db.annotation_directory = value
+    Given an annnotation file location and a frame number, returns the bounding
+    box coordinates corresponding to the frame.
 
-    @property
-    def annotation_extension(self):
-        return self._db.annotation_extension
+    The replay-mobile annotation files are composed of 4 columns and N rows for
+    N frames of the video:
 
-    @annotation_extension.setter
-    def annotation_extension(self, value):
-        self._db.annotation_extension = value
+    120 230 40 40
+    125 230 40 40
+    ...
+    <x> <y> <w> <h>
 
-    @property
-    def annotation_type(self):
-        return self._db.annotation_type
+    Parameters
+    ----------
 
-    @annotation_type.setter
-    def annotation_type(self, value):
-        self._db.annotation_type = value
+    file_name: str
+        The annotation file name (relative to annotations_path).
 
-    def protocol_names(self):
-        return self._db.protocols()
+    frame: int
+        The video frame index.
+    """
+    logger.debug(f"Reading annotation file '{file_name}', frame {frame}.")
 
-    def groups(self):
-        return self._db.groups()
+    video_annotations = read_annotation_file(
+        file_name, annotation_type=annotations_type
+    )
+    # read_annotation_file returns an ordered dict with str keys as frame number
+    frame_annotations = video_annotations[str(frame)]
+    if frame_annotations is None:
+        logger.warning(
+            f"Annotation for file '{file_name}' at frame {frame} was 'null'."
+        )
+    return frame_annotations
 
-    def annotations(self, myfile):
-        """Will return the bounding box annotation of nth frame of the video."""
-        return myfile.annotations
 
-    def model_ids_with_protocol(self, groups=None, protocol=None, **kwargs):
-        return self._db.model_ids_with_protocol(groups, protocol, **kwargs)
+class FrameBoundingBoxAnnotationLoader(AnnotationsLoader):
+    """A transformer that adds bounding-box to a sample from annotations files.
 
-    def objects(self, groups=None, protocol=None, purposes=None, model_ids=None, **kwargs):
-        return [ReplayMobileBioFile(f) for f in self._db.objects(groups, protocol, purposes, model_ids, **kwargs)]
+    Parameters
+    ----------
 
-    def arrange_by_client(self, files):
-        client_files = {}
-        for file in files:
-            if str(file.client_id) not in client_files:
-                client_files[str(file.client_id)] = []
-            client_files[str(file.client_id)].append(file)
+    annotation_directory: str or None
+    """
 
-        files_by_clients = []
-        for client in sorted(client_files.keys()):
-            files_by_clients.append(client_files[client])
-        return files_by_clients
+    def __init__(
+        self, annotation_directory=None, annotation_extension=".json", **kwargs
+    ):
+        super().__init__(
+            annotation_directory=annotation_directory,
+            annotation_extension=annotation_extension,
+            **kwargs,
+        )
+
+    def transform(self, X):
+        """Adds the bounding-box annotations to a series of samples."""
+        if self.annotation_directory is None:
+            return None
+
+        annotated_samples = []
+        for x in X:
+            # Adds the annotations as delayed_attributes, loading them when needed
+            annotated_samples.append(
+                DelayedSample(
+                    x._load,
+                    parent=x,
+                    delayed_attributes=dict(
+                        annotations=functools.partial(
+                            read_frame_annotation_file_replaymobile,
+                            file_name=f"{self.annotation_directory}:{x.path}{self.annotation_extension}",
+                            frame=int(x.frame),
+                            annotations_type=self.annotation_type,
+                        )
+                    ),
+                )
+            )
+
+        return annotated_samples
+
+
+class ReplayMobileBioDatabase(CSVDataset):
+    """Database interface that loads a csv definition for replay-mobile
+
+    Looks for the protocol definition files (structure of CSV files). If not
+    present, downloads them.
+    Then sets the data and annotation paths from __init__ parameters or from
+    the configuration (``bob config`` command).
+
+    Parameters
+    ----------
+
+    protocol_name: str
+        The protocol to use. Must be a sub-folder of ``protocol_definition_path``
+
+    protocol_definition_path: str or None
+        Specifies a path where to fetch the database definition from.
+        (See :py:func:`bob.extension.download.get_file`)
+        If None: Downloads the file in the path from ``bob_data_folder`` config.
+        If None and the config does not exist: Downloads the file in ``~/bob_data``.
+
+    data_path: str or None
+        Overrides the config-defined data location.
+        If None: uses the ``bob.db.replaymobile.directory`` config.
+        If None and the config does not exist, set as cwd.
+
+    annotation_path: str or None
+        Specifies a path where the annotation files are located.
+        If None: Downloads the files to the path poited by the
+        ``bob.db.replaymobile.annotation_directory`` config.
+        If None and the config does not exist: Downloads the file in ``~/bob_data``.
+    """
+
+    def __init__(
+        self,
+        protocol="grandtest",
+        protocol_definition_path=None,
+        data_path=None,
+        data_extension=".mov",
+        annotations_path=None,
+        annotations_extension=".json",
+        **kwargs,
+    ):
+        if protocol_definition_path is None:
+            # Downloading database description files if it is not specified
+            proto_def_name = "bio-face-replaymobile-img-3a584a97.tar.gz"
+            proto_def_urls = [
+                f"https://www.idiap.ch/software/bob/data/bob/bob.bio.face/{proto_def_name}",
+                f"http://www.idiap.ch/software/bob/data/bob/bob.bio.face/{proto_def_name}",
+            ]
+            protocol_definition_path = get_file(
+                filename=proto_def_name,
+                urls=proto_def_urls,
+                cache_subdir="datasets",
+                file_hash="3a584a97",
+            )
+
+        if data_path is None:
+            data_path = rc.get("bob.db.replaymobile.directory", "")
+        if data_path == "":
+            logger.warning(
+                "Raw data path is not configured. Please set "
+                "'bob.db.replaymobile.directory' with the 'bob config set' command. "
+                "Will now attempt with current directory."
+            )
+
+        if annotations_path is None:
+            annot_name = "annotations-replaymobile-mtcnn-9cd6e452.tar.xz"
+            annot_urls = [
+                f"https://www.idiap.ch/software/bob/data/bob/bob.pad.face/{annot_name}",
+                f"http://www.idiap.ch/software/bob/data/bob/bob.pad.face/{annot_name}",
+            ]
+            annotations_path = get_file(
+                filename=annot_name,
+                urls=annot_urls,
+                cache_subdir="annotations",
+                file_hash="9cd6e452",
+            )
+
+        logger.info(
+            f"Database: Will read CSV protocol definitions in '{protocol_definition_path}'."
+        )
+        logger.info(f"Database: Will read raw data files in '{data_path}'.")
+        logger.info(f"Database: Will read annotation files in '{annotations_path}'.")
+        super().__init__(
+            name="replaymobile-img",
+            protocol=protocol,
+            dataset_protocol_path=protocol_definition_path,
+            csv_to_sample_loader=make_pipeline(
+                ReplayMobileCSVFrameSampleLoader(
+                    dataset_original_directory=data_path,
+                    extension=data_extension,
+                ),
+                FrameBoundingBoxAnnotationLoader(
+                    annotation_directory=annotations_path,
+                    annotation_extension=annotations_extension,
+                ),
+            ),
+            fetch_probes=False,
+            **kwargs,
+        )
+        self.annotation_type = "eyes-center"
+        self.fixed_positions = None
diff --git a/bob/bio/face/test/test_databases.py b/bob/bio/face/test/test_databases.py
index 7c68ed4efb23c203369b18cf9d7894561d9b62a2..ff6213f4852f99e04c68179826a80896a2ea4ad3 100644
--- a/bob/bio/face/test/test_databases.py
+++ b/bob/bio/face/test/test_databases.py
@@ -26,6 +26,7 @@ from bob.bio.base.test.utils import db_available
 from bob.bio.base.test.test_database_implementations import check_database
 import bob.core
 from bob.extension.download import get_file
+from nose.plugins.skip import SkipTest
 
 logger = bob.core.log.setup("bob.bio.face")
 
@@ -271,46 +272,36 @@ def test_replay_spoof():
         )
 
 
-@db_available("replaymobile")
-def test_replaymobile_licit():
+def test_replaymobile():
     database = bob.bio.base.load_resource(
-        "replaymobile-img-licit", "database", preferred_package="bob.bio.face"
+        "replaymobile-img", "database", preferred_package="bob.bio.face"
     )
+    samples = database.all_samples(groups=("dev", "eval"))
+    assert len(samples) == 8300, len(samples)
+    sample = samples[0]
+    assert hasattr(sample, "annotations")
+    assert "reye" in sample.annotations
+    assert "leye" in sample.annotations
+    assert hasattr(sample, "path")
+    assert hasattr(sample, "frame")
+    assert len(database.references()) == 16
+    assert len(database.references(group="eval")) == 12
+    assert len(database.probes()) == 4160
+    assert len(database.probes(group="eval")) == 3020
     try:
-        check_database(database, groups=("dev", "eval"))
-    except IOError as e:
-        pytest.skip(
-            "The database could not be queried; probably the db.sql3 file is missing. Here is the error: '%s'"
-            % e
-        )
-    try:
-        _check_annotations(database, topleft=True, limit_files=20)
-    except IOError as e:
-        pytest.skip(
-            "The annotations could not be queried; probably the annotation files are missing. Here is the error: '%s'"
-            % e
-        )
-
-
-@db_available("replaymobile")
-def test_replaymobile_spoof():
-    database = bob.bio.base.load_resource(
-        "replaymobile-img-spoof", "database", preferred_package="bob.bio.face"
-    )
-    try:
-        check_database(database, groups=("dev", "eval"))
-    except IOError as e:
-        pytest.skip(
-            "The database could not be queried; probably the db.sql3 file is missing. Here is the error: '%s'"
-            % e
-        )
-    try:
-        _check_annotations(database, topleft=True, limit_files=20)
-    except IOError as e:
-        pytest.skip(
-            "The annotations could not be queried; probably the annotation files are missing. Here is the error: '%s'"
-            % e
-        )
+        assert sample.annotations == {
+            "bottomright": [785, 395],
+            "topleft": [475, 167],
+            "leye": [587, 336],
+            "reye": [588, 238],
+            "mouthleft": [705, 252],
+            "mouthright": [706, 326],
+            "nose": [643, 295],
+        }
+        assert sample.data.shape == (3, 1280, 720)
+        assert sample.data[0, 0, 0] == 87
+    except RuntimeError as e:
+        raise SkipTest(e)
 
 
 def test_ijbc():
diff --git a/conda/meta.yaml b/conda/meta.yaml
index 6250242cefc4bbd94d530964896e8a0b02cc7957..d9814e84589733f5c302db0c083a3576da669eba 100644
--- a/conda/meta.yaml
+++ b/conda/meta.yaml
@@ -26,6 +26,7 @@ requirements:
     - bob.core
     - bob.io.base
     - bob.io.image
+    - bob.io.video
     - bob.math
     - bob.sp
     - bob.ip.base
diff --git a/setup.py b/setup.py
index ded13e23b4f4326f28c48532700311235313affa..1a1578b55d5aec93e6798244708c82c741c0f017 100644
--- a/setup.py
+++ b/setup.py
@@ -105,10 +105,7 @@ setup(
             "mobio-all        = bob.bio.face.config.database.mobio_all:database",
             "multipie          = bob.bio.face.config.database.multipie:database",
             "multipie-pose     = bob.bio.face.config.database.multipie_pose:database",
-            "replay-img-licit  = bob.bio.face.config.database.replay:replay_licit",
-            "replay-img-spoof  = bob.bio.face.config.database.replay:replay_spoof",
-            "replaymobile-img-licit  = bob.bio.face.config.database.replaymobile:replaymobile_licit",
-            "replaymobile-img-spoof  = bob.bio.face.config.database.replaymobile:replaymobile_spoof",
+            "replaymobile-img  = bob.bio.face.config.database.replaymobile:database",
             "fargo  = bob.bio.face.config.database.fargo:database",
             "meds = bob.bio.face.config.database.meds:database",
             "morph = bob.bio.face.config.database.morph:database",
@@ -173,10 +170,7 @@ setup(
             "mobio-all        = bob.bio.face.config.database.mobio_all",
             "multipie          = bob.bio.face.config.database.multipie",
             "multipie-pose     = bob.bio.face.config.database.multipie_pose",
-            "replay-img-licit  = bob.bio.face.config.database.replay_licit",
-            "replay-img-spoof  = bob.bio.face.config.database.replay_spoof",
-            "replaymobile-img-licit  = bob.bio.face.config.database.replaymobile_licit",
-            "replaymobile-img-spoof  = bob.bio.face.config.database.replaymobile_spoof",
+            "replaymobile-img  = bob.bio.face.config.database.replaymobile",
             "fargo  = bob.bio.face.config.database.fargo",
             "meds = bob.bio.face.config.database.meds",
             "morph = bob.bio.face.config.database.morph",