// Send HTTP response and a stream of JPG-frames void MjpegServerImpl::ConnThread::SendStream(wpi::raw_socket_ostream& os) { if (m_noStreaming) { SERROR("Too many simultaneous client streams"); SendError(os, 503, "Too many simultaneous streams"); return; } os.SetUnbuffered(); wpi::SmallString<256> header; wpi::raw_svector_ostream oss{header}; SendHeader(oss, 200, "OK", "multipart/x-mixed-replace;boundary=" BOUNDARY); os << oss.str(); SDEBUG("Headers send, sending stream now"); Frame::Time lastFrameTime = 0; Frame::Time timePerFrame = 0; if (m_fps != 0) timePerFrame = 1000000.0 / m_fps; Frame::Time averageFrameTime = 0; Frame::Time averagePeriod = 1000000; // 1 second window if (averagePeriod < timePerFrame) averagePeriod = timePerFrame * 10; StartStream(); while (m_active && !os.has_error()) { auto source = GetSource(); if (!source) { // Source disconnected; sleep so we don't consume all processor time. os << "\r\n"; // Keep connection alive std::this_thread::sleep_for(std::chrono::milliseconds(200)); continue; } SDEBUG4("waiting for frame"); Frame frame = source->GetNextFrame(0.225); // blocks if (!m_active) break; if (!frame) { // Bad frame; sleep for 20 ms so we don't consume all processor time. os << "\r\n"; // Keep connection alive std::this_thread::sleep_for(std::chrono::milliseconds(20)); continue; } auto thisFrameTime = frame.GetTime(); if (thisFrameTime != 0 && timePerFrame != 0 && lastFrameTime != 0) { Frame::Time deltaTime = thisFrameTime - lastFrameTime; // drop frame if it is early compared to the desired frame rate AND // the current average is higher than the desired average if (deltaTime < timePerFrame && averageFrameTime < timePerFrame) { // sleep for 1 ms so we don't consume all processor time std::this_thread::sleep_for(std::chrono::milliseconds(1)); continue; } // update average if (averageFrameTime != 0) { averageFrameTime = averageFrameTime * (averagePeriod - timePerFrame) / averagePeriod + deltaTime * timePerFrame / averagePeriod; } else { averageFrameTime = deltaTime; } } int width = m_width != 0 ? m_width : frame.GetOriginalWidth(); int height = m_height != 0 ? m_height : frame.GetOriginalHeight(); Image* image = frame.GetImageMJPEG( width, height, m_compression, m_compression == -1 ? m_defaultCompression : m_compression); if (!image) { // Shouldn't happen, but just in case... std::this_thread::sleep_for(std::chrono::milliseconds(20)); continue; } const char* data = image->data(); size_t size = image->size(); bool addDHT = false; size_t locSOF = size; switch (image->pixelFormat) { case VideoMode::kMJPEG: // Determine if we need to add DHT to it, and allocate enough space // for adding it if required. addDHT = JpegNeedsDHT(data, &size, &locSOF); break; case VideoMode::kYUYV: case VideoMode::kRGB565: default: // Bad frame; sleep for 10 ms so we don't consume all processor time. std::this_thread::sleep_for(std::chrono::milliseconds(10)); continue; } SDEBUG4("sending frame size=" << size << " addDHT=" << addDHT); // print the individual mimetype and the length // sending the content-length fixes random stream disruption observed // with firefox lastFrameTime = thisFrameTime; double timestamp = lastFrameTime / 1000000.0; header.clear(); oss << "\r\n--" BOUNDARY "\r\n" << "Content-Type: image/jpeg\r\n" << "Content-Length: " << size << "\r\n" << "X-Timestamp: " << timestamp << "\r\n" << "\r\n"; os << oss.str(); if (addDHT) { // Insert DHT data immediately before SOF os << wpi::StringRef(data, locSOF); os << JpegGetDHT(); os << wpi::StringRef(data + locSOF, image->size() - locSOF); } else { os << wpi::StringRef(data, size); } // os.flush(); } StopStream(); }