text: # Copyright 2017 The Imaging Source Europe GmbH
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
  # You may obtain a copy of the License at
  #
  # http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS IS" BASIS,
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  # See the License for the specific language governing permissions and
  # limitations under the License.
  
  import re
  import time
  from tcam_capture.CapsDesc import CapsDesc
  from tcam_capture.TcamScreen import TcamScreen
  from tcam_capture.FileNameGenerator import FileNameGenerator
  from tcam_capture.MediaSaver import MediaSaver
  from tcam_capture.Settings import Settings
  from tcam_capture.Encoder import MediaType, get_encoder_dict
  from tcam_capture.TcamCaptureData import TcamCaptureData
  from tcam_capture.FPSCounter import FPSCounter
  from PyQt5 import QtGui, QtWidgets, QtCore
  from PyQt5.QtWidgets import (QWidget, QHBoxLayout)
  from PyQt5.QtCore import QObject, pyqtSignal, Qt, QEvent
  
  import logging
  
  import gi
  
  gi.require_version("Gst", "1.0")
  gi.require_version("Tcam", "0.1")
  gi.require_version("GstVideo", "1.0")
  
  from gi.repository import Tcam, Gst, GLib, GstVideo
  
  log = logging.getLogger(__name__)
  
  
  class TcamView(QWidget):
  
      image_saved = pyqtSignal(str)
      video_saved = pyqtSignal(str)
      new_pixel_under_mouse = pyqtSignal(bool, int, int, QtGui.QColor)
      current_fps = pyqtSignal(float)
      format_selected = pyqtSignal(str, str, str)  # format, widthxheight, framerate
      first_image = pyqtSignal()
  
      def __init__(self, serial: str, dev_type: str, parent=None):
          super(TcamView, self).__init__(parent)
          self.layout = QHBoxLayout()
          self.container = TcamScreen(self)
          self.container.new_pixel_under_mouse.connect(self.new_pixel_under_mouse_slot)
          self.fullscreen_container = None  # separate widget for fullscreen usage
          self.is_fullscreen = False
  
          self.layout.addWidget(self.container)
          self.layout.setSizeConstraint(QtWidgets.QLayout.SetMaximumSize)
          self.setLayout(self.layout)
          self.serial = serial
          self.dev_type = dev_type
          self.tcam = None
          self.pipeline = None
          self.image = None
          self.mouse_is_pressed = False
          self.use_dutils = True
          self.current_width = 0
          self.current_height = 0
          self.device_lost_callbacks = []
          self.caps_desc = CapsDesc(None)
          self.video_format = None
          self.retry_countdown = 0
  
          self.settings = None
          self.video_fng = None
          self.image_fng = None
  
          # additional timer to update actual_fps
          # when no images arrive
          self.fps_timer = QtCore.QTimer()
          self.fps_timer.timeout.connect(self.fps_tick)
          self.fps = None
          self.file_pattern = ""
          self.file_location = "/tmp"
          self.caps = None
          self.state = None
          self.videosaver = None
          self.imagesaver = None
  
      def get_caps_desc(self):
          """
          Returns a CapsDesc describing the caps of the currently opened device
          Returns None if device is not opened
          """
          if not self.caps_desc:
              tcam = self.get_tcam()
              if not tcam:
                  return None
              caps = tcam.get_static_pad("src").query_caps()
              self.caps_desc = CapsDesc(caps)
          return self.caps_desc
  
      def new_pixel_under_mouse_slot(self, active: bool,
                                     mouse_x: int, mouse_y: int,
                                     color: QtGui.QColor):
          self.new_pixel_under_mouse.emit(active, mouse_x, mouse_y, color)
  
      def eventFilter(self, obj, event):
          """"""
          if event.type == QEvent.KeyPress:
              if event.key() == Qt.Key_F11:
                  self.toggle_fullscreen()
                  return True
  
          return QObject.eventFilter(self, obj, event)
  
      def set_settings(self, new_settings: Settings):
          """
          Update settings of all subclasses
          """
          self.settings = new_settings
  
          self.use_dutils = self.settings.use_dutils
          if not self.video_fng:
              self.video_fng = FileNameGenerator(self.serial,
                                                 self.settings.video_name)
          else:
              self.video_fng.set_settings(self.settings.video_name)
          self.video_fng.location = self.settings.save_location
          self.video_fng.file_suffix = get_encoder_dict()[self.settings.video_type].file_ending
  
          if not self.image_fng:
              self.image_fng = FileNameGenerator(self.serial,
                                                 self.settings.image_name)
          else:
              self.image_fng.set_settings(self.settings.image_name)
          self.image_fng.location = self.settings.save_location
          self.image_fng.file_suffix = get_encoder_dict()[self.settings.image_type].file_ending
  
      def toggle_fullscreen(self):
          if self.is_fullscreen:
              self.is_fullscreen = False
              self.showNormal()
              self.fullscreen_container.hide()
              # self.fullscreen_container.deleteLater()
              self.fullscreen_container = None
          else:
              self.is_fullscreen = True
              self.fullscreen_container = TcamScreen()
              self.fullscreen_container.is_fullscreen = True
  
              self.fullscreen_container.setAttribute(QtCore.Qt.WA_DeleteOnClose)
              self.fullscreen_container.showFullScreen()
              self.fullscreen_container.show()
              self.container.first_image = True
  
              self.fullscreen_container.setFocusPolicy(QtCore.Qt.StrongFocus)
              self.fullscreen_container.installEventFilter(self.fullscreen_container)
              self.fullscreen_container.destroy_widget.connect(self.toggle_fullscreen)
              # either show info that we are in trigger mode and still waiting for the first image
              # or show that last image we had. This way we always have something to show to the user
              if self.is_trigger_mode_on() and self.container.first_image:
                  self.fullscreen_container.wait_for_first_image()
              else:
                  self.fullscreen_container.on_new_pixmap(self.container.pix.pixmap())
  
      def fit_view(self):
          if self.is_fullscreen:
              self.fullscreen_container.fit_in_view.emit()
          else:
              self.container.fit_in_view.emit()
  
      def save_image(self, image_type: str):
  
          if not self.imagesaver:
              self.imagesaver = MediaSaver(self.serial, self.caps, MediaType.image)
              self.imagesaver.saved.connect(self.image_saved_callback)
              self.imagesaver.error.connect(self.image_error_callback)
          self.image_fng.set_settings(self.settings.image_name)
  
          fn = self.image_fng.create_file_name("image")
  
          self.imagesaver.current_filename = fn
          self.imagesaver.save_image(get_encoder_dict()[image_type])
  
      def image_saved_callback(self, image_path: str):
          """
          SLOT for imagesaver callback for successfull saving
          """
          self.image_saved.emit(image_path)
  
      def image_error_callback(self, error_msg: str):
          pass
  
      def video_saved_callback(self, video_path: str):
          """
          SLOT for videosaver callback for successfull saving
          """
          self.video_saved.emit(video_path)
  
      def start_recording_video(self, video_type: str):
          """
  
          """
          if self.videosaver:
              log.error("A video recording is already ongoing.")
              return
          self.videosaver = MediaSaver(self.serial, self.caps, MediaType.video)
          self.videosaver.set_encoder(video_type)
          self.videosaver.location = self.file_location
  
          self.videosaver.current_filename = self.video_fng.create_file_name()
          self.videosaver.saved.connect(self.video_saved_callback)
          self.videosaver.start_recording_video(video_type)
  
      def stop_recording_video(self):
          """
  
          """
          if self.videosaver:
              self.videosaver.stop_recording_video()
              self.videosaver = None
  
      def get_gst_state(self, timeout=5):
          """
          Arguments:
          timeout=5, optional
          """
  
          if not self.pipeline:
              return None
          return self.pipeline.get_state(timeout).state
  
      def play(self, video_format=None):
  
          if self.videosaver:
              self.stop_recording_video()
  
          if self.pipeline is None:
              self.create_pipeline()
  
          if self.get_gst_state() == Gst.State.PLAYING:
              log.debug("Setting state to NULL")
              # Set to NULL to ensure that buffers,
              # etc are destroyed.
              # do this by calling stop
              # so that additional steps like fps.stop()
              # are taken
              self.stop()
              self.pipeline.set_state(Gst.State.READY)
  
          if video_format:
              caps_desc = self.get_caps_desc()
              if caps_desc.contains(video_format):
                  self.video_format = video_format
              else:
                  log.error("Given format caps could not be found in caps descriptions. {}".format(video_format))
                  log.error("Falling back to default behavior.")
  
          if self.video_format is not None:
              log.info("Setting format to {}".format(video_format))
              caps = self.pipeline.get_by_name("bin")
              caps.set_property("device-caps",
                                video_format)
  
          if self.state and self.settings.apply_property_cache:
              log.info("Property state found.")
              # log.debug("Setting state: ==>{}<==".format(self.state))
              self.tcam.set_property("state", str(self.state))
              self.state = None
          else:
              log.info("No property state to be applied. Starting vanilla camera")
  
          log.debug("Setting state to PLAYING")
          self.pipeline.set_state(Gst.State.PLAYING)
          self.fps_timer.start(1000)  # 1 second
          self.fps = FPSCounter()
          self.fps.start()
          self.container.first_image = True
          if self.is_trigger_mode_on():
              self.container.wait_for_first_image()
  
      def fps_tick(self):
          """
          Recalculate the current fps and emit current_fps signal
          """
          self.current_fps.emit(self.fps.get_fps())
  
      def new_buffer(self, appsink):
          """
          callback for appsink new-sample signal
          converts gstbuffer into qpixmap and gives it to the display container
          """
          buf = self.pipeline.get_by_name("sink").emit("pull-sample")
          caps = buf.get_caps()
          self.caps = caps
          struc = caps.get_structure(0)
          b = buf.get_buffer()
          try:
              (ret, buffer_map) = b.map(Gst.MapFlags.READ)
              if self.current_width == 0:
                  self.current_width = struc.get_value("width")
              if self.current_height == 0:
                  self.current_height = struc.get_value("height")
  
              self.fps.tick()
              self.fps_tick()
  
              if self.container.first_image:
                  self.first_image.emit()
  
              self.image = QtGui.QPixmap.fromImage(QtGui.QImage(buffer_map.data,
                                                                struc.get_value("width"),
                                                                struc.get_value("height"),
                                                                QtGui.QImage.Format_ARGB32))
              if self.fullscreen_container is not None:
                  if self.fullscreen_container.isFullScreen():
                      self.fullscreen_container.new_pixmap.emit(self.image)
              else:
                  self.container.new_pixmap.emit(self.image)
              if self.videosaver and self.videosaver.accept_buffer:
                  self.videosaver.feed_image(b)
              if self.imagesaver and self.imagesaver.accept_buffer:
                  self.imagesaver.feed_image(b)
  
          finally:
              b.unmap(buffer_map)
  
          return Gst.FlowReturn.OK
  
      def create_pipeline(self, video_format=None):
  
          # we cheat
          # inject the type into the serial
          # this ensures that no matter what we
          # always have the correct backend
          if self.dev_type:
              self.serial = "{}-{}".format(self.serial, self.dev_type.lower())
  
          # the queue element before the sink is important.
          # it allows set_state to work as expected.
          # the sink is synced with our main thread (the display thread).
          # changing the state from out main thread will cause a deadlock,
          # since the remaining buffers can not be displayed because our main thread
          # is currently in set_state
          pipeline_str = ("tcambin serial={serial} name=bin use-dutils={dutils} "
                          "! video/x-raw,format=BGRx "
                          "! tee name=tee tee. "
                          "! queue max-size-buffers=2 leaky=downstream "
                          "! videoconvert "
                          "! video/x-raw,format=BGRx "
                          "! appsink name=sink emit-signals=true sync=false")
  
          self.pipeline = None
          self.pipeline = Gst.parse_launch(pipeline_str.format(serial=self.serial,
                                                               type=self.dev_type.lower(),
                                                               dutils=self.use_dutils))
  
          sink = self.pipeline.get_by_name("sink")
          sink.connect("new-sample", self.new_buffer)
          # Create bus to get events from GStreamer pipeline
          self.bus = self.pipeline.get_bus()
          self.bus.add_signal_watch()
          self.bus.enable_sync_message_emission()
          self.bus.connect('message::error', self.on_error)
          self.bus.connect('message::info', self.on_info)
  
          self.tcam = self.pipeline.get_by_name("bin")
  
          if video_format:
              self.tcam.set_property("device-caps", video_format)
  
          # This ready is required so that get_caps_desc
          # works and does not return ANY
          self.pipeline.set_state(Gst.State.READY)
          log.debug("Created pipeline and set to READY")
          log.debug("Pipeline is: {}".format(pipeline_str.format(serial=self.serial,
                                                                 type=self.dev_type.lower(),
                                                                 dutils=self.use_dutils)))
  
      def pause(self):
          log.info("Setting state to PAUSED")
          if self.pipeline:
              self.pipeline.set_state(Gst.State.PAUSED)
          else:
              log.error("Pipeline object does not exist.")
  
          self.fps_timer.stop()
          if self.fps:
              self.fps.stop()
  
      def stop(self):
          """
          Stop playback
          """
          log.info("Setting state to NULL")
          self.fps_timer.stop()
          if self.fps:
              self.fps.stop()
  
          self.pipeline.set_state(Gst.State.NULL)
  
      def on_info(self, bus, msg):
          """
          Callback for gst bus info messages
          """
          info, dbg = msg.parse_info()
  
          log.info(dbg)
  
          if msg.src.get_name() == "bin":
  
              if dbg.startswith("Working with src caps:"):
                  log.info("{}".format(dbg.split(": ")[1]))
                  self.caps = dbg.split(": ")[1]
                  self.fire_format_selected(dbg.split(": ")[1])
              else:
                  log.error("Info from bin: {}".format(dbg))
          else:
              log.error("ERROR:", msg.src.get_name())
              if dbg:
                  log.debug("Debug info:", dbg)
  
      def fire_format_selected(self, caps: str):
          """
          Emit SIGNAL that the pipeline has selected
          src caps and inform listeners what the caps are
          """
  
          if caps is None or caps == "NULL":
              log.error("Bin returned faulty source caps. Not firiing format_selected")
              return
  
          c = Gst.Caps.from_string(caps)
  
          if c.is_empty():
              log.error("Received empty caps. Aborting fire_format_selected")
              return
  
          structure = c.get_structure(0)
  
          self.image_fng.set_caps(c)
          self.video_fng.set_caps(c)
  
          if structure.get_name() == "image/jpeg":
              fmt = "jpeg"
          else:
              fmt = structure.get_value("format")
  
          resolution = "{}x{}".format(structure.get_value("width"),
                                      structure.get_value("height"))
  
          # compatability problems
          # Older python bindings do not know the type Gst.Fraction.
          # Thus we have to work around this problem...
          results = re.search("framerate=\(fraction\)\d+/\d+", caps)
  
          if results:
              fps = results.group()
              fps = fps.replace("framerate=(fraction)", "")
          else:
              fps = None
              log.error("Unable to determine framerate settings. This will affect usability.")
  
          self.format_selected.emit(fmt, resolution, str(fps))
  
      def on_error(self, bus, msg):
          """
          Callback for gst bus messages
          Receives errors and chooses appropriate actions
          """
          err, dbg = msg.parse_error()
  
          if msg.src.get_name() == "tcambin-source":
  
              if err:
  
                  if "Device lost (" in err.message:
  
                      m = re.search('Device lost \((.*)\)', err.message)
                      log.error("Received device lost message for {}".format(m.group(1)))
  
                      self.fire_device_lost()
                  else:
                      log.error("Error from source: {}".format(err.message))
  
                      self.retry_countdown -= 1
  
                      if self.retry_countdown <= 0:
                          log.error("Repeatedly retried to start stream. No Success. Giving up.")
                          return
  
                      log.info("Trying restart of stream")
                      self.stop()
                      self.play(self.video_format)
  
          else:
              log.error("ERROR: {} : {}".format(msg.src.get_name(), err.message))
              if dbg:
                  log.debug("Debug info: {}".format(dbg))
  
      def get_tcam(self):
          return self.tcam
  
      def register_device_lost(self, callback):
          self.device_lost_callbacks.append(callback)
  
      def fire_device_lost(self):
          """
          Notify all callback that our device is gone
          """
          for cb in self.device_lost_callbacks:
              cb()
  
      def is_trigger_mode_on(self):
          if not self.tcam:
  
              return False
  
          names = self.tcam.get_tcam_property_names()
          if "Trigger Mode" not in names:
              return False
  
          try:
              (result, value,
               minval, maxval,
               defval, step,
               valuetype,
               flags,
               category, group) = self.tcam.get_tcam_property("Trigger Mode")
          except TypeError:
              log.warning("get_tcam_property failed for '{}'".format("Trigger Mode"))
              return False
  
          if valuetype == "boolean":
              if value:
                  return True
              return False
  
          elif valuetype == "enum":
              if value == "On":
                  return True
  
          return True
  
      def trigger_image(self):
          """
          Checks if trigger mode is active and try to trigger an image
          """
          if self.is_trigger_mode_on():
              self.tcam.set_tcam_property("Software Trigger", True)
  
      def start_roi_capture(self, finished_signal):
          """
          Start capturing a ROI and emit finished_signal once the capture is finished
          """
          self.container.start_roi_capture(finished_signal)
  
      def add_roi(self, roi_widget):
          """
          Add the given roi_widget for permanent display.
          Call remove_roi to undo.
          """
          self.container.add_roi(roi_widget)
  
      def remove_roi(self, roi_widget):
          """
          Remove roi_widget from display
          """
          self.container.remove_roi(roi_widget)
  
      def get_state(self):
          """
          Retrieve a json description of the current property settings
          Returns:
          str or None
          """
          if not self.tcam:
              return None
  
          return self.tcam.get_property("state")
  
      def load_state(self, state: str):
          """
          Arguments:
          state:
          str containing json descibing the property values
          """
          self.state = state
  
      @staticmethod
      def has_dutils():
          """
          Check to see if the gstreamer module gsttcamdutils is available.
          """
  
          factory = Gst.ElementFactory.find("tcamdutils")
  
          if factory:
              return True
          return False
  