]> git.sesse.net Git - nageru/commitdiff
Serialize state to disk between runs, using SQLite.
authorSteinar H. Gunderson <sgunderson@bigfoot.com>
Sun, 30 Sep 2018 21:40:46 +0000 (23:40 +0200)
committerSteinar H. Gunderson <sgunderson@bigfoot.com>
Sun, 30 Sep 2018 21:40:51 +0000 (23:40 +0200)
.gitignore
Makefile
clip_list.cpp
clip_list.h
db.cpp [new file with mode: 0644]
db.h [new file with mode: 0644]
mainwindow.cpp
mainwindow.h
state.proto [new file with mode: 0644]

index 5596e8e1186812e23eb5e4d0a3f635e43a612dcc..ea6747510932e8e99d6d65e5e8b34aefa36866bd 100644 (file)
@@ -16,3 +16,4 @@ frames/*.jpeg
 eval
 flow
 vis
+futatabi.db
index a15bb6bee1a8360337e666d4cdfc34f7e19cddaa..9e234caf4dac38e2046f47d75e63ed9d87c1053d 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,6 @@
 CXX=g++
-PKG_MODULES := Qt5Core Qt5Gui Qt5Widgets Qt5OpenGLExtensions Qt5OpenGL Qt5PrintSupport libjpeg movit libmicrohttpd
+PROTOC=protoc
+PKG_MODULES := Qt5Core Qt5Gui Qt5Widgets Qt5OpenGLExtensions Qt5OpenGL Qt5PrintSupport libjpeg movit libmicrohttpd protobuf sqlite3
 CXXFLAGS ?= -O2 -g -Wall  # Will be overridden by environment.
 CXXFLAGS += -fPIC $(shell pkg-config --cflags $(PKG_MODULES)) -DMOVIT_SHADER_DIR=\"$(shell pkg-config --variable=shaderdir movit)\" -pthread
 
@@ -14,7 +15,8 @@ OBJS += $(OBJS_WITH_MOC:.o=.moc.o)
 OBJS += flow.o gpu_timers.o
 
 OBJS += ffmpeg_raii.o main.o player.o httpd.o mux.o metacube2.o video_stream.o context.o chroma_subsampler.o
-OBJS += vaapi_jpeg_decoder.o memcpy_interleaved.o
+OBJS += vaapi_jpeg_decoder.o memcpy_interleaved.o db.o
+OBJS += state.pb.o
 
 %.o: %.cpp
        $(CXX) -MMD -MP $(CPPFLAGS) $(CXXFLAGS) -o $@ -c $<
@@ -33,6 +35,10 @@ all: futatabi flow vis eval
 
 mainwindow.o: ui_mainwindow.h
 
+clip_list.h: state.pb.h
+
+db.h: state.pb.h
+
 futatabi: $(OBJS) $(CEF_LIBS)
        $(CXX) -o $@ $^ $(LDFLAGS) $(LDLIBS)
 flow: flow.o flow_main.o gpu_timers.o
@@ -46,4 +52,4 @@ DEPS=$(OBJS:.o=.d)
 -include $(DEPS)
 
 clean:
-       $(RM) $(OBJS) $(DEPS) flow_main.o gpu_timers.o futatabi $(OBJS_WITH_MOC:.o=.moc.cpp)
+       $(RM) $(OBJS) $(DEPS) flow_main.o gpu_timers.o futatabi $(OBJS_WITH_MOC:.o=.moc.cpp) *.pb.cc *.pb.h
index 69a6334cd473984d1b730998464c61cc5af422cc..2f805756003264470a37b25682e12db736c1ac3f 100644 (file)
@@ -325,6 +325,7 @@ void ClipList::add_clip(const Clip &clip)
        beginInsertRows(QModelIndex(), clips.size(), clips.size());
        clips.push_back(clip);
        endInsertRows();
+       emit any_content_changed();
 }
 
 void PlayList::add_clip(const Clip &clip)
@@ -332,6 +333,7 @@ void PlayList::add_clip(const Clip &clip)
        beginInsertRows(QModelIndex(), clips.size(), clips.size());
        clips.push_back(clip);
        endInsertRows();
+       emit any_content_changed();
 }
 
 void PlayList::duplicate_clips(size_t first, size_t last)
@@ -339,6 +341,7 @@ void PlayList::duplicate_clips(size_t first, size_t last)
        beginInsertRows(QModelIndex(), first, last);
        clips.insert(clips.begin() + first, clips.begin() + first, clips.begin() + last + 1);
        endInsertRows();
+       emit any_content_changed();
 }
 
 void PlayList::erase_clips(size_t first, size_t last)
@@ -346,6 +349,7 @@ void PlayList::erase_clips(size_t first, size_t last)
        beginRemoveRows(QModelIndex(), first, last);
        clips.erase(clips.begin() + first, clips.begin() + last + 1);
        endRemoveRows();
+       emit any_content_changed();
 }
 
 void PlayList::move_clips(size_t first, size_t last, int delta)
@@ -360,16 +364,19 @@ void PlayList::move_clips(size_t first, size_t last, int delta)
                rotate(clips.rbegin() + last - 1, clips.rbegin() + last, clips.rbegin() + first + 1);
        }
        endMoveRows();
+       emit any_content_changed();
 }
 
 void ClipList::emit_data_changed(size_t row)
 {
        emit dataChanged(index(row, 0), index(row, int(Column::NUM_COLUMNS)));
+       emit any_content_changed();
 }
 
 void PlayList::emit_data_changed(size_t row)
 {
        emit dataChanged(index(row, 0), index(row, int(Column::NUM_COLUMNS)));
+       emit any_content_changed();
 }
 
 void PlayList::set_currently_playing(int index, double progress)
@@ -390,3 +397,61 @@ void PlayList::set_currently_playing(int index, double progress)
                emit dataChanged(this->index(index, column), this->index(index, column));
        }
 }
+
+namespace {
+
+Clip deserialize_clip(const ClipProto &clip_proto)
+{
+       Clip clip;
+       clip.pts_in = clip_proto.pts_in();
+       clip.pts_out = clip_proto.pts_out();
+       for (int camera_idx = 0; camera_idx < min(clip_proto.description_size(), NUM_CAMERAS); ++camera_idx) {
+               clip.descriptions[camera_idx] = clip_proto.description(camera_idx);
+       }
+       clip.stream_idx = clip_proto.stream_idx();
+       return clip;
+}
+
+void serialize_clip(const Clip &clip, ClipProto *clip_proto)
+{
+       clip_proto->set_pts_in(clip.pts_in);
+       clip_proto->set_pts_out(clip.pts_out);
+       for (int camera_idx = 0; camera_idx < NUM_CAMERAS; ++camera_idx) {
+               *clip_proto->add_description() = clip.descriptions[camera_idx];
+       }
+       clip_proto->set_stream_idx(clip.stream_idx);
+}
+
+}  // namespace
+
+ClipList::ClipList(const ClipListProto &serialized)
+{
+       for (const ClipProto &clip_proto : serialized.clip()) {
+               clips.push_back(deserialize_clip(clip_proto));
+       }
+}
+
+ClipListProto ClipList::serialize() const
+{
+       ClipListProto ret;
+       for (const Clip &clip : clips) {
+               serialize_clip(clip, ret.add_clip());
+       }
+       return ret;
+}
+
+PlayList::PlayList(const ClipListProto &serialized)
+{
+       for (const ClipProto &clip_proto : serialized.clip()) {
+               clips.push_back(deserialize_clip(clip_proto));
+       }
+}
+
+ClipListProto PlayList::serialize() const
+{
+       ClipListProto ret;
+       for (const Clip &clip : clips) {
+               serialize_clip(clip, ret.add_clip());
+       }
+       return ret;
+}
index 2d37eab4fda0959e23e004f78cdd4120523f6188..31ddb6cb297965512c5c7a65d26bfc52b485ca9b 100644 (file)
@@ -9,6 +9,7 @@
 #include <string>
 
 #include "defs.h"
+#include "state.pb.h"
 
 struct Clip {
        int64_t pts_in = -1, pts_out = -1;  // pts_in is inclusive, pts_out is exclusive.
@@ -44,7 +45,7 @@ class ClipList : public QAbstractTableModel, public DataChangedReceiver {
        Q_OBJECT
 
 public:
-       ClipList() {}
+       explicit ClipList(const ClipListProto &serialized);
 
        enum class Column {
                IN,
@@ -74,8 +75,13 @@ public:
        ClipProxy mutable_back() { return mutable_clip(size() - 1); }
        const Clip *back() const { return clip(size() - 1); }
 
+       ClipListProto serialize() const;
+
        void emit_data_changed(size_t row) override;
 
+signals:
+       void any_content_changed();
+
 private:
        std::vector<Clip> clips;
 };
@@ -84,7 +90,7 @@ class PlayList : public QAbstractTableModel, public DataChangedReceiver {
        Q_OBJECT
 
 public:
-       PlayList() {}
+       explicit PlayList(const ClipListProto &serialized);
 
        enum class Column {
                PLAYING,
@@ -123,8 +129,13 @@ public:
        void set_currently_playing(int index, double progress);  // -1 = none.
        int get_currently_playing() const { return currently_playing_index; }
 
+       ClipListProto serialize() const;
+
        void emit_data_changed(size_t row) override;
 
+signals:
+       void any_content_changed();
+
 private:
        std::vector<Clip> clips;
        int currently_playing_index = -1;
diff --git a/db.cpp b/db.cpp
new file mode 100644 (file)
index 0000000..c999a1d
--- /dev/null
+++ b/db.cpp
@@ -0,0 +1,95 @@
+#include "db.h"
+
+#include <string>
+
+using namespace std;
+
+DB::DB(const char *filename)
+{
+       int ret = sqlite3_open(filename, &db);
+       if (ret != SQLITE_OK) {
+               fprintf(stderr, "%s: %s\n", filename, sqlite3_errmsg(db));
+               exit(1);
+       }
+
+       sqlite3_exec(db, R"(
+               CREATE TABLE IF NOT EXISTS state (state BLOB);
+       )", nullptr, nullptr, nullptr);  // Ignore errors.
+}
+
+StateProto DB::get_state()
+{
+       StateProto state;
+
+       sqlite3_stmt *stmt;
+       int ret = sqlite3_prepare(db, "SELECT state FROM state", -1, &stmt, 0);
+       if (ret != SQLITE_OK) {
+               fprintf(stderr, "SELECT prepare: %s\n", sqlite3_errmsg(db));
+               exit(1);
+       }
+
+       ret = sqlite3_step(stmt);
+       if (ret == SQLITE_ROW) {
+               bool ok = state.ParseFromArray(sqlite3_column_blob(stmt, 0), sqlite3_column_bytes(stmt, 0));
+               if (!ok) {
+                       fprintf(stderr, "State in database is corrupted!\n");
+                       exit(1);
+               }
+       } else if (ret != SQLITE_DONE) {
+               fprintf(stderr, "SELECT step: %s\n", sqlite3_errmsg(db));
+               exit(1);
+       }
+
+       ret = sqlite3_finalize(stmt);
+       if (ret != SQLITE_OK) {
+               fprintf(stderr, "SELECT finalize: %s\n", sqlite3_errmsg(db));
+               exit(1);
+       }
+
+       return state;
+}
+
+void DB::store_state(const StateProto &state)
+{
+       string serialized;
+       state.SerializeToString(&serialized);
+
+       int ret = sqlite3_exec(db, "BEGIN", nullptr, nullptr, nullptr);
+       if (ret != SQLITE_OK) {
+               fprintf(stderr, "BEGIN: %s\n", sqlite3_errmsg(db));
+               exit(1);
+       }
+
+       ret = sqlite3_exec(db, "DELETE FROM state", nullptr, nullptr, nullptr);
+       if (ret != SQLITE_OK) {
+               fprintf(stderr, "DELETE: %s\n", sqlite3_errmsg(db));
+               exit(1);
+       }
+
+       sqlite3_stmt *stmt;
+       ret = sqlite3_prepare(db, "INSERT INTO state VALUES (?)", -1, &stmt, 0);
+       if (ret != SQLITE_OK) {
+               fprintf(stderr, "INSERT prepare: %s\n", sqlite3_errmsg(db));
+               exit(1);
+       }
+
+       sqlite3_bind_blob(stmt, 1, serialized.data(), serialized.size(), SQLITE_STATIC);
+
+       ret = sqlite3_step(stmt);
+       if (ret == SQLITE_ROW) {
+               fprintf(stderr, "INSERT step: %s\n", sqlite3_errmsg(db));
+               exit(1);
+       }
+
+       ret = sqlite3_finalize(stmt);
+       if (ret != SQLITE_OK) {
+               fprintf(stderr, "INSERT finalize: %s\n", sqlite3_errmsg(db));
+               exit(1);
+       }
+
+       ret = sqlite3_exec(db, "COMMIT", nullptr, nullptr, nullptr);
+       if (ret != SQLITE_OK) {
+               fprintf(stderr, "COMMIT: %s\n", sqlite3_errmsg(db));
+               exit(1);
+       }
+}
diff --git a/db.h b/db.h
new file mode 100644 (file)
index 0000000..826ab3f
--- /dev/null
+++ b/db.h
@@ -0,0 +1,20 @@
+#ifndef DB_H
+#define DB_H 1
+
+#include "state.pb.h"
+#include <sqlite3.h>
+
+class DB {
+public:
+       explicit DB(const char *filename);
+       DB(const DB &) = delete;
+
+       StateProto get_state();
+       void store_state(const StateProto &state);
+
+private:
+       StateProto state;
+       sqlite3 *db;
+};
+
+#endif  // !defined(DB_H)
index 0d9e80a4752a5d13cb882c7cdddf3a4e321ede55..cfc976965bfb4988efb82bec69cb4978c94666ba 100644 (file)
@@ -12,6 +12,9 @@
 #include <QMouseEvent>
 #include <QWheelEvent>
 #include <QShortcut>
+#include <QTimer>
+
+#include <sqlite3.h>
 
 using namespace std;
 
@@ -24,16 +27,21 @@ extern mutex frame_mu;
 extern vector<int64_t> frames[MAX_STREAMS];
 
 MainWindow::MainWindow()
-       : ui(new Ui::MainWindow)
+       : ui(new Ui::MainWindow),
+         db("futatabi.db")
 {
        global_mainwindow = this;
        ui->setupUi(this);
 
-       cliplist_clips = new ClipList();
+       StateProto state = db.get_state();
+
+       cliplist_clips = new ClipList(state.clip_list());
        ui->clip_list->setModel(cliplist_clips);
+       connect(cliplist_clips, &ClipList::any_content_changed, this, &MainWindow::content_changed);
 
-       playlist_clips = new PlayList();
+       playlist_clips = new PlayList(state.play_list());
        ui->playlist->setModel(playlist_clips);
+       connect(playlist_clips, &PlayList::any_content_changed, this, &MainWindow::content_changed);
 
        // For scrubbing in the pts columns.
        ui->clip_list->viewport()->installEventFilter(this);
@@ -113,6 +121,10 @@ MainWindow::MainWindow()
                        live_player_clip_progress(played_this_clip, total_length);
                });
        });
+
+       defer_timeout = new QTimer(this);
+       defer_timeout->setSingleShot(true);
+       connect(defer_timeout, &QTimer::timeout, this, &MainWindow::defer_timer_expired);
 }
 
 void MainWindow::cue_in_clicked()
@@ -252,6 +264,37 @@ void MainWindow::playlist_move(int delta)
        playlist_selection_changed();
 }
 
+void MainWindow::defer_timer_expired()
+{
+       state_changed(deferred_state);
+}
+
+void MainWindow::content_changed()
+{
+       if (defer_timeout->isActive() &&
+           (!currently_deferring_model_changes || deferred_change_id != current_change_id)) {
+               // There's some deferred event waiting, but this event is unrelated.
+               // So it's time to short-circuit that timer and do the work it wanted to do.
+               defer_timeout->stop();
+               state_changed(deferred_state);
+       }
+       StateProto state;
+       *state.mutable_clip_list() = cliplist_clips->serialize();
+       *state.mutable_play_list() = playlist_clips->serialize();
+       if (currently_deferring_model_changes) {
+               deferred_change_id = current_change_id;
+               deferred_state = std::move(state);
+               defer_timeout->start(200);
+               return;
+       }
+       state_changed(state);
+}
+
+void MainWindow::state_changed(const StateProto &state)
+{
+       db.store_state(state);
+}
+
 void MainWindow::play_clicked()
 {
        if (playlist_clips->empty()) return;
@@ -405,13 +448,15 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event)
                        }
 
                        int64_t pts = scrub_pts_origin + adjusted_offset * scrub_sensitivity;
-
+                       currently_deferring_model_changes = true;
                        if (scrub_type == SCRUBBING_CLIP_LIST) {
                                ClipProxy clip = cliplist_clips->mutable_clip(scrub_row);
                                if (scrub_column == int(ClipList::Column::IN)) {
+                                       current_change_id = "cliplist:in:" + to_string(scrub_row);
                                        set_pts_in(pts, current_pts, clip);
                                        preview_single_frame(pts, stream_idx, FIRST_AT_OR_AFTER);
                                } else {
+                                       current_change_id = "cliplist:out" + to_string(scrub_row);
                                        pts = std::max(pts, clip->pts_in);
                                        pts = std::min(pts, current_pts);
                                        clip->pts_out = pts;
@@ -420,15 +465,18 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event)
                        } else {
                                ClipProxy clip = playlist_clips->mutable_clip(scrub_row);
                                if (scrub_column == int(PlayList::Column::IN)) {
+                                       current_change_id = "playlist:in:" + to_string(scrub_row);
                                        set_pts_in(pts, current_pts, clip);
                                        preview_single_frame(pts, clip->stream_idx, FIRST_AT_OR_AFTER);
                                } else {
+                                       current_change_id = "playlist:out:" + to_string(scrub_row);
                                        pts = std::max(pts, clip->pts_in);
                                        pts = std::min(pts, current_pts);
                                        clip->pts_out = pts;
                                        preview_single_frame(pts, clip->stream_idx, LAST_BEFORE);
                                }
                        }
+                       currently_deferring_model_changes = false;
 
                        return true;  // Don't use this mouse movement for selecting things.
                }
@@ -456,41 +504,49 @@ bool MainWindow::eventFilter(QObject *watched, QEvent *event)
                int row = destination->rowAt(wheel->y());
                if (column == -1 || row == -1) return false;
 
-               ClipProxy clip = (watched == ui->clip_list->viewport()) ?
-                       cliplist_clips->mutable_clip(row) : playlist_clips->mutable_clip(row);
-               if (watched == ui->playlist->viewport()) {
-                       stream_idx = clip->stream_idx;
-               }
+               currently_deferring_model_changes = true;
+               {
+                       current_change_id = (watched == ui->clip_list->viewport()) ? "cliplist:" : "playlist:";
+                       ClipProxy clip = (watched == ui->clip_list->viewport()) ?
+                               cliplist_clips->mutable_clip(row) : playlist_clips->mutable_clip(row);
+                       if (watched == ui->playlist->viewport()) {
+                               stream_idx = clip->stream_idx;
+                       }
 
-               if (column != camera_column) {
-                       last_mousewheel_camera_row = -1;
-               }
-               if (column == in_column) {
-                       int64_t pts = clip->pts_in + wheel->angleDelta().y() * wheel_sensitivity;
-                       set_pts_in(pts, current_pts, clip);
-                       preview_single_frame(pts, stream_idx, FIRST_AT_OR_AFTER);
-               } else if (column == out_column) {
-                       int64_t pts = clip->pts_out + wheel->angleDelta().y() * wheel_sensitivity;
-                       pts = std::max(pts, clip->pts_in);
-                       pts = std::min(pts, current_pts);
-                       clip->pts_out = pts;
-                       preview_single_frame(pts, stream_idx, LAST_BEFORE);
-               } else if (column == camera_column) {
-                       int angle_degrees = wheel->angleDelta().y();
-                       if (last_mousewheel_camera_row == row) {
-                               angle_degrees += leftover_angle_degrees;
+                       if (column != camera_column) {
+                               last_mousewheel_camera_row = -1;
                        }
+                       if (column == in_column) {
+                               current_change_id += "in:" + to_string(row);
+                               int64_t pts = clip->pts_in + wheel->angleDelta().y() * wheel_sensitivity;
+                               set_pts_in(pts, current_pts, clip);
+                               preview_single_frame(pts, stream_idx, FIRST_AT_OR_AFTER);
+                       } else if (column == out_column) {
+                               current_change_id += "out:" + to_string(row);
+                               int64_t pts = clip->pts_out + wheel->angleDelta().y() * wheel_sensitivity;
+                               pts = std::max(pts, clip->pts_in);
+                               pts = std::min(pts, current_pts);
+                               clip->pts_out = pts;
+                               preview_single_frame(pts, stream_idx, LAST_BEFORE);
+                       } else if (column == camera_column) {
+                               current_change_id += "camera:" + to_string(row);
+                               int angle_degrees = wheel->angleDelta().y();
+                               if (last_mousewheel_camera_row == row) {
+                                       angle_degrees += leftover_angle_degrees;
+                               }
 
-                       int stream_idx = clip->stream_idx + angle_degrees / camera_degrees_per_pixel;
-                       stream_idx = std::max(stream_idx, 0);
-                       stream_idx = std::min(stream_idx, NUM_CAMERAS - 1);
-                       clip->stream_idx = stream_idx;
+                               int stream_idx = clip->stream_idx + angle_degrees / camera_degrees_per_pixel;
+                               stream_idx = std::max(stream_idx, 0);
+                               stream_idx = std::min(stream_idx, NUM_CAMERAS - 1);
+                               clip->stream_idx = stream_idx;
 
-                       last_mousewheel_camera_row = row;
-                       leftover_angle_degrees = angle_degrees % camera_degrees_per_pixel;
+                               last_mousewheel_camera_row = row;
+                               leftover_angle_degrees = angle_degrees % camera_degrees_per_pixel;
 
-                       // Don't update the live view, that's rarely what the operator wants.
+                               // Don't update the live view, that's rarely what the operator wants.
+                       }
                }
+               currently_deferring_model_changes = false;
        } else if (event->type() == QEvent::MouseButtonRelease) {
                scrubbing = false;
        }
index 24eea609e03bbdf24049018b03cb9193b15cfa38..3621d32ce65f8467ad99d3941f78a7d2e3c16672 100644 (file)
@@ -5,7 +5,8 @@
 #include <sys/types.h>
 #include <QMainWindow>
 
-#include "clip_list.h"
+#include "db.h"
+#include "state.pb.h"
 
 namespace Ui {
 class MainWindow;
@@ -25,6 +26,7 @@ public:
 
 private:
        Player *preview_player, *live_player;
+       DB db;
 
        // State when doing a scrub operation on a timestamp with the mouse.
        bool scrubbing = false;
@@ -40,6 +42,22 @@ private:
        int last_mousewheel_camera_row = -1;
        int leftover_angle_degrees = 0;
 
+       // Some operations, notably scrubbing and scrolling, happen in so large increments
+       // that we want to group them instead of saving to disk every single time.
+       // If they happen (ie., we get a callback from the model that it's changed) while
+       // currently_deferring_model_changes, we fire off this timer. If it manages to elapse
+       // before some other event happens, we count the event. (If the other event is of the
+       // same kind, we just fire off the timer anew instead of taking any action.)
+       QTimer *defer_timeout;
+       std::string deferred_change_id;
+       StateProto deferred_state;
+
+       // Before a change that should be deferred (see above), currently_deferring_model_changes
+       // must be set to true, and current_change_id must be given contents describing what's
+       // changed to avoid accidental grouping.
+       bool currently_deferring_model_changes = false;
+       std::string current_change_id;
+
        void cue_in_clicked();
        void cue_out_clicked();
        void queue_clicked();
@@ -52,6 +70,10 @@ private:
        void playlist_remove();
        void playlist_move(int delta);
 
+       void defer_timer_expired();
+       void content_changed();  // In clip_list or play_list.
+       void state_changed(const StateProto &state);  // Called post-filtering.
+
        enum Rounding { FIRST_AT_OR_AFTER, LAST_BEFORE };
        void preview_single_frame(int64_t pts, unsigned stream_idx, Rounding rounding);
 
diff --git a/state.proto b/state.proto
new file mode 100644 (file)
index 0000000..7fe78dc
--- /dev/null
@@ -0,0 +1,18 @@
+syntax = "proto3";
+
+// Corresponds to struct Clip.
+message ClipProto {
+       int64 pts_in = 1;
+       int64 pts_out = 2;
+       repeated string description = 3;
+       int64 stream_idx = 4;
+}
+
+message ClipListProto {
+       repeated ClipProto clip = 1;
+}
+
+message StateProto {
+       ClipListProto clip_list = 1;
+       ClipListProto play_list = 2;
+}