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);
|
replayMessage(time);
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> lock(m_continuous_in_time_to_frameinfo_lock);
|
||||||
|
|
||||||
if (!m_continuous_in_time_to_frameinfo.empty()) {
|
if (!m_continuous_in_time_to_frameinfo.empty()) {
|
||||||
// We are replaying a continuous recording.
|
// We are replaying a continuous recording.
|
||||||
//
|
//
|
||||||
|
@ -1615,6 +1617,7 @@ void FGReplay::replay(
|
||||||
double
|
double
|
||||||
FGReplay::get_start_time()
|
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()) {
|
if (!m_continuous_in_time_to_frameinfo.empty()) {
|
||||||
double ret = m_continuous_in_time_to_frameinfo.begin()->first;
|
double ret = m_continuous_in_time_to_frameinfo.begin()->first;
|
||||||
SG_LOG(SG_SYSTEMS, SG_DEBUG,
|
SG_LOG(SG_SYSTEMS, SG_DEBUG,
|
||||||
|
@ -1645,6 +1648,7 @@ FGReplay::get_start_time()
|
||||||
double
|
double
|
||||||
FGReplay::get_end_time()
|
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()) {
|
if (!m_continuous_in_time_to_frameinfo.empty()) {
|
||||||
double ret = m_continuous_in_time_to_frameinfo.rbegin()->first;
|
double ret = m_continuous_in_time_to_frameinfo.rbegin()->first;
|
||||||
SG_LOG(SG_SYSTEMS, SG_DEBUG,
|
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.
|
/** Read a flight recorder tape with given filename from disk.
|
||||||
* Copies MetaData's "meta" node into MetaMeta out-param.
|
* Copies MetaData's "meta" node into MetaMeta out-param.
|
||||||
* Actual data and signal configuration is not read when in "Preview" mode.
|
* Actual data and signal configuration is not read when in "Preview" mode.
|
||||||
*/
|
*/
|
||||||
bool
|
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);
|
SG_LOG(SG_SYSTEMS, SG_DEBUG, "loading Preview=" << Preview << " Filename=" << Filename);
|
||||||
{
|
|
||||||
/* Try to load as uncompressed Continuous recording first. */
|
/* Try to load as uncompressed Continuous recording first. */
|
||||||
std::ifstream in_preview;
|
std::ifstream in_preview;
|
||||||
std::ifstream& in(Preview ? in_preview : m_continuous_in);
|
std::ifstream& in(Preview ? in_preview : m_continuous_in);
|
||||||
in.open( Filename.str());
|
in.open(Filename.str());
|
||||||
if (!in) {
|
if (!in) {
|
||||||
SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to open Filename=" << Filename);
|
SG_LOG(SG_SYSTEMS, SG_ALERT, "Failed to open"
|
||||||
return false;
|
<< " Filename=" << Filename.str()
|
||||||
}
|
<< " in.is_open()=" << in.is_open()
|
||||||
std::vector<char> buffer(strlen( FlightRecorderFileMagic) + 1);
|
);
|
||||||
in.read(&buffer.front(), buffer.size());
|
return false;
|
||||||
if (strcmp(&buffer.front(), FlightRecorderFileMagic)) {
|
}
|
||||||
SG_LOG(SG_SYSTEMS, SG_ALERT, "fgtape prefix doesn't match FlightRecorderFileMagic: '" << &buffer.front() << "'");
|
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();
|
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;
|
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;
|
bool ok = true;
|
||||||
|
|
||||||
/* open input stream ********************************************/
|
/* Open as a gzipped Normal recording. ********************************************/
|
||||||
gzContainerReader input(Filename, FlightRecorderFileMagic);
|
gzContainerReader input(Filename, FlightRecorderFileMagic);
|
||||||
if (input.eof() || !input.good())
|
if (input.eof() || !input.good())
|
||||||
{
|
{
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
#include <simgear/props/props.hxx>
|
#include <simgear/props/props.hxx>
|
||||||
#include <simgear/structure/subsystem_mgr.hxx>
|
#include <simgear/structure/subsystem_mgr.hxx>
|
||||||
#include <simgear/io/iostreams/gzcontainerfile.hxx>
|
#include <simgear/io/iostreams/gzcontainerfile.hxx>
|
||||||
|
#include <simgear/io/HTTPFileRequest.hxx>
|
||||||
|
|
||||||
#include <MultiPlayer/multiplaymgr.hxx>
|
#include <MultiPlayer/multiplaymgr.hxx>
|
||||||
|
|
||||||
|
@ -121,6 +122,27 @@ public:
|
||||||
|
|
||||||
bool saveTape(const SGPropertyNode* ConfigData);
|
bool saveTape(const SGPropertyNode* ConfigData);
|
||||||
bool loadTape(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:
|
private:
|
||||||
void clear();
|
void clear();
|
||||||
|
@ -159,7 +181,16 @@ private:
|
||||||
|
|
||||||
bool listTapes(bool SameAircraftFilter, const SGPath& tapeDirectory);
|
bool listTapes(bool SameAircraftFilter, const SGPath& tapeDirectory);
|
||||||
bool saveTape(const SGPath& Filename, SGPropertyNode_ptr MetaData);
|
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(
|
SGPropertyNode_ptr continuousWriteHeader(
|
||||||
std::ofstream& out,
|
std::ofstream& out,
|
||||||
const SGPath& path,
|
const SGPath& path,
|
||||||
|
@ -215,9 +246,19 @@ private:
|
||||||
std::ifstream m_continuous_in;
|
std::ifstream m_continuous_in;
|
||||||
bool m_continuous_in_multiplayer;
|
bool m_continuous_in_multiplayer;
|
||||||
bool m_continuous_in_extra_properties;
|
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;
|
std::map<double, FGFrameInfo> m_continuous_in_time_to_frameinfo;
|
||||||
SGPropertyNode_ptr m_continuous_in_config;
|
SGPropertyNode_ptr m_continuous_in_config;
|
||||||
double m_continuous_in_time_last;
|
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.
|
// For writing uncompressed fgtape file.
|
||||||
SGPropertyNode_ptr m_continuous_out_config;
|
SGPropertyNode_ptr m_continuous_out_config;
|
||||||
|
|
|
@ -974,7 +974,7 @@ void fgCreateSubsystems(bool duringReset) {
|
||||||
throw sg_io_exception("Error loading materials file", mpath);
|
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>()) {
|
if (!globals->get_subsystem<FGHTTPClient>()) {
|
||||||
globals->add_new_subsystem<FGHTTPClient>();
|
globals->add_new_subsystem<FGHTTPClient>();
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,6 +42,8 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
|
#include <simgear/io/HTTPClient.hxx>
|
||||||
|
#include <simgear/io/HTTPFileRequest.hxx>
|
||||||
#include <simgear/math/sg_random.h>
|
#include <simgear/math/sg_random.h>
|
||||||
#include <simgear/props/props_io.hxx>
|
#include <simgear/props/props_io.hxx>
|
||||||
#include <simgear/io/iostreams/sgstream.hxx>
|
#include <simgear/io/iostreams/sgstream.hxx>
|
||||||
|
@ -78,6 +80,7 @@
|
||||||
#include <Viewer/viewmgr.hxx>
|
#include <Viewer/viewmgr.hxx>
|
||||||
#include <Environment/presets.hxx>
|
#include <Environment/presets.hxx>
|
||||||
#include <Network/http/httpd.hxx>
|
#include <Network/http/httpd.hxx>
|
||||||
|
#include <Network/HTTPClient.hxx>
|
||||||
#include "AircraftDirVisitorBase.hxx"
|
#include "AircraftDirVisitorBase.hxx"
|
||||||
|
|
||||||
#include <osg/Version>
|
#include <osg/Version>
|
||||||
|
@ -1558,45 +1561,173 @@ fgOptSetProperty(const char* raw)
|
||||||
return ret ? FG_OPTIONS_OK : FG_OPTIONS_ERROR;
|
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
|
static int
|
||||||
fgOptLoadTape(const char* arg)
|
fgOptLoadTape(const char* arg)
|
||||||
{
|
{
|
||||||
// load a flight recorder tape but wait until the fdm is initialized
|
// load a flight recorder tape but wait until the fdm is initialized.
|
||||||
class DelayedTapeLoader : SGPropertyChangeListener {
|
//
|
||||||
public:
|
struct DelayedTapeLoader : SGPropertyChangeListener {
|
||||||
DelayedTapeLoader( const char * tape ) :
|
|
||||||
_tape(SGPath::fromUtf8(tape))
|
DelayedTapeLoader( const char * tape, simgear::HTTP::FileRequest* filerequest) :
|
||||||
{
|
_tape(SGPath::fromUtf8(tape)),
|
||||||
SGPropertyNode_ptr n = fgGetNode("/sim/signals/fdm-initialized", true);
|
_filerequest(filerequest)
|
||||||
n->addChangeListener( this );
|
{
|
||||||
|
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() {}
|
// Arrange to load the recording after FDM has initialised.
|
||||||
|
new DelayedTapeLoader(path.c_str(), filerequest);
|
||||||
virtual void valueChanged(SGPropertyNode * node)
|
|
||||||
{
|
return FG_OPTIONS_OK;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int fgOptDisableGUI(const char*)
|
static int fgOptDisableGUI(const char*)
|
||||||
|
|
Loading…
Add table
Reference in a new issue