Allow replay of Continuous recordings if --load-tape is given a URL.
E.g. for --load-tape=http[s]://foo.com/foo/bar/wibble.fgtape, we download in the background to a file called foo.com_[MD5]_wibble.fgtape, where [MD5] is an 8-character hash of /foo/bar. We assume any existing file contains valid data and only download any remaining data (by specifying an http Range header). Also, when loading/downloading and replaying continuous recordings at startup, we set the aircraft and airport from the recording. src/Aircraft/replay.cxx src/Aircraft/replay.hxx FGReplay::loadContinuousHeader() Loads properties from Continuous recording's header, distinguishing between failure due to incorrect header, or due to a truncated file. FGReplay::indexContinuousRecording() Contains Continuous recording indexing code, in a form that can be used in background while we are downloading. Added a mutex to protect m_continuous_in_time_to_frameinfo, which can now be modified in background as Continuous recording is downloaded. src/Main/fg_init.cxx src/Main/options.cxx fgOptLoadTape(): Modified to handle --load-tape=<url>. We start download, and read the header before returning, so that we can force the FDM to use the recording's aircraft instead of the user's default. Limit recording download rate if /sim/replay/download-max-bytes-per-sec is set, by calling new filerequest->setMaxBytesPerSec().
This commit is contained in:
parent
02e0d17dbc
commit
f3679f121d
4 changed files with 418 additions and 159 deletions
|
@ -1239,6 +1239,8 @@ FGReplay::replay( double time ) {
|
|||
|
||||
replayMessage(time);
|
||||
|
||||
std::lock_guard<std::mutex> lock(m_continuous_in_time_to_frameinfo_lock);
|
||||
|
||||
if (!m_continuous_in_time_to_frameinfo.empty()) {
|
||||
// We are replaying a continuous recording.
|
||||
//
|
||||
|
@ -1615,6 +1617,7 @@ void FGReplay::replay(
|
|||
double
|
||||
FGReplay::get_start_time()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_continuous_in_time_to_frameinfo_lock);
|
||||
if (!m_continuous_in_time_to_frameinfo.empty()) {
|
||||
double ret = m_continuous_in_time_to_frameinfo.begin()->first;
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG,
|
||||
|
@ -1645,6 +1648,7 @@ FGReplay::get_start_time()
|
|||
double
|
||||
FGReplay::get_end_time()
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(m_continuous_in_time_to_frameinfo_lock);
|
||||
if (!m_continuous_in_time_to_frameinfo.empty()) {
|
||||
double ret = m_continuous_in_time_to_frameinfo.rbegin()->first;
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG,
|
||||
|
@ -1824,142 +1828,225 @@ FGReplay::saveTape(const SGPropertyNode* Extra)
|
|||
}
|
||||
|
||||
|
||||
int FGReplay::loadContinuousHeader(const std::string& path, std::istream* in, SGPropertyNode* properties)
|
||||
{
|
||||
std::ifstream in0;
|
||||
if (!in) {
|
||||
in0.open(path);
|
||||
in = &in0;
|
||||
}
|
||||
if (!*in) {
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Failed to open path=" << path);
|
||||
return +1;
|
||||
}
|
||||
std::vector<char> buffer(strlen( FlightRecorderFileMagic) + 1);
|
||||
in->read(&buffer.front(), buffer.size());
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG, "in->gcount()=" << in->gcount() << " buffer.size()=" << buffer.size());
|
||||
if ((size_t) in->gcount() != buffer.size()) {
|
||||
// Further download is needed.
|
||||
return +1;
|
||||
}
|
||||
if (strcmp(&buffer.front(), FlightRecorderFileMagic)) {
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG, "fgtape prefix doesn't match FlightRecorderFileMagic in path: " << path);
|
||||
return -1;
|
||||
}
|
||||
bool ok = false;
|
||||
try {
|
||||
PropertiesRead(*in, properties);
|
||||
ok = true;
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Failed to read Config properties in: " << path);
|
||||
}
|
||||
if (!ok) {
|
||||
// Failed to read properties, so indicate that further download is needed.
|
||||
return +1;
|
||||
}
|
||||
SG_LOG(SG_SYSTEMS, SG_BULK, "properties is:\n"
|
||||
<< writePropertiesInline(properties, true /*write_all*/) << "\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
void FGReplay::indexContinuousRecording(const void* data, size_t numbytes)
|
||||
{
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Indexing Continuous recording "
|
||||
<< " data=" << data
|
||||
<< " numbytes=" << numbytes
|
||||
<< " m_continuous_indexing_pos=" << m_continuous_indexing_pos
|
||||
<< " m_continuous_in_time_to_frameinfo.size()=" << m_continuous_in_time_to_frameinfo.size()
|
||||
);
|
||||
time_t t0 = time(NULL);
|
||||
std::streampos original_pos = m_continuous_indexing_pos;
|
||||
size_t original_num_frames = m_continuous_in_time_to_frameinfo.size();
|
||||
|
||||
// Reset any EOF because there might be new data.
|
||||
m_continuous_indexing_in.clear();
|
||||
|
||||
for(;;)
|
||||
{
|
||||
SG_LOG(SG_SYSTEMS, SG_BULK, "reading frame."
|
||||
<< " m_continuous_in.tellg()=" << m_continuous_in.tellg()
|
||||
);
|
||||
m_continuous_indexing_in.seekg(m_continuous_indexing_pos);
|
||||
double sim_time;
|
||||
m_continuous_indexing_in.read(reinterpret_cast<char*>(&sim_time), sizeof(sim_time));
|
||||
|
||||
SG_LOG(SG_SYSTEMS, SG_BULK, ""
|
||||
<< " m_continuous_indexing_pos=" << m_continuous_indexing_pos
|
||||
<< " m_continuous_indexing_in.tellg()=" << m_continuous_indexing_in.tellg()
|
||||
<< " sim_time=" << sim_time
|
||||
);
|
||||
FGFrameInfo frameinfo;
|
||||
frameinfo.offset = m_continuous_indexing_pos;
|
||||
|
||||
auto datas = m_continuous_in_config->getChildren("data");
|
||||
SG_LOG(SG_SYSTEMS, SG_BULK, "datas.size()=" << datas.size());
|
||||
for (auto data: datas) {
|
||||
uint32_t length;
|
||||
m_continuous_indexing_in.read(reinterpret_cast<char*>(&length), sizeof(length));
|
||||
SG_LOG(SG_SYSTEMS, SG_BULK,
|
||||
"m_continuous_in.tellg()=" << m_continuous_indexing_in.tellg()
|
||||
<< " Skipping data_type=" << data->getStringValue()
|
||||
<< " length=" << length
|
||||
);
|
||||
// Move forward <length> bytes.
|
||||
m_continuous_indexing_in.seekg(length, std::ios_base::cur);
|
||||
if (length) {
|
||||
if (!strcmp(data->getStringValue(), "signals")) {
|
||||
frameinfo.has_signals = true;
|
||||
}
|
||||
else if (!strcmp(data->getStringValue(), "multiplayer")) {
|
||||
frameinfo.has_multiplayer = true;
|
||||
++m_num_frames_multiplayer;
|
||||
}
|
||||
else if (!strcmp(data->getStringValue(), "extra-properties")) {
|
||||
frameinfo.has_extra_properties = true;
|
||||
++m_num_frames_extra_properties;
|
||||
}
|
||||
}
|
||||
}
|
||||
SG_LOG(SG_SYSTEMS, SG_BULK, ""
|
||||
<< " pos=" << m_continuous_indexing_pos
|
||||
<< " sim_time=" << sim_time
|
||||
<< " m_num_frames_multiplayer=" << m_num_frames_multiplayer
|
||||
<< " m_num_frames_extra_properties=" << m_num_frames_extra_properties
|
||||
);
|
||||
|
||||
if (!m_continuous_indexing_in) {
|
||||
// Failed to read a frame, e.g. because of EOF. Leave
|
||||
// m_continuous_indexing_pos unchanged so we can try again at same
|
||||
// starting position if recording is upated by background download.
|
||||
//
|
||||
SG_LOG(SG_SYSTEMS, SG_BULK, "m_continuous_indexing_in failed, giving up");
|
||||
break;
|
||||
}
|
||||
|
||||
// We have successfully read a frame, so add it to
|
||||
// m_continuous_in_time_to_frameinfo[].
|
||||
//
|
||||
m_continuous_indexing_pos = m_continuous_indexing_in.tellg();
|
||||
std::lock_guard<std::mutex> lock(m_continuous_in_time_to_frameinfo_lock);
|
||||
m_continuous_in_time_to_frameinfo[sim_time] = frameinfo;
|
||||
}
|
||||
time_t t = time(NULL) - t0;
|
||||
auto new_bytes = m_continuous_indexing_pos - original_pos;
|
||||
auto num_frames = m_continuous_in_time_to_frameinfo.size();
|
||||
auto num_new_frames = num_frames - original_num_frames;
|
||||
if (num_new_frames) {
|
||||
SG_LOG(SG_SYSTEMS, SG_ALERT, "Continuous recording: index updated:"
|
||||
<< " num_frames=" << num_frames
|
||||
<< " num_new_frames=" << num_new_frames
|
||||
<< " new_bytes=" << new_bytes
|
||||
);
|
||||
}
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Indexed uncompressed recording."
|
||||
<< " time taken=" << t << "s."
|
||||
<< " num_new_frames=" << num_new_frames
|
||||
<< " m_continuous_indexing_pos=" << m_continuous_indexing_pos
|
||||
<< " m_continuous_in_time_to_frameinfo.size()=" << m_continuous_in_time_to_frameinfo.size()
|
||||
<< " m_num_frames_multiplayer=" << m_num_frames_multiplayer
|
||||
<< " m_num_frames_extra_properties=" << m_num_frames_extra_properties
|
||||
);
|
||||
// Probably don't need this lock because we're only reading
|
||||
// m_continuous_in_time_to_frameinfo, and nothing else can be writing it.
|
||||
//
|
||||
std::lock_guard<std::mutex> lock(m_continuous_in_time_to_frameinfo_lock);
|
||||
fgSetInt("/sim/replay/continuous-stats-num-frames", m_continuous_in_time_to_frameinfo.size());
|
||||
fgSetInt("/sim/replay/continuous-stats-num-frames-extra-properties", m_num_frames_extra_properties);
|
||||
fgSetInt("/sim/replay/continuous-stats-num-frames-multiplayer", m_num_frames_multiplayer);
|
||||
if (!numbytes) {
|
||||
SG_LOG(SG_SYSTEMS, SG_ALERT, "Continuous recording: indexing finished");
|
||||
m_continuous_indexing_in.close();
|
||||
}
|
||||
}
|
||||
|
||||
/** Read a flight recorder tape with given filename from disk.
|
||||
* Copies MetaData's "meta" node into MetaMeta out-param.
|
||||
* Actual data and signal configuration is not read when in "Preview" mode.
|
||||
*/
|
||||
bool
|
||||
FGReplay::loadTape(const SGPath& Filename, bool Preview, SGPropertyNode& MetaMeta)
|
||||
FGReplay::loadTape(const SGPath& Filename, bool Preview, SGPropertyNode& MetaMeta, simgear::HTTP::FileRequest* filerequest)
|
||||
{
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG, "loading Preview=" << Preview << " Filename=" << Filename);
|
||||
{
|
||||
/* Try to load as uncompressed Continuous recording first. */
|
||||
std::ifstream in_preview;
|
||||
std::ifstream& in(Preview ? in_preview : m_continuous_in);
|
||||
in.open( Filename.str());
|
||||
if (!in) {
|
||||
SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to open Filename=" << Filename);
|
||||
return false;
|
||||
}
|
||||
std::vector<char> buffer(strlen( FlightRecorderFileMagic) + 1);
|
||||
in.read(&buffer.front(), buffer.size());
|
||||
if (strcmp(&buffer.front(), FlightRecorderFileMagic)) {
|
||||
SG_LOG(SG_SYSTEMS, SG_ALERT, "fgtape prefix doesn't match FlightRecorderFileMagic: '" << &buffer.front() << "'");
|
||||
|
||||
/* Try to load as uncompressed Continuous recording first. */
|
||||
std::ifstream in_preview;
|
||||
std::ifstream& in(Preview ? in_preview : m_continuous_in);
|
||||
in.open(Filename.str());
|
||||
if (!in) {
|
||||
SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to open"
|
||||
<< " Filename=" << Filename.str()
|
||||
<< " in.is_open()=" << in.is_open()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
m_continuous_in_config = new SGPropertyNode;
|
||||
int e = loadContinuousHeader(Filename.str(), &in, m_continuous_in_config);
|
||||
if (e == 0) {
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG, "m_continuous_in_config is:\n"
|
||||
<< writePropertiesInline(m_continuous_in_config, true /*write_all*/) << "\n");
|
||||
copyProperties(m_continuous_in_config->getNode("meta", 0, true), &MetaMeta);
|
||||
if (Preview) {
|
||||
in.close();
|
||||
}
|
||||
else {
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG, "fgtape is uncompressed: " << Filename);
|
||||
m_continuous_in_config = new SGPropertyNode;
|
||||
try {
|
||||
PropertiesRead(in, m_continuous_in_config.get());
|
||||
}
|
||||
catch (std::exception& e) {
|
||||
SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to read Config properties in: " << Filename);
|
||||
in.close();
|
||||
return false;
|
||||
}
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG, "m_continuous_in_config is:\n"
|
||||
<< writePropertiesInline(m_continuous_in_config, true /*write_all*/) << "\n");
|
||||
copyProperties(m_continuous_in_config->getNode("meta", 0, true), &MetaMeta);
|
||||
if (Preview) {
|
||||
in.close();
|
||||
return true;
|
||||
}
|
||||
m_pRecorder->reinit(m_continuous_in_config);
|
||||
clear();
|
||||
fillRecycler();
|
||||
time_t t = time(NULL);
|
||||
size_t pos = 0;
|
||||
m_continuous_in_time_last = -1;
|
||||
m_continuous_in_time_to_frameinfo.clear();
|
||||
int num_frames_extra_properties = 0;
|
||||
int num_frames_multiplayer = 0;
|
||||
// Read entire recording and build up in-memory cache of simulator
|
||||
// time to file offset, so we can handle random access.
|
||||
//
|
||||
// We also cache any frames that modify extra-properties.
|
||||
//
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Indexing Continuous recording " << Filename);
|
||||
for(;;)
|
||||
{
|
||||
SG_LOG(SG_SYSTEMS, SG_BULK, "reading frame."
|
||||
<< " m_continuous_in.tellg()=" << m_continuous_in.tellg()
|
||||
);
|
||||
pos = m_continuous_in.tellg();
|
||||
m_continuous_in.seekg(pos);
|
||||
double sim_time;
|
||||
m_continuous_in.read(reinterpret_cast<char*>(&sim_time), sizeof(sim_time));
|
||||
|
||||
SG_LOG(SG_SYSTEMS, SG_BULK,
|
||||
"pos=" << pos
|
||||
<< " m_continuous_in.tellg()=" << m_continuous_in.tellg()
|
||||
<< " sim_time=" << sim_time
|
||||
);
|
||||
FGFrameInfo frameinfo;
|
||||
frameinfo.offset = pos;
|
||||
|
||||
//bool frame_has_property_changes = false;
|
||||
auto datas = m_continuous_in_config->getChildren("data");
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG, "datas.size()=" << datas.size());
|
||||
for (auto data: datas) {
|
||||
uint32_t length;
|
||||
m_continuous_in.read(reinterpret_cast<char*>(&length), sizeof(length));
|
||||
SG_LOG(SG_SYSTEMS, SG_BULK,
|
||||
"m_continuous_in.tellg()=" << m_continuous_in.tellg()
|
||||
<< " Skipping data_type=" << data->getStringValue()
|
||||
<< " length=" << length
|
||||
);
|
||||
m_continuous_in.seekg(length, std::ios_base::cur);
|
||||
if (length) {
|
||||
if (!strcmp(data->getStringValue(), "signals")) {
|
||||
frameinfo.has_signals = true;
|
||||
}
|
||||
else if (!strcmp(data->getStringValue(), "multiplayer")) {
|
||||
frameinfo.has_multiplayer = true;
|
||||
++num_frames_multiplayer;
|
||||
}
|
||||
else if (!strcmp(data->getStringValue(), "extra-properties")) {
|
||||
frameinfo.has_extra_properties = true;
|
||||
++num_frames_extra_properties;
|
||||
}
|
||||
}
|
||||
}
|
||||
SG_LOG(SG_SYSTEMS, SG_BULK, ""
|
||||
<< " pos=" << pos
|
||||
<< " sim_time=" << sim_time
|
||||
<< " num_frames_multiplayer=" << num_frames_multiplayer
|
||||
<< " num_frames_extra_properties=" << num_frames_extra_properties
|
||||
);
|
||||
|
||||
if (!m_continuous_in) {
|
||||
// EOF; we need to cope if last frame is incomplete, as
|
||||
// this can easily happen if Flightgear was killed while
|
||||
// recording.
|
||||
break;
|
||||
}
|
||||
|
||||
m_continuous_in_time_to_frameinfo[sim_time] = frameinfo;
|
||||
}
|
||||
t = time(NULL) - t;
|
||||
SG_LOG(SG_SYSTEMS, SG_ALERT, "Indexed uncompressed recording"
|
||||
<< ". time taken: " << t << "s"
|
||||
<< ". recording size: " << pos
|
||||
<< ". Number of frames: " << m_continuous_in_time_to_frameinfo.size()
|
||||
<< ". num_frames_multiplayer: " << num_frames_multiplayer
|
||||
<< ". num_frames_extra_properties: " << num_frames_extra_properties
|
||||
);
|
||||
fgSetInt("/sim/replay/continuous-stats-num-frames", m_continuous_in_time_to_frameinfo.size());
|
||||
fgSetInt("/sim/replay/continuous-stats-num-frames-extra-properties", num_frames_extra_properties);
|
||||
fgSetInt("/sim/replay/continuous-stats-num-frames-multiplayer", num_frames_multiplayer);
|
||||
start(true /*NewTape*/);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
m_pRecorder->reinit(m_continuous_in_config);
|
||||
clear();
|
||||
fillRecycler();
|
||||
m_continuous_in_time_last = -1;
|
||||
m_continuous_in_time_to_frameinfo.clear();
|
||||
m_num_frames_extra_properties = 0;
|
||||
m_num_frames_multiplayer = 0;
|
||||
m_continuous_indexing_in.open(Filename.str());
|
||||
m_continuous_indexing_pos = in.tellg();
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG, "filerequest=" << filerequest);
|
||||
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Filename=" << Filename);
|
||||
// Make an in-memory index of the recording.
|
||||
if (filerequest) {
|
||||
// Always call indexContinuousRecording once in case there is
|
||||
// nothing to download.
|
||||
indexContinuousRecording(nullptr, 1 /*Zero means EOF. */);
|
||||
filerequest->setCallback( [this](const void* data, size_t numbytes) {
|
||||
SG_LOG(SG_GENERAL, SG_BULK, "calling indexContinuousRecording() data=" << data << " numbytes=" << numbytes);
|
||||
indexContinuousRecording(data, numbytes);
|
||||
});
|
||||
}
|
||||
else {
|
||||
indexContinuousRecording(nullptr, 0);
|
||||
}
|
||||
start(true /*NewTape*/);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Not a continuous recording.
|
||||
in.close();
|
||||
if (filerequest) {
|
||||
SG_LOG(SG_SYSTEMS, SG_DEBUG, "Cannot load Filename=" << Filename << " because it is download but not Continuous recording");
|
||||
return false;
|
||||
}
|
||||
bool ok = true;
|
||||
|
||||
/* open input stream ********************************************/
|
||||
/* Open as a gzipped Normal recording. ********************************************/
|
||||
gzContainerReader input(Filename, FlightRecorderFileMagic);
|
||||
if (input.eof() || !input.good())
|
||||
{
|
||||
|
|
|
@ -34,6 +34,7 @@
|
|||
#include <simgear/props/props.hxx>
|
||||
#include <simgear/structure/subsystem_mgr.hxx>
|
||||
#include <simgear/io/iostreams/gzcontainerfile.hxx>
|
||||
#include <simgear/io/HTTPFileRequest.hxx>
|
||||
|
||||
#include <MultiPlayer/multiplaymgr.hxx>
|
||||
|
||||
|
@ -121,6 +122,27 @@ public:
|
|||
|
||||
bool saveTape(const SGPropertyNode* ConfigData);
|
||||
bool loadTape(const SGPropertyNode* ConfigData);
|
||||
|
||||
// If filerequest is set, the local file is a Continuous recording and
|
||||
// it might increase in size as downloading progresses, so we need to
|
||||
// incrementally index the file until the file request has finished the
|
||||
// download.
|
||||
//
|
||||
bool loadTape(
|
||||
const SGPath& Filename,
|
||||
bool Preview,
|
||||
SGPropertyNode& MetaMeta,
|
||||
simgear::HTTP::FileRequest* filerequest=nullptr
|
||||
);
|
||||
|
||||
// Attempts to load Continuous recording header properties into
|
||||
// <properties>. If in is null we use internal std::fstream, otherwise we
|
||||
// use *in.
|
||||
//
|
||||
// Returns 0 on success, +1 if we may succeed after further download, or -1
|
||||
// if recording is not a Continuous recording.
|
||||
//
|
||||
static int loadContinuousHeader(const std::string& path, std::istream* in, SGPropertyNode* properties);
|
||||
|
||||
private:
|
||||
void clear();
|
||||
|
@ -159,7 +181,16 @@ private:
|
|||
|
||||
bool listTapes(bool SameAircraftFilter, const SGPath& tapeDirectory);
|
||||
bool saveTape(const SGPath& Filename, SGPropertyNode_ptr MetaData);
|
||||
bool loadTape(const SGPath& Filename, bool Preview, SGPropertyNode& MetaMeta);
|
||||
|
||||
// Build up in-memory cache of simulator time to file offset, so we can
|
||||
// handle random access.
|
||||
//
|
||||
// We also cache any frames that modify extra-properties.
|
||||
//
|
||||
// Can be called multiple times, e.g. if recording is being downlaoded.
|
||||
//
|
||||
void indexContinuousRecording(const void* data, size_t numbytes);
|
||||
|
||||
SGPropertyNode_ptr continuousWriteHeader(
|
||||
std::ofstream& out,
|
||||
const SGPath& path,
|
||||
|
@ -215,9 +246,19 @@ private:
|
|||
std::ifstream m_continuous_in;
|
||||
bool m_continuous_in_multiplayer;
|
||||
bool m_continuous_in_extra_properties;
|
||||
std::mutex m_continuous_in_time_to_frameinfo_lock;
|
||||
std::map<double, FGFrameInfo> m_continuous_in_time_to_frameinfo;
|
||||
SGPropertyNode_ptr m_continuous_in_config;
|
||||
double m_continuous_in_time_last;
|
||||
|
||||
std::ifstream m_continuous_indexing_in;
|
||||
std::streampos m_continuous_indexing_pos;
|
||||
|
||||
// Only used for gathering statistics that are then written into
|
||||
// properties.
|
||||
//
|
||||
int m_num_frames_extra_properties = 0;
|
||||
int m_num_frames_multiplayer = 0;
|
||||
|
||||
// For writing uncompressed fgtape file.
|
||||
SGPropertyNode_ptr m_continuous_out_config;
|
||||
|
|
|
@ -974,7 +974,7 @@ void fgCreateSubsystems(bool duringReset) {
|
|||
throw sg_io_exception("Error loading materials file", mpath);
|
||||
}
|
||||
|
||||
// may exist already due to GUI startup
|
||||
// may exist already due to GUI startup or --load-tape=http...
|
||||
if (!globals->get_subsystem<FGHTTPClient>()) {
|
||||
globals->add_new_subsystem<FGHTTPClient>();
|
||||
}
|
||||
|
|
|
@ -42,6 +42,8 @@
|
|||
#include <string>
|
||||
#include <sstream>
|
||||
|
||||
#include <simgear/io/HTTPClient.hxx>
|
||||
#include <simgear/io/HTTPFileRequest.hxx>
|
||||
#include <simgear/math/sg_random.h>
|
||||
#include <simgear/props/props_io.hxx>
|
||||
#include <simgear/io/iostreams/sgstream.hxx>
|
||||
|
@ -78,6 +80,7 @@
|
|||
#include <Viewer/viewmgr.hxx>
|
||||
#include <Environment/presets.hxx>
|
||||
#include <Network/http/httpd.hxx>
|
||||
#include <Network/HTTPClient.hxx>
|
||||
#include "AircraftDirVisitorBase.hxx"
|
||||
|
||||
#include <osg/Version>
|
||||
|
@ -1558,45 +1561,173 @@ fgOptSetProperty(const char* raw)
|
|||
return ret ? FG_OPTIONS_OK : FG_OPTIONS_ERROR;
|
||||
}
|
||||
|
||||
/* If <url> is a URL, return suitable name for downloaded file. */
|
||||
static std::string urlToLocalPath(const char* url)
|
||||
{
|
||||
bool http = simgear::strutils::starts_with(url, "http://");
|
||||
bool https = simgear::strutils::starts_with(url, "https://");
|
||||
if (!http && !https) {
|
||||
return "";
|
||||
}
|
||||
// e.g. http://fg.com/foo/bar/wibble.fgtape
|
||||
const char* s2 = (http) ? url+7 : url+8; // fg.com/foo/bar/wibble.fgtape
|
||||
const char* s3 = strchr(s2, '/'); // /foo/bar/wibble.fgtape
|
||||
const char* s4 = (s3) ? strrchr(s3, '/') : NULL; // /wibble.fgtape
|
||||
std::string path;
|
||||
if (s3) path = std::string(s2, s3-s2); // fg.com
|
||||
path += '_'; // fg.com_
|
||||
if (s3 && s4 > s3) {
|
||||
path += simgear::strutils::md5(s3, s4-s3).substr(0, 8);
|
||||
path += '_'; // fg.com_12345678_
|
||||
}
|
||||
if (s4) path += (s4+1); // fg.com_12345678_wibble.fgtape
|
||||
if (!simgear::strutils::ends_with(path, ".fgtape")) path += ".fgtape";
|
||||
return path;
|
||||
}
|
||||
|
||||
|
||||
static int
|
||||
fgOptLoadTape(const char* arg)
|
||||
{
|
||||
// load a flight recorder tape but wait until the fdm is initialized
|
||||
class DelayedTapeLoader : SGPropertyChangeListener {
|
||||
public:
|
||||
DelayedTapeLoader( const char * tape ) :
|
||||
_tape(SGPath::fromUtf8(tape))
|
||||
{
|
||||
SGPropertyNode_ptr n = fgGetNode("/sim/signals/fdm-initialized", true);
|
||||
n->addChangeListener( this );
|
||||
// load a flight recorder tape but wait until the fdm is initialized.
|
||||
//
|
||||
struct DelayedTapeLoader : SGPropertyChangeListener {
|
||||
|
||||
DelayedTapeLoader( const char * tape, simgear::HTTP::FileRequest* filerequest) :
|
||||
_tape(SGPath::fromUtf8(tape)),
|
||||
_filerequest(filerequest)
|
||||
{
|
||||
fgGetNode("/sim/signals/fdm-initialized", true)->addChangeListener( this );
|
||||
}
|
||||
|
||||
virtual ~ DelayedTapeLoader() {}
|
||||
|
||||
virtual void valueChanged(SGPropertyNode * node)
|
||||
{
|
||||
if (!fgGetBool("/sim/signals/fdm-initialized")) {
|
||||
return;
|
||||
}
|
||||
fgGetNode("/sim/signals/fdm-initialized", true)->removeChangeListener( this );
|
||||
|
||||
// tell the replay subsystem to load the tape
|
||||
FGReplay* replay = globals->get_subsystem<FGReplay>();
|
||||
assert(replay);
|
||||
SGPropertyNode_ptr arg = new SGPropertyNode();
|
||||
arg->setStringValue("tape", _tape.utf8Str() );
|
||||
arg->setBoolValue( "same-aircraft", 0 );
|
||||
if (!replay->loadTape(_tape, false /*preview*/, *arg, _filerequest)) {
|
||||
// Force shutdown if we can't load tape specified on command-line.
|
||||
SG_LOG(SG_GENERAL, SG_POPUP, "Exiting because unable to load fgtape: " << _tape.str());
|
||||
flightgear::modalMessageBox("Exiting because unable to load fgtape", _tape.str(), "");
|
||||
fgOSExit(1);
|
||||
}
|
||||
delete this; // commence suicide
|
||||
}
|
||||
private:
|
||||
SGPath _tape;
|
||||
simgear::HTTP::FileRequest* _filerequest;
|
||||
};
|
||||
|
||||
SGPropertyNode_ptr properties(new SGPropertyNode);
|
||||
simgear::HTTP::FileRequest* filerequest = nullptr;
|
||||
|
||||
std::string path = urlToLocalPath(arg);
|
||||
if (path == "") {
|
||||
// <arg> is a local file.
|
||||
//
|
||||
// Load the recording's header if it is a Continuous recording.
|
||||
//
|
||||
(void) FGReplay::loadContinuousHeader(arg, nullptr /*in*/, properties);
|
||||
}
|
||||
else {
|
||||
// <arg> is a URL. Start download.
|
||||
//
|
||||
// Load the recording's header if it is a Continuous recording.
|
||||
//
|
||||
// This is a little messy - we need to create a FGHTTPClient subsystem
|
||||
// in order to do the download, and we call its update() method
|
||||
// directly in order to download at least the header.
|
||||
//
|
||||
const char* url = arg;
|
||||
FGHTTPClient* http = globals->add_new_subsystem<FGHTTPClient>();
|
||||
http->init();
|
||||
filerequest = new simgear::HTTP::FileRequest(url, path, true /*append*/);
|
||||
long max_download_speed = fgGetLong("/sim/replay/download-max-bytes-per-sec");
|
||||
if (max_download_speed != 0) {
|
||||
// Can be useful to limite download speed for testing background
|
||||
// download.
|
||||
//
|
||||
SG_LOG(SG_GENERAL, SG_ALERT, "Limiting download speed"
|
||||
<< " /sim/replay/download-max-bytes-per-sec=" << max_download_speed
|
||||
);
|
||||
filerequest->setMaxBytesPerSec(max_download_speed);
|
||||
}
|
||||
http->client()->makeRequest(filerequest);
|
||||
SG_LOG(SG_GENERAL, SG_DEBUG, ""
|
||||
<< " filerequest->responseCode()=" << filerequest->responseCode()
|
||||
<< " filerequest->responseReason()=" << filerequest->responseReason()
|
||||
);
|
||||
|
||||
// Load recording header, looping so that we wait for the initial
|
||||
// portion of the recording to be downloaded. We give up after a fixed
|
||||
// timeout.
|
||||
//
|
||||
time_t t0 = time(NULL);
|
||||
for(;;) {
|
||||
// Run http client's update() to download any pending data.
|
||||
http->update(0);
|
||||
|
||||
// Try to load properties from recording header.
|
||||
int e = FGReplay::loadContinuousHeader(path, nullptr /*in*/, properties);
|
||||
if (e == 0) {
|
||||
// Success. We leave <filerequest> active - it will carry
|
||||
// on downloading when the main update loop gets going
|
||||
// later. Hopefully the delay before that happens will not
|
||||
// cause a server timeout.
|
||||
//
|
||||
break;
|
||||
}
|
||||
if (e == -1) {
|
||||
SG_LOG(SG_GENERAL, SG_POPUP, "Not a Continuous recording: url=" << url << " local filename=" << path);
|
||||
// Replay from URL only works with Continuous recordings.
|
||||
return FG_OPTIONS_EXIT;
|
||||
}
|
||||
|
||||
// If we get here, need to download some more.
|
||||
if (time(NULL) - t0 > 30) {
|
||||
SG_LOG(SG_GENERAL, SG_POPUP, "Timeout while reading downloaded recording from " << url << ". local path=" << path);
|
||||
return FG_OPTIONS_EXIT;
|
||||
}
|
||||
sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Set aircraft from recording header if we loaded it above; this has to
|
||||
// happen now, before the FDM is initialised. Also set the airport; we
|
||||
// don't actually have to do this because the replay doesn't need terrain
|
||||
// to work, but we might as well load the correct terrain.
|
||||
//
|
||||
std::string aircraft = properties->getStringValue("meta/aircraft-type");
|
||||
std::string airport = properties->getStringValue("meta/closest-airport-id");
|
||||
SG_LOG(SG_GENERAL, SG_ALERT, "From recording header: aircraft=" << aircraft << " airport=" << airport);
|
||||
if (aircraft != "") {
|
||||
// Force --aircraft and --airport options to use values from the
|
||||
// recording.
|
||||
//
|
||||
Options::sharedInstance()->setOption("aircraft", aircraft);
|
||||
}
|
||||
if (airport != "") {
|
||||
// Looks like setting --airport option doesn't work - we need to call
|
||||
// fgOptAirport() directly.
|
||||
//
|
||||
Options::sharedInstance()->setOption("airport", airport);
|
||||
fgOptAirport(airport.c_str());
|
||||
}
|
||||
|
||||
virtual ~ DelayedTapeLoader() {}
|
||||
|
||||
virtual void valueChanged(SGPropertyNode * node)
|
||||
{
|
||||
node->removeChangeListener( this );
|
||||
|
||||
// tell the replay subsystem to load the tape
|
||||
FGReplay* replay = globals->get_subsystem<FGReplay>();
|
||||
SGPropertyNode_ptr arg = new SGPropertyNode();
|
||||
arg->setStringValue("tape", _tape.utf8Str() );
|
||||
arg->setBoolValue( "same-aircraft", 0 );
|
||||
if (!replay->loadTape(arg)) {
|
||||
// Force shutdown.
|
||||
SG_LOG(SG_GENERAL, SG_POPUP, "Exiting because unable to load fgtape: " << _tape.str());
|
||||
flightgear::modalMessageBox("Exiting because unable to load fgtape", _tape.str(), "");
|
||||
fgOSExit(1);
|
||||
}
|
||||
delete this; // commence suicide
|
||||
}
|
||||
private:
|
||||
SGPath _tape;
|
||||
|
||||
};
|
||||
|
||||
new DelayedTapeLoader(arg);
|
||||
return FG_OPTIONS_OK;
|
||||
// Arrange to load the recording after FDM has initialised.
|
||||
new DelayedTapeLoader(path.c_str(), filerequest);
|
||||
|
||||
return FG_OPTIONS_OK;
|
||||
}
|
||||
|
||||
static int fgOptDisableGUI(const char*)
|
||||
|
|
Loading…
Reference in a new issue