Eyeson Blog

Stop Meetings on Inactivity

Written by Christoph Lipautz | November 22, 2021

Any eyeson video conference will stay open as long as at least one participant is present, and shut down after a short waiting time when the last user has disconnected. In case your user did miss to disconnect or is gone for some other reason, we strongly suggest using some sort of inactivity detection and auto-shutdown in your client applications. Using the default web UI we already got you covered: Any participant will be shown a dialog if there is no action detected for a long time.

Besides checking your client's active status we recommend implementing an additional server-side hard limit that ensures that no meeting will run for too long.

Soft Limit - Detect User Inactivity

Given a web user, we want to check if there is any interaction with our interface or even just react on mouse movements. The following example code does join a meeting with a given access key and disconnects if there has been neither a mouse movement nor a keyboard event for at least one hour.


      const eyeson = window.eyeson.default;
      const accessKey = "";

      eyeson.onEvent(({ type, remoteStream }) => {
        if (type !== "accept") {
          return;
        }
        const video = document.querySelector("video");
        video.srcObject = remoteStream;
        video.play();
      });
      eyeson.start(accessKey);

      const TIMEOUT = 1 * 60 * 60 * 1000;

      const shutdown = () => {
        window.removeEventListener("mousemove", resetTimer);
        window.removeEventListener("keydown", resetTimer);
        console.debug("Run Shutdown");
        eyeson.send({ type: "session_termination" });
      }

      let inactivityTimeout = setTimeout(shutdown, TIMEOUT);

      const resetTimer = () => {
        clearTimeout(inactivityTimeout);
        inactivityTimeout = setTimeout(shutdown, TIMEOUT);
        console.debug("Inactivity Timer has been Reset");
      };

      window.addEventListener("mousemove", resetTimer);
      window.addEventListener("keydown", resetTimer);
    

For sure you can also utilize browser features like the Page Visibility API for your solution depending on the use of your application client.

Hard Limit - Shutdown Any Meeting After a Time Limit

In order to force a meeting shutdown, you need to keep track of the current state of all your video conferences. This can easily be done by storing information whenever a meeting gets started. All left is to recheck the meeting shutdown state at the eyeson API when your hard time limit is reached. Another approach is to use webhooks to receive the details.

We have put together an example written in Go that registers a webhook, listens for incoming requests from eyeson for new meeting information. It also keeps track of all running meetings, periodically tests those if they have exceeded the time limit, and enforces a shutdown in case the limit has been reached.

package autoclose

import (
	"encoding/json"
	"log"
	"net/http"
	"os"
	"os/signal"
	"time"

	eyeson "github.com/eyeson-team/eyeson-go"
)

// CHECK_PERIOD sets the period the durations of all meetings should be
// checked.
const CHECK_PERIOD int = 60

// MAX_DURATION sets the hard limit in seconds. A meeting will not be allowed
// to run longer than this time range.
const MAX_DURATION int = 1800

// Server keeps an eyeson API client and tracks all active meetings.
type Server struct {
	client   *eyeson.Client
	meetings map[string]time.Time
}

// NewServer provides a new server instance by a given eyeson api key.
func NewServer(apiKey string) *Server {
	return &Server{client: eyeson.NewClient(apiKey),
		meetings: make(map[string]time.Time)}
}

// StartAndListen starts observing meetings and listens for incoming webhooks.
func (s *Server) StartAndListen(url, port string) {
	log.Println("Register webhook for endpoint", url)
	err := s.client.Webhook.Register(url, eyeson.WEBHOOK_ROOM)
	if err != nil {
		log.Fatal(err)
	}

	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		var data eyeson.Webhook
		if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
			log.Println("Could not parse request: ", err)
		}
		log.Println("Got new webhook for Room", data.Room.Name)
		s.handleRoomUpdate(&data)
		w.WriteHeader(204)
	})
	srv := &http.Server{Addr: port, Handler: mux}
	stop := make(chan os.Signal)
	signal.Notify(stop, os.Interrupt)

	go s.observeMeetings()
	go func() {
		log.Printf("Listen for connections on port %v", port)
		if err = srv.ListenAndServe(); err != nil {
			if err != http.ErrServerClosed {
				log.Fatal(err)
			}
		}
	}()
	<-stop

	log.Println("Shutting down...")
	log.Println("Unregister webhook")
	if err = s.client.Webhook.Unregister(); err != nil {
		log.Fatal("Failed to unregister webhook: ", err)
	}
}

// handleRoomUpdate takes the data received from a webhook and updates the map
// of active meetings.
func (s *Server) handleRoomUpdate(data *eyeson.Webhook) {
	if data.Room.Shutdown == false {
		if s.IsRegistered(data.Room.Id) == false {
			log.Printf("Add meeting %v", data.Room.Id)
			s.meetings[data.Room.Id] = data.Room.StartedAt
		}
	} else {
		if s.IsRegistered(data.Room.Id) == true {
			log.Printf("Normal shutdown for meeting %v", data.Room.Id)
			delete(s.meetings, data.Room.Id)
		}
	}
}

// IsRegistered checks if a meeting id is in the list of registered meetings.
func (s *Server) IsRegistered(id string) bool {
	if _, ok := s.meetings[id]; ok {
		return true
	}
	return false
}

// observeMeetings periodically checks the current meetings for exceeding the
// time limit.
func (s *Server) observeMeetings() {
	for {
		log.Printf("Testing %d meeting(s) for exceeding the hard limit", len(s.meetings))
		for id, startTime := range s.meetings {
			timeLimit := startTime.Add(time.Duration(MAX_DURATION) * time.Second)
			if time.Now().Before(timeLimit) {
				continue
			}
			log.Printf("Force shutdown for meeting %v that started at %v", id, startTime)
			if err := s.client.Rooms.Shutdown(id); err != nil {
				log.Println("Could not shutdown meeting: ", err)
			}
			delete(s.meetings, id)
		}
		time.Sleep(time.Duration(CHECK_PERIOD) * time.Second)
	}
}