main_wnd.cc (15667B)
1 /* 2 * Copyright 2012 The WebRTC Project Authors. All rights reserved. 3 * 4 * Use of this source code is governed by a BSD-style license 5 * that can be found in the LICENSE file in the root of the source 6 * tree. An additional intellectual property rights grant can be found 7 * in the file PATENTS. All contributing project authors may 8 * be found in the AUTHORS file in the root of the source tree. 9 */ 10 11 #include "examples/peerconnection/client/linux/main_wnd.h" 12 13 #include <cairo.h> 14 #include <gdk/gdk.h> 15 #include <gdk/gdkkeysyms.h> 16 #include <glib-object.h> 17 #include <glib.h> 18 #include <glibconfig.h> 19 #include <gobject/gclosure.h> 20 #include <gtk/gtk.h> 21 22 #include <cstddef> 23 #include <cstdio> 24 #include <cstdlib> 25 #include <cstring> 26 #include <map> 27 28 #include "api/media_stream_interface.h" 29 #include "api/scoped_refptr.h" 30 #include "api/video/i420_buffer.h" 31 #include "api/video/video_frame.h" 32 #include "api/video/video_frame_buffer.h" 33 #include "api/video/video_rotation.h" 34 #include "api/video/video_source_interface.h" 35 #include "examples/peerconnection/client/main_wnd.h" 36 #include "examples/peerconnection/client/peer_connection_client.h" 37 #include "rtc_base/checks.h" 38 #include "rtc_base/logging.h" 39 #include "third_party/libyuv/include/libyuv/convert_from.h" 40 41 namespace { 42 43 // 44 // Simple static functions that simply forward the callback to the 45 // GtkMainWnd instance. 46 // 47 48 gboolean OnDestroyedCallback(GtkWidget* widget, 49 GdkEvent* event, 50 gpointer data) { 51 reinterpret_cast<GtkMainWnd*>(data)->OnDestroyed(widget, event); 52 return FALSE; 53 } 54 55 void OnClickedCallback(GtkWidget* widget, gpointer data) { 56 reinterpret_cast<GtkMainWnd*>(data)->OnClicked(widget); 57 } 58 59 gboolean SimulateButtonClick(gpointer button) { 60 g_signal_emit_by_name(button, "clicked"); 61 return false; 62 } 63 64 gboolean OnKeyPressCallback(GtkWidget* widget, 65 GdkEventKey* key, 66 gpointer data) { 67 reinterpret_cast<GtkMainWnd*>(data)->OnKeyPress(widget, key); 68 return false; 69 } 70 71 void OnRowActivatedCallback(GtkTreeView* tree_view, 72 GtkTreePath* path, 73 GtkTreeViewColumn* column, 74 gpointer data) { 75 reinterpret_cast<GtkMainWnd*>(data)->OnRowActivated(tree_view, path, column); 76 } 77 78 gboolean SimulateLastRowActivated(gpointer data) { 79 GtkTreeView* tree_view = reinterpret_cast<GtkTreeView*>(data); 80 GtkTreeModel* model = gtk_tree_view_get_model(tree_view); 81 82 // "if iter is NULL, then the number of toplevel nodes is returned." 83 int rows = gtk_tree_model_iter_n_children(model, nullptr); 84 GtkTreePath* lastpath = gtk_tree_path_new_from_indices(rows - 1, -1); 85 86 // Select the last item in the list 87 GtkTreeSelection* selection = gtk_tree_view_get_selection(tree_view); 88 gtk_tree_selection_select_path(selection, lastpath); 89 90 // Our TreeView only has one column, so it is column 0. 91 GtkTreeViewColumn* column = gtk_tree_view_get_column(tree_view, 0); 92 93 gtk_tree_view_row_activated(tree_view, lastpath, column); 94 95 gtk_tree_path_free(lastpath); 96 return false; 97 } 98 99 // Creates a tree view, that we use to display the list of peers. 100 void InitializeList(GtkWidget* list) { 101 GtkCellRenderer* renderer = gtk_cell_renderer_text_new(); 102 GtkTreeViewColumn* column = gtk_tree_view_column_new_with_attributes( 103 "List Items", renderer, "text", 0, NULL); 104 gtk_tree_view_append_column(GTK_TREE_VIEW(list), column); 105 GtkListStore* store = gtk_list_store_new(2, G_TYPE_STRING, G_TYPE_INT); 106 gtk_tree_view_set_model(GTK_TREE_VIEW(list), GTK_TREE_MODEL(store)); 107 g_object_unref(store); 108 } 109 110 // Adds an entry to a tree view. 111 void AddToList(GtkWidget* list, const gchar* str, int value) { 112 GtkListStore* store = 113 GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(list))); 114 115 GtkTreeIter iter; 116 gtk_list_store_append(store, &iter); 117 gtk_list_store_set(store, &iter, 0, str, 1, value, -1); 118 } 119 120 struct UIThreadCallbackData { 121 explicit UIThreadCallbackData(MainWndCallback* cb, int id, void* d) 122 : callback(cb), msg_id(id), data(d) {} 123 MainWndCallback* callback; 124 int msg_id; 125 void* data; 126 }; 127 128 gboolean HandleUIThreadCallback(gpointer data) { 129 UIThreadCallbackData* cb_data = reinterpret_cast<UIThreadCallbackData*>(data); 130 cb_data->callback->UIThreadCallback(cb_data->msg_id, cb_data->data); 131 delete cb_data; 132 return false; 133 } 134 135 gboolean Redraw(gpointer data) { 136 GtkMainWnd* wnd = reinterpret_cast<GtkMainWnd*>(data); 137 wnd->OnRedraw(); 138 return false; 139 } 140 141 gboolean Draw(GtkWidget* widget, cairo_t* cr, gpointer data) { 142 GtkMainWnd* wnd = reinterpret_cast<GtkMainWnd*>(data); 143 wnd->Draw(widget, cr); 144 return false; 145 } 146 147 } // namespace 148 149 // 150 // GtkMainWnd implementation. 151 // 152 153 GtkMainWnd::GtkMainWnd(const char* server, 154 int port, 155 bool autoconnect, 156 bool autocall) 157 : window_(nullptr), 158 draw_area_(nullptr), 159 vbox_(nullptr), 160 server_edit_(nullptr), 161 port_edit_(nullptr), 162 peer_list_(nullptr), 163 callback_(nullptr), 164 server_(server), 165 autoconnect_(autoconnect), 166 autocall_(autocall) { 167 char buffer[10]; 168 snprintf(buffer, sizeof(buffer), "%i", port); 169 port_ = buffer; 170 } 171 172 GtkMainWnd::~GtkMainWnd() { 173 RTC_DCHECK(!IsWindow()); 174 } 175 176 void GtkMainWnd::RegisterObserver(MainWndCallback* callback) { 177 callback_ = callback; 178 } 179 180 bool GtkMainWnd::IsWindow() { 181 return window_ != nullptr && GTK_IS_WINDOW(window_); 182 } 183 184 void GtkMainWnd::MessageBox(const char* caption, 185 const char* text, 186 bool is_error) { 187 GtkWidget* dialog = gtk_message_dialog_new( 188 GTK_WINDOW(window_), GTK_DIALOG_DESTROY_WITH_PARENT, 189 is_error ? GTK_MESSAGE_ERROR : GTK_MESSAGE_INFO, GTK_BUTTONS_CLOSE, "%s", 190 text); 191 gtk_window_set_title(GTK_WINDOW(dialog), caption); 192 gtk_dialog_run(GTK_DIALOG(dialog)); 193 gtk_widget_destroy(dialog); 194 } 195 196 MainWindow::UI GtkMainWnd::current_ui() { 197 if (vbox_) 198 return CONNECT_TO_SERVER; 199 200 if (peer_list_) 201 return LIST_PEERS; 202 203 return STREAMING; 204 } 205 206 void GtkMainWnd::StartLocalRenderer(webrtc::VideoTrackInterface* local_video) { 207 local_renderer_.reset(new VideoRenderer(this, local_video)); 208 } 209 210 void GtkMainWnd::StopLocalRenderer() { 211 local_renderer_.reset(); 212 } 213 214 void GtkMainWnd::StartRemoteRenderer( 215 webrtc::VideoTrackInterface* remote_video) { 216 remote_renderer_.reset(new VideoRenderer(this, remote_video)); 217 } 218 219 void GtkMainWnd::StopRemoteRenderer() { 220 remote_renderer_.reset(); 221 } 222 223 void GtkMainWnd::QueueUIThreadCallback(int msg_id, void* data) { 224 g_idle_add(HandleUIThreadCallback, 225 new UIThreadCallbackData(callback_, msg_id, data)); 226 } 227 228 bool GtkMainWnd::Create() { 229 RTC_DCHECK(window_ == nullptr); 230 231 window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL); 232 if (window_) { 233 gtk_window_set_position(GTK_WINDOW(window_), GTK_WIN_POS_CENTER); 234 gtk_window_set_default_size(GTK_WINDOW(window_), 640, 480); 235 gtk_window_set_title(GTK_WINDOW(window_), "PeerConnection client"); 236 g_signal_connect(G_OBJECT(window_), "delete-event", 237 G_CALLBACK(&OnDestroyedCallback), this); 238 g_signal_connect(window_, "key-press-event", G_CALLBACK(OnKeyPressCallback), 239 this); 240 241 SwitchToConnectUI(); 242 } 243 244 return window_ != nullptr; 245 } 246 247 bool GtkMainWnd::Destroy() { 248 if (!IsWindow()) 249 return false; 250 251 gtk_widget_destroy(window_); 252 window_ = nullptr; 253 254 return true; 255 } 256 257 void GtkMainWnd::SwitchToConnectUI() { 258 RTC_LOG(LS_INFO) << __FUNCTION__; 259 260 RTC_DCHECK(IsWindow()); 261 RTC_DCHECK(vbox_ == nullptr); 262 263 gtk_container_set_border_width(GTK_CONTAINER(window_), 10); 264 265 if (peer_list_) { 266 gtk_widget_destroy(peer_list_); 267 peer_list_ = nullptr; 268 } 269 270 vbox_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 5); 271 GtkWidget* valign = gtk_alignment_new(0, 1, 0, 0); 272 gtk_container_add(GTK_CONTAINER(vbox_), valign); 273 gtk_container_add(GTK_CONTAINER(window_), vbox_); 274 275 GtkWidget* hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 5); 276 277 GtkWidget* label = gtk_label_new("Server"); 278 gtk_container_add(GTK_CONTAINER(hbox), label); 279 280 server_edit_ = gtk_entry_new(); 281 gtk_entry_set_text(GTK_ENTRY(server_edit_), server_.c_str()); 282 gtk_widget_set_size_request(server_edit_, 400, 30); 283 gtk_container_add(GTK_CONTAINER(hbox), server_edit_); 284 285 port_edit_ = gtk_entry_new(); 286 gtk_entry_set_text(GTK_ENTRY(port_edit_), port_.c_str()); 287 gtk_widget_set_size_request(port_edit_, 70, 30); 288 gtk_container_add(GTK_CONTAINER(hbox), port_edit_); 289 290 GtkWidget* button = gtk_button_new_with_label("Connect"); 291 gtk_widget_set_size_request(button, 70, 30); 292 g_signal_connect(button, "clicked", G_CALLBACK(OnClickedCallback), this); 293 gtk_container_add(GTK_CONTAINER(hbox), button); 294 295 GtkWidget* halign = gtk_alignment_new(1, 0, 0, 0); 296 gtk_container_add(GTK_CONTAINER(halign), hbox); 297 gtk_box_pack_start(GTK_BOX(vbox_), halign, FALSE, FALSE, 0); 298 299 gtk_widget_show_all(window_); 300 301 if (autoconnect_) 302 g_idle_add(SimulateButtonClick, button); 303 } 304 305 void GtkMainWnd::SwitchToPeerList(const Peers& peers) { 306 RTC_LOG(LS_INFO) << __FUNCTION__; 307 308 if (!peer_list_) { 309 gtk_container_set_border_width(GTK_CONTAINER(window_), 0); 310 if (vbox_) { 311 gtk_widget_destroy(vbox_); 312 vbox_ = nullptr; 313 server_edit_ = nullptr; 314 port_edit_ = nullptr; 315 } else if (draw_area_) { 316 gtk_widget_destroy(draw_area_); 317 draw_area_ = nullptr; 318 draw_buffer_.SetSize(0); 319 } 320 321 peer_list_ = gtk_tree_view_new(); 322 g_signal_connect(peer_list_, "row-activated", 323 G_CALLBACK(OnRowActivatedCallback), this); 324 gtk_tree_view_set_headers_visible(GTK_TREE_VIEW(peer_list_), FALSE); 325 InitializeList(peer_list_); 326 gtk_container_add(GTK_CONTAINER(window_), peer_list_); 327 gtk_widget_show_all(window_); 328 } else { 329 GtkListStore* store = 330 GTK_LIST_STORE(gtk_tree_view_get_model(GTK_TREE_VIEW(peer_list_))); 331 gtk_list_store_clear(store); 332 } 333 334 AddToList(peer_list_, "List of currently connected peers:", -1); 335 for (Peers::const_iterator i = peers.begin(); i != peers.end(); ++i) 336 AddToList(peer_list_, i->second.c_str(), i->first); 337 338 if (autocall_ && peers.begin() != peers.end()) 339 g_idle_add(SimulateLastRowActivated, peer_list_); 340 } 341 342 void GtkMainWnd::SwitchToStreamingUI() { 343 RTC_LOG(LS_INFO) << __FUNCTION__; 344 345 RTC_DCHECK(draw_area_ == nullptr); 346 347 gtk_container_set_border_width(GTK_CONTAINER(window_), 0); 348 if (peer_list_) { 349 gtk_widget_destroy(peer_list_); 350 peer_list_ = nullptr; 351 } 352 353 draw_area_ = gtk_drawing_area_new(); 354 gtk_container_add(GTK_CONTAINER(window_), draw_area_); 355 g_signal_connect(G_OBJECT(draw_area_), "draw", G_CALLBACK(&::Draw), this); 356 357 gtk_widget_show_all(window_); 358 } 359 360 void GtkMainWnd::OnDestroyed(GtkWidget* widget, GdkEvent* event) { 361 callback_->Close(); 362 window_ = nullptr; 363 draw_area_ = nullptr; 364 vbox_ = nullptr; 365 server_edit_ = nullptr; 366 port_edit_ = nullptr; 367 peer_list_ = nullptr; 368 } 369 370 void GtkMainWnd::OnClicked(GtkWidget* widget) { 371 // Make the connect button insensitive, so that it cannot be clicked more than 372 // once. Now that the connection includes auto-retry, it should not be 373 // necessary to click it more than once. 374 gtk_widget_set_sensitive(widget, false); 375 server_ = gtk_entry_get_text(GTK_ENTRY(server_edit_)); 376 port_ = gtk_entry_get_text(GTK_ENTRY(port_edit_)); 377 int port = !port_.empty() ? atoi(port_.c_str()) : 0; 378 callback_->StartLogin(server_, port); 379 } 380 381 void GtkMainWnd::OnKeyPress(GtkWidget* widget, GdkEventKey* key) { 382 if (key->type == GDK_KEY_PRESS) { 383 switch (key->keyval) { 384 case GDK_KEY_Escape: 385 if (draw_area_) { 386 callback_->DisconnectFromCurrentPeer(); 387 } else if (peer_list_) { 388 callback_->DisconnectFromServer(); 389 } 390 break; 391 392 case GDK_KEY_KP_Enter: 393 case GDK_KEY_Return: 394 if (vbox_) { 395 OnClicked(nullptr); 396 } else if (peer_list_) { 397 // OnRowActivated will be called automatically when the user 398 // presses enter. 399 } 400 break; 401 402 default: 403 break; 404 } 405 } 406 } 407 408 void GtkMainWnd::OnRowActivated(GtkTreeView* tree_view, 409 GtkTreePath* path, 410 GtkTreeViewColumn* column) { 411 RTC_DCHECK(peer_list_ != nullptr); 412 GtkTreeIter iter; 413 GtkTreeModel* model; 414 GtkTreeSelection* selection = 415 gtk_tree_view_get_selection(GTK_TREE_VIEW(tree_view)); 416 if (gtk_tree_selection_get_selected(selection, &model, &iter)) { 417 char* text; 418 int id = -1; 419 gtk_tree_model_get(model, &iter, 0, &text, 1, &id, -1); 420 if (id != -1) 421 callback_->ConnectToPeer(id); 422 g_free(text); 423 } 424 } 425 426 void GtkMainWnd::OnRedraw() { 427 gdk_threads_enter(); 428 429 VideoRenderer* remote_renderer = remote_renderer_.get(); 430 if (remote_renderer && !remote_renderer->image().empty() && 431 draw_area_ != nullptr) { 432 if (width_ != remote_renderer->width() || 433 height_ != remote_renderer->height()) { 434 width_ = remote_renderer->width(); 435 height_ = remote_renderer->height(); 436 gtk_widget_set_size_request(draw_area_, remote_renderer->width(), 437 remote_renderer->height()); 438 } 439 draw_buffer_.SetData(remote_renderer->image()); 440 gtk_widget_queue_draw(draw_area_); 441 } 442 // Here we can draw the local preview as well if we want.... 443 gdk_threads_leave(); 444 } 445 446 void GtkMainWnd::Draw(GtkWidget* widget, cairo_t* cr) { 447 cairo_format_t format = CAIRO_FORMAT_ARGB32; 448 cairo_surface_t* surface = cairo_image_surface_create_for_data( 449 draw_buffer_.data(), format, width_, height_, 450 cairo_format_stride_for_width(format, width_)); 451 cairo_set_source_surface(cr, surface, 0, 0); 452 cairo_rectangle(cr, 0, 0, width_, height_); 453 cairo_fill(cr); 454 cairo_surface_destroy(surface); 455 } 456 457 GtkMainWnd::VideoRenderer::VideoRenderer( 458 GtkMainWnd* main_wnd, 459 webrtc::VideoTrackInterface* track_to_render) 460 : width_(0), 461 height_(0), 462 main_wnd_(main_wnd), 463 rendered_track_(track_to_render) { 464 rendered_track_->AddOrUpdateSink(this, webrtc::VideoSinkWants()); 465 } 466 467 GtkMainWnd::VideoRenderer::~VideoRenderer() { 468 rendered_track_->RemoveSink(this); 469 } 470 471 void GtkMainWnd::VideoRenderer::SetSize(int width, int height) { 472 gdk_threads_enter(); 473 474 if (width_ == width && height_ == height) { 475 return; 476 } 477 478 width_ = width; 479 height_ = height; 480 // ARGB 481 image_.SetSize(width * height * 4); 482 gdk_threads_leave(); 483 } 484 485 void GtkMainWnd::VideoRenderer::OnFrame(const webrtc::VideoFrame& video_frame) { 486 gdk_threads_enter(); 487 488 webrtc::scoped_refptr<webrtc::I420BufferInterface> buffer( 489 video_frame.video_frame_buffer()->ToI420()); 490 if (video_frame.rotation() != webrtc::kVideoRotation_0) { 491 buffer = webrtc::I420Buffer::Rotate(*buffer, video_frame.rotation()); 492 } 493 SetSize(buffer->width(), buffer->height()); 494 495 // TODO(bugs.webrtc.org/6857): This conversion is correct for little-endian 496 // only. Cairo ARGB32 treats pixels as 32-bit values in *native* byte order, 497 // with B in the least significant byte of the 32-bit value. Which on 498 // little-endian means that memory layout is BGRA, with the B byte stored at 499 // lowest address. Libyuv's ARGB format (surprisingly?) uses the same 500 // little-endian format, with B in the first byte in memory, regardless of 501 // native endianness. 502 libyuv::I420ToARGB(buffer->DataY(), buffer->StrideY(), buffer->DataU(), 503 buffer->StrideU(), buffer->DataV(), buffer->StrideV(), 504 image_.data(), width_ * 4, buffer->width(), 505 buffer->height()); 506 507 gdk_threads_leave(); 508 509 g_idle_add(Redraw, main_wnd_); 510 }